mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
1 Commits
v1.4.0
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b630f8d5 |
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,8 +31,9 @@ body:
|
||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||
and provide a link to the applicable build in the field below.
|
||||
options:
|
||||
- v1.3.1
|
||||
- v1.2.1
|
||||
- Test build
|
||||
- Development build
|
||||
- type: input
|
||||
attributes:
|
||||
label: Test/Development build
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
3
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,8 +39,9 @@ body:
|
||||
If the bug was found in a test/development build, select "Test build" or "Development build"
|
||||
and provide a link to the applicable build in the field below.
|
||||
options:
|
||||
- 1.3.1
|
||||
- 1.0.0
|
||||
- Test build
|
||||
- Development build
|
||||
- type: input
|
||||
attributes:
|
||||
label: Test/Development build
|
||||
|
||||
4
.github/actions/build-app/action.yaml
vendored
4
.github/actions/build-app/action.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Build Retribution package
|
||||
description: Assembles the full Retribution application.
|
||||
name: Build Liberation package
|
||||
description: Assembles the full Liberation application.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Retribution JS set-up
|
||||
description: Sets up the Retribution Javascript environment.
|
||||
name: Liberation JS set-up
|
||||
description: Sets up the Liberation Javascript environment.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name: Retribution Python set-up
|
||||
description: Sets up the Retribution Python environment.
|
||||
name: Liberation Python set-up
|
||||
description: Sets up the Liberation Python environment.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
|
||||
- name: Install environment
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
Compress-Archive -Path .\dist\dcs-retribution\ -DestinationPath
|
||||
dist\dcs-retribution.zip
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dcs-retribution
|
||||
path: dist/dcs-retribution.zip
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: github.repository == 'dcs-retribution/dcs-retribution' && startsWith(github.ref_name, 'test')
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
|
||||
- name: Send status to Discord
|
||||
uses: stegzilla/discord-notify@v2
|
||||
@@ -60,4 +60,4 @@ jobs:
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: github.repository == 'dcs-retribution/dcs-retribution' && github.ref_name == 'dev'
|
||||
if: github.ref_name == 'dev'
|
||||
|
||||
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
with:
|
||||
src: "."
|
||||
options: "--check"
|
||||
version: "23.9.1"
|
||||
|
||||
mypy:
|
||||
name: Type checking
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
release: true
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dcs-retribution
|
||||
path: dist/
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
needs: [build]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dcs-retribution
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,5 @@ env/
|
||||
|
||||
/logs/
|
||||
/resources/logging.yaml
|
||||
/resources/plugins/pretense/pretense_output.lua
|
||||
|
||||
*.psd
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.8.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
||||
|
||||
[](https://patreon.com/dcsliberation)
|
||||
[](https://patreon.com/khopa)
|
||||
|
||||
[](https://github.com/dcs-retribution/dcs-retribution/releases)
|
||||
|
||||
@@ -90,7 +90,5 @@ Excellent lua scripts DCS Liberation/Retribution uses as plugins:
|
||||
|
||||
* For the JTAC feature, DCS Retribution embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||
* Carstens Arty Spotter https://www.digitalcombatsimulator.com/en/files/3339128/ is an amazing force multiplyer to drop the hammer on enemies.
|
||||
* MBot's [Call Artillery Script](https://forum.dcs.world/topic/310506-call-artillery-script/) uses in-map artillery and forward observers to enable artillery fire missions.
|
||||
|
||||
Please also show some support to these projects !
|
||||
|
||||
87
changelog.md
87
changelog.md
@@ -1,79 +1,3 @@
|
||||
# Retribution v1.4.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Payload Editor]** Ability to configure liveries on flight/flight-member level
|
||||
* **[Factions]** Support for definitions in yml/yaml format
|
||||
* **[Campaigns/Factions]** Support for inline recommended faction in campaign's yaml file
|
||||
* **[Squadrons]** Ability to define a livery-set for each squadron from which Retribution will randomly choose during mission generation
|
||||
* **[Modding]** Updated support for F/A-18E/F/G mod version 2.2.5
|
||||
* **[Modding]** Added VSN F-106 Delta Dart mod support (v2.9.4.101)
|
||||
* **[Modding]** Added OH-6 Cayuse (v1.2) mod support, including the Vietnam Asset Pack v1.0
|
||||
* **[Modding]** Added VSN EA-6B Prowler mod support (v2.9.4.102)
|
||||
* **[Modding]** Added tripod3 Cold War assets mod support (v1.2)
|
||||
* **[Modding]** Added VSN Mirage III mod support (2.5.7.01)
|
||||
* **[Campaign Setup]** Allow adjustments to naval TGOs (except carriers) on turn 0
|
||||
* **[Campaign Design]** Ability to configure specific carrier names & types in campaign's yaml file
|
||||
* **[Mission Generation]** Ability to inject custom kneeboards
|
||||
* **[Options]** Extend option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Ramon Airbase, similar to the Nevatim fix in Retribution 1.3.0
|
||||
* **[Options]** New option in Settings: Default start type for Player flights.
|
||||
* **[AirWing]** Expose OPFOR Squadrons, giving the ability to change liveries, auto-assignable mission types & an easy way to retrieve debug information.
|
||||
* **[ATO]** Allow planning as OPFOR
|
||||
* **[Campaign Design]** Support for latest maps (Kola, Afghanistan, Iraq)
|
||||
* **[UI]** Zoom level retained when switching campaigns
|
||||
* **[UX]** Allow changing squadrons in flight's edit dialog
|
||||
* **[Cheats]** Sink/Resurrect carriers instead of showing an error during cheat-capture (use AWCD-cheat to add squadrons upon resurrection)
|
||||
* **[UI/UX]** Allow changing conditions such as Time, Date & Weather
|
||||
* **[Modding]** Added support for Su-15 Flagon mod (v1.0)
|
||||
* **[Plugins]** Support for Carsten's Arty Spotter script
|
||||
* **[Plugins]** Support for MBot's Call Artillery script (using on-map artillery)
|
||||
* **[Modding]** Added support for SK-60 mod (v1.2.1)
|
||||
* **[Mission Generation]** Introducing the Armed Recon flight plan, i.e. CAS against any Theater Ground Object
|
||||
* **[Doctrine]** Ability to customize the startup time allocated to the player
|
||||
* **[Mission Generation]** Ability to choose whether player flights can spawn on the sixpack or not
|
||||
* **[Options]** New options in Mission Generator section: Limit AI radio callouts & Suppress AI radio callouts.
|
||||
* **[Options]** New option to use the combat landing flag in the landing waypoint task for helicopters.
|
||||
* **[UI/UX]** Sync package waypoints when primary flight's waypoints are updated and recreate other flights within the package to ensure JOIN, INGRESS & SPLIT are synced
|
||||
* **[UI/UX]** Allow changing loadout on flight creation
|
||||
* **[UI]** Display TOT for all waypoints in the flight plan
|
||||
* **[UI]** Edit basic datalink properties for applicable aircraft
|
||||
* **[Mission Generation]** Automatic datalink network setup for applicable aircraft (_should_ in theory avoid the need to re-save the mission)
|
||||
* **[Options]** New option to force-enable deck-crew for super-carriers on dedicated server.
|
||||
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
|
||||
* **[UX]** Default settings are now loaded from Default.zip
|
||||
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
|
||||
* **[Package Planning]** Ability to plan recovery tanker flights
|
||||
* **[Modding]** Support for Bandit's cloud presets mod (v15)
|
||||
* **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap
|
||||
* **[New Game Wizard]** Ability to save an edited faction during new game creation
|
||||
* **[Options]** New option to make AI helicopters prefer vertical takeoff and landing
|
||||
* **[Campaign Design/Mission Generation]** Introduction of "rebel zones" which randomly spawn units according to the campaign's definitions.
|
||||
* **[Mission Generation]** Missile sites now fire at random times instead of all at the beginning of the mission
|
||||
* **[Modding]** Support for CurrentHill's Chinese Asset Pack (v1.1.4)
|
||||
* **[Modding]** Updated support for CurrentHill's Swedish Asset Pack (v1.1.0)
|
||||
* **[Modding]** Support for CurrentHill's Russian Asset Pack (v1.2.0)
|
||||
* **[Modding]** Support for CurrentHill's USA Asset Pack (v1.1.5)
|
||||
* **[Modding]** Update CJS Super Hornet to 2.4.2
|
||||
* **[Modding]** Support for Cowboy's E-7A Wedgetail mod (Supports EW Script Offensive Jamming)
|
||||
* **[Modding]** Support for szcz's MiG-31BM (v2.0)
|
||||
* **[Plugins]** Added initial AI support for EW Script 2.0
|
||||
* **[Options]** Ability to configure certain forced-options via a file (WIP: https://github.com/dcs-retribution/dcs-retribution/issues/490).
|
||||
|
||||
## Fixes
|
||||
* **[UI/UX]** A-10A flights can be edited again
|
||||
* **[Mission Generation]** IADS bug sometimes triggering "no skynet usable units" error during mission generation
|
||||
* **[New Game Wizard]** Campaign errors show a dialog again and avoid CTDs
|
||||
* **[UI]** Landmap wasn't updating when switching to a different theater
|
||||
* **[Mission Results Processor]** Squadrons of a sunken carrier are now disbanded
|
||||
* **[Mission Generation]** Introduced option to switch alt-type to AMSL during mission generation to avoid helicopters wanting to submerge over certain parts of the sea.
|
||||
|
||||
# Retribution v1.3.1
|
||||
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
|
||||
|
||||
## Fixes
|
||||
* **[UX]** Fix save-compatibility issue
|
||||
* **[UX]** Avoid crash on startup due to incompatible save
|
||||
|
||||
|
||||
# Retribution v1.3.0
|
||||
#### Note: Re-save your missions in DCS' Mission Editor to avoid possible crashes due to datalink (usually the case when F-16C blk50s are used) when hosting missions on a dedicated server.
|
||||
|
||||
@@ -91,8 +15,6 @@
|
||||
* **[Modding]** Updated support for F-4B/C Phantom mod to 2.8.7.204
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.2.0 release.
|
||||
* **[Modding]** Added F/A-18E/F Super Hornet AI Tanker mod support (Chiller Juice Studios SuperBug Tanker AI version 1.4)
|
||||
* **[Modding]** Added VSN Super Étendard mod support (v2.5.5)
|
||||
* **[Modding]** Added F9F Panther mod support (version v2.8.7.101)
|
||||
* **[Modding]** Updated Irondome support to IDF Assets Pack V1.1, adding support for the David's Sling
|
||||
* **[Radios]** Added HF-FM band for AN/ARC-222
|
||||
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
|
||||
@@ -116,16 +38,10 @@
|
||||
* **[Mission Generator]** Set F-14's IP waypoint according to the flight-plan's ingress point
|
||||
* **[Mission Generator]** Automatically de-spawn aircraft when arrival/divert is an off-map spawn
|
||||
* **[Options]** Option to de-spawn AI flights in the air if their start-type was manually set to In-Flight
|
||||
* **[Campaign Design]** Ability to add separate ground spawns for C-130 and other large aircraft to campaigns.
|
||||
* **[Config]** Preference setting to use custom Liberation payloads instead of prioritizing Retribution's default
|
||||
* **[Config]** Preference setting to configure the server-port on which Retribution's back-end will run
|
||||
* **[Options]** Made AI jettisoning empty fuel tanks optional (disabled by default)
|
||||
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/
|
||||
* **[Cheat]** Add cheat option to manually manage REDFOR's TGOs
|
||||
* **[UX]** Buy/Replace TGOs for free before the campaign has started
|
||||
* **[Data]** Ability to define "cruise" & "combat" altitudes for airplanes
|
||||
* **[Options]** Option to randomize altitudes for flights with airplanes
|
||||
* **[Options]** Options to configure/override maximum mission distance for airplanes & helicopters
|
||||
* **[Options]** Add option (so it can be disabled when fixed in DCS) to force air-starts (except for the slots that work) at Nevatim due to https://forum.dcs.world/topic/335545-29-nevatim-ramp-starts-still-bugged/.
|
||||
|
||||
## Fixes
|
||||
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
|
||||
@@ -351,7 +267,6 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
|
||||
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
|
||||
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind pacakge" offset.
|
||||
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
|
||||
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
|
||||
|
||||
# 8.1.0
|
||||
|
||||
|
||||
17881
client/package-lock.json
generated
17881
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,26 +6,26 @@
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^18.19.26",
|
||||
"@types/react": "^18.2.69",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-redux": "^7.1.33",
|
||||
"axios": "^1.7.4",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/jest": "^29.1.2",
|
||||
"@types/node": "^18.8.3",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"axios": "^1.6.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esri-leaflet": "^3.0.12",
|
||||
"leaflet": "^1.9.4",
|
||||
"esri-leaflet": "^3.0.8",
|
||||
"leaflet": "^1.9.2",
|
||||
"leaflet-ruler": "^1.0.0",
|
||||
"milsymbol": "^2.2.0",
|
||||
"milsymbol": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-esri-leaflet": "^2.0.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-leaflet": "^4.1.0",
|
||||
"react-redux": "^8.0.4",
|
||||
"redux-logger": "^3.0.6",
|
||||
"typescript": "~4.8.4"
|
||||
},
|
||||
@@ -61,20 +61,20 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rtk-query/codegen-openapi": "^1.2.0",
|
||||
"@rtk-query/codegen-openapi": "^1.0.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@types/websocket": "^1.0.10",
|
||||
"electron": "^22.3.27",
|
||||
"@types/leaflet": "^1.8.0",
|
||||
"@types/redux-logger": "^3.0.9",
|
||||
"@types/websocket": "^1.0.5",
|
||||
"electron": "^22.3.25",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"generate-license-file": "^2.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"nth-check": "^2.1.1",
|
||||
"nth-check": "^2.0.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"wait-on": "^7.2.0"
|
||||
"ts-node": "^10.9.1",
|
||||
"wait-on": "^6.0.1"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
|
||||
@@ -466,7 +466,6 @@ export type Game = {
|
||||
navmeshes: NavMeshes;
|
||||
map_center?: LatLng;
|
||||
unculled_zones: UnculledZone[];
|
||||
map_zones: MapZones;
|
||||
};
|
||||
export type MapZones = {
|
||||
inclusion: LatLng[][][];
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { RootState } from "../app/store";
|
||||
import { gameLoaded, gameUnloaded } from "./actions";
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { MapZones } from "./_liberationApi";
|
||||
|
||||
interface MapZonesState {
|
||||
mapZones: MapZones;
|
||||
}
|
||||
|
||||
const initialState: MapZonesState = {
|
||||
mapZones: { inclusion: [], exclusion: [], sea: [] },
|
||||
};
|
||||
|
||||
const mapZonesSlice = createSlice({
|
||||
name: "map",
|
||||
initialState: initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(gameLoaded, (state, action) => {
|
||||
if (action.payload != null) {
|
||||
state.mapZones.exclusion = action.payload.map_zones.exclusion;
|
||||
state.mapZones.inclusion = action.payload.map_zones.inclusion;
|
||||
state.mapZones.sea = action.payload.map_zones.sea;
|
||||
}
|
||||
});
|
||||
builder.addCase(gameUnloaded, (state) => {
|
||||
state.mapZones = initialState.mapZones;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const selectMapZones = (state: RootState) => state.mapZones;
|
||||
|
||||
export default mapZonesSlice.reducer;
|
||||
@@ -10,7 +10,6 @@ import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
||||
import tgosReducer from "../api/tgosSlice";
|
||||
import threatZonesReducer from "../api/threatZonesSlice";
|
||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||
import mapZonesReducer from "../api/mapZonesSlice";
|
||||
import {
|
||||
Action,
|
||||
PreloadedState,
|
||||
@@ -32,7 +31,6 @@ const rootReducer = combineReducers({
|
||||
threatZones: threatZonesReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
unculledZones: unculledZonesReducer,
|
||||
mapZones: mapZonesReducer,
|
||||
});
|
||||
|
||||
export function setupStore(preloadedState?: PreloadedState<RootState>) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Flight } from "../../api/liberationApi";
|
||||
import { Icon, Point } from "leaflet";
|
||||
import ms from "milsymbol";
|
||||
import { Symbol } from "milsymbol";
|
||||
import { Marker } from "react-leaflet";
|
||||
|
||||
function iconForFlight(flight: Flight) {
|
||||
const symbol = new ms.Symbol(flight.sidc, {
|
||||
const symbol = new Symbol(flight.sidc, {
|
||||
size: 20,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ControlPoint } from "../../api/_liberationApi";
|
||||
import { Icon, Point } from "leaflet";
|
||||
import ms from "milsymbol";
|
||||
import { Symbol } from "milsymbol";
|
||||
|
||||
export const iconForControlPoint = (cp: ControlPoint) => {
|
||||
const symbol = new ms.Symbol(cp.sidc, {
|
||||
const symbol = new Symbol(cp.sidc, {
|
||||
size: 24,
|
||||
colorMode: "Dark",
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Flight } from "../../api/liberationApi";
|
||||
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
||||
import WaypointMarker from "../waypointmarker";
|
||||
import { Polyline as LPolyline } from "leaflet";
|
||||
import { ReactElement, useEffect, useRef } from "react";
|
||||
import { ReactElement } from "react";
|
||||
import { Polyline } from "react-leaflet";
|
||||
|
||||
const BLUE_PATH = "#0084ff";
|
||||
@@ -28,44 +27,16 @@ const pathColor = (props: FlightPlanProps) => {
|
||||
function FlightPlanPath(props: FlightPlanProps) {
|
||||
const color = pathColor(props);
|
||||
const waypoints = props.flight.waypoints;
|
||||
|
||||
const polylineRef = useRef<LPolyline | null>(null);
|
||||
|
||||
// Flight paths should be drawn under everything else. There seems to be an
|
||||
// issue where `interactive: false` doesn't do as its told (there's nuance,
|
||||
// see the bug for details). It looks better if we draw the other elements on
|
||||
// top of the flight plans anyway, so just push the flight plan to the back.
|
||||
//
|
||||
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
|
||||
//
|
||||
// It's not possible to z-index a polyline (and leaflet says it never will be,
|
||||
// because this is a limitation of SVG, not leaflet:
|
||||
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
|
||||
// bringToBack() to push the flight paths to the back of the drawing once
|
||||
// they've been added to the map. They'll still draw on top of the map, but
|
||||
// behind everything than was added before them. Anything added after always
|
||||
// goes on top.
|
||||
useEffect(() => {
|
||||
if (props.selected) {
|
||||
polylineRef.current?.bringToFront();
|
||||
}
|
||||
else {
|
||||
polylineRef.current?.bringToBack();
|
||||
}
|
||||
});
|
||||
|
||||
if (waypoints == null) {
|
||||
return <></>;
|
||||
}
|
||||
const points = waypoints
|
||||
.filter((waypoint) => waypoint.include_in_path)
|
||||
.map((waypoint) => waypoint.position);
|
||||
|
||||
return (
|
||||
<Polyline
|
||||
positions={points}
|
||||
pathOptions={{ color: color, interactive: false }}
|
||||
ref={polylineRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,12 +95,8 @@ describe("FlightPlansLayer", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// For some reason passing ref to PolyLine causes it and its group to be
|
||||
// redrawn, so these numbers don't match what you'd expect from the test.
|
||||
// It probably needs to be rewritten without mocks.
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(3);
|
||||
expect(mockLayerGroup).toBeCalledTimes(2);
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn if wrong coalition", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
@@ -182,7 +178,7 @@ describe("FlightPlansLayer", () => {
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn when only selected flights are to be drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer selectedOnly />, {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
@@ -355,7 +351,7 @@ describe("FlightPlansLayer", () => {
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn if red", () => {
|
||||
renderWithProviders(<FlightPlansLayer selectedOnly />, {
|
||||
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
@@ -397,7 +393,7 @@ describe("FlightPlansLayer", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalled();
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import FlightPlan from "../flightplan";
|
||||
import { LayerGroup } from "react-leaflet";
|
||||
|
||||
interface FlightPlansLayerProps {
|
||||
blue?: boolean;
|
||||
blue: boolean;
|
||||
selectedOnly?: true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ function SelectedFlightPlan(props: FlightPlansLayerProps) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!props.blue) {
|
||||
// We don't currently support playing as red, so nothing to draw.
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlightPlan
|
||||
key={flight.id}
|
||||
|
||||
@@ -25,10 +25,10 @@ export default function LiberationMap() {
|
||||
const map = useRef<Map>(null);
|
||||
const mapCenter = useAppSelector(selectMapCenter);
|
||||
useEffect(() => {
|
||||
map.current?.setView(mapCenter, map.current?.getZoom() ?? 8, { animate: true, duration: 1 });
|
||||
map.current?.setView(mapCenter, 8, { animate: true, duration: 1 });
|
||||
});
|
||||
return (
|
||||
<MapContainer zoom={map.current?.getZoom() ?? 8} zoomControl={false} ref={map}>
|
||||
<MapContainer zoom={8} zoomControl={false} ref={map}>
|
||||
<ScaleControl />
|
||||
<LeafletRuler />
|
||||
<LayersControl collapsed={false}>
|
||||
@@ -98,8 +98,8 @@ export default function LiberationMap() {
|
||||
<LayersControl.Overlay name="Allied IADS Network">
|
||||
<Iadsnetworklayer blue={true} />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="Selected flight plan">
|
||||
<FlightPlansLayer selectedOnly />
|
||||
<LayersControl.Overlay name="Selected blue flight plan">
|
||||
<FlightPlansLayer blue={true} selectedOnly />
|
||||
</LayersControl.Overlay>
|
||||
<LayersControl.Overlay name="All blue flight plans" checked>
|
||||
<FlightPlansLayer blue={true} />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAppSelector } from "../../app/hooks";
|
||||
import { useGetTerrainZonesQuery } from "../../api/liberationApi";
|
||||
import { LatLngLiteral } from "leaflet";
|
||||
import { LayerGroup, LayersControl, Polygon } from "react-leaflet";
|
||||
import { selectMapZones } from "../../api/mapZonesSlice";
|
||||
|
||||
interface TerrainZoneLayerProps {
|
||||
zones: LatLngLiteral[][][];
|
||||
@@ -29,28 +28,33 @@ function TerrainZoneLayer(props: TerrainZoneLayerProps) {
|
||||
}
|
||||
|
||||
export default function TerrainZonesLayers() {
|
||||
const zones = useAppSelector(selectMapZones).mapZones;
|
||||
const { data, error, isLoading } = useGetTerrainZonesQuery();
|
||||
var exclusion = <></>;
|
||||
var inclusion = <></>;
|
||||
var sea = <></>;
|
||||
|
||||
if (zones) {
|
||||
if (error) {
|
||||
console.error("Error while loading terrain zones", error);
|
||||
} else if (isLoading) {
|
||||
} else if (!data) {
|
||||
console.log("Empty response when loading terrain zones");
|
||||
} else {
|
||||
exclusion = (
|
||||
<TerrainZoneLayer
|
||||
zones={zones.exclusion}
|
||||
zones={data.exclusion}
|
||||
color="#969696"
|
||||
fillColor="#303030"
|
||||
/>
|
||||
);
|
||||
inclusion = (
|
||||
<TerrainZoneLayer
|
||||
zones={zones.inclusion}
|
||||
zones={data.inclusion}
|
||||
color="#969696"
|
||||
fillColor="#4b4b4b"
|
||||
/>
|
||||
);
|
||||
sea = (
|
||||
<TerrainZoneLayer zones={zones.sea} color="#344455" fillColor="#344455" />
|
||||
<TerrainZoneLayer zones={data.sea} color="#344455" fillColor="#344455" />
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
import { Tgo as TgoModel } from "../../api/liberationApi";
|
||||
import SplitLines from "../splitlines/SplitLines";
|
||||
import { Icon, Point } from "leaflet";
|
||||
import ms from "milsymbol";
|
||||
import { Symbol as MilSymbol } from "milsymbol";
|
||||
import { Marker, Tooltip } from "react-leaflet";
|
||||
|
||||
function iconForTgo(cp: TgoModel) {
|
||||
const symbol = new ms.Symbol(cp.sidc, {
|
||||
const symbol = new MilSymbol(cp.sidc, {
|
||||
size: 24,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "DCS Retribution"
|
||||
copyright = "2024, DCS Retribution Team"
|
||||
author = "DCS Retribution Team"
|
||||
release = "1.4.0"
|
||||
project = "DCS Liberation"
|
||||
copyright = "2023, DCS Liberation Team"
|
||||
author = "DCS Liberation Team"
|
||||
release = "7.0.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
@@ -9,30 +9,20 @@ from typing import Any, ClassVar, Iterator, Optional, TYPE_CHECKING, Type
|
||||
import yaml
|
||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||
|
||||
from game import persistency
|
||||
from game.data.groups import GroupTask
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.helpers import static_type_from_name
|
||||
from game.dcs.shipunittype import ShipUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.layout import LAYOUTS
|
||||
from game.layout.layout import (
|
||||
TgoLayout,
|
||||
TgoLayoutUnitGroup,
|
||||
)
|
||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
IadsGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.theater.theatergroup import (
|
||||
IadsGroundGroup,
|
||||
IadsRole,
|
||||
TheaterGroup,
|
||||
FIXED_POS_ARG,
|
||||
FIXED_HDG_ARG,
|
||||
)
|
||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
||||
from game.utils import escape_string_for_lua
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -259,11 +249,7 @@ class ForceGroup:
|
||||
# No units to be created so dont create a theater group for them
|
||||
return
|
||||
# Generate Units
|
||||
fixed_pos = FIXED_POS_ARG in unit_group.name
|
||||
fixed_hdg = FIXED_HDG_ARG in unit_group.name
|
||||
units = unit_group.generate_units(
|
||||
ground_object, unit_type, unit_count, fixed_pos, fixed_hdg
|
||||
)
|
||||
units = unit_group.generate_units(ground_object, unit_type, unit_count)
|
||||
# Get or create the TheaterGroup
|
||||
ground_group = ground_object.group_by_name(group_name)
|
||||
if ground_group is not None:
|
||||
@@ -318,22 +304,11 @@ class ForceGroup:
|
||||
# Reverse the heading of the unit
|
||||
unit.position.heading = unit.position.heading.opposite
|
||||
# Rotate unit around the center to align the orientation of the group
|
||||
unit.rotate_position_clockwise(ground_object.position, rotation)
|
||||
unit.position.rotate(ground_object.position, rotation)
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
locations = [
|
||||
Path("resources/groups"),
|
||||
persistency.groups_dir(),
|
||||
]
|
||||
for path in locations:
|
||||
cls._process_path(path)
|
||||
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _process_path(cls, path: Path) -> None:
|
||||
for file in path.glob("*.yaml"):
|
||||
for file in Path("resources/groups").glob("*.yaml"):
|
||||
if not file.is_file():
|
||||
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
|
||||
|
||||
@@ -381,3 +356,5 @@ class ForceGroup:
|
||||
)
|
||||
|
||||
cls._by_name[force_group.name] = force_group
|
||||
|
||||
cls._loaded = True
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.ato.package import Package
|
||||
|
||||
|
||||
@@ -12,16 +11,6 @@ class AirTaskingOrder:
|
||||
#: The set of all planned packages in the ATO.
|
||||
packages: List[Package] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def has_awacs_package(self) -> bool:
|
||||
return any(
|
||||
[
|
||||
p
|
||||
for p in self.packages
|
||||
if any([f for f in p.flights if f.flight_type is FlightType.AEWC])
|
||||
]
|
||||
)
|
||||
|
||||
def add_package(self, package: Package) -> None:
|
||||
"""Adds a package to the ATO."""
|
||||
self.packages.append(package)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Objective adjacency lists."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime, timedelta
|
||||
@@ -10,7 +9,6 @@ from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .flightmembers import FlightMembers
|
||||
from .flightroster import FlightRoster
|
||||
@@ -35,6 +33,7 @@ if TYPE_CHECKING:
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater import ControlPoint
|
||||
from game.transfers import TransferOrder
|
||||
from game.data.weapons import WeaponType
|
||||
from .flightmember import FlightMember
|
||||
@@ -89,7 +88,6 @@ class Flight(
|
||||
|
||||
self.initialize_fuel()
|
||||
self.use_same_loadout_for_all_members = True
|
||||
self.use_same_livery_for_all_members = True
|
||||
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
@@ -125,11 +123,6 @@ class Flight(
|
||||
)
|
||||
)
|
||||
|
||||
# altitude offset for planes
|
||||
offset_factor = self.coalition.game.settings.max_plane_altitude_offset
|
||||
offset_factor = random.randint(0, offset_factor)
|
||||
self.plane_altitude_offset = 1000 * offset_factor * random.choice([-1, 1])
|
||||
|
||||
@property
|
||||
def available_callsigns(self) -> List[str]:
|
||||
callsigns = set()
|
||||
@@ -225,13 +218,6 @@ class Flight(
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
@property
|
||||
def custom_targets(self) -> List[MissionTarget]:
|
||||
return [
|
||||
MissionTarget(wpt.name, wpt.position)
|
||||
for wpt in self.flight_plan.layout.custom_waypoints
|
||||
]
|
||||
|
||||
def position(self) -> Point:
|
||||
return self.state.estimate_position()
|
||||
|
||||
@@ -275,6 +261,8 @@ class Flight(
|
||||
self.fuel = unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
|
||||
self.fuel = unit_type.fuel_max * 0.75
|
||||
|
||||
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||
return any(
|
||||
@@ -285,7 +273,7 @@ class Flight(
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type} - {self.start_type.value}"
|
||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} - {string}"
|
||||
return string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.loadouts import Loadout
|
||||
from game.lasercodes import LaserCode
|
||||
@@ -16,14 +16,14 @@ class FlightMember:
|
||||
self.use_custom_loadout = False
|
||||
self.tgp_laser_code: LaserCode | None = None
|
||||
self.weapon_laser_code: LaserCode | None = None
|
||||
self.properties: dict[str, bool | float | int | str] = {}
|
||||
self.livery: Optional[str] = None
|
||||
self.use_livery_set: bool = True
|
||||
self.properties: dict[str, bool | float | int] = {}
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
new_state = FlightMember(state["pilot"], state["loadout"])
|
||||
new_state.__dict__.update(state)
|
||||
self.__dict__.update(new_state.__dict__)
|
||||
if "tgp_laser_code" not in state:
|
||||
state["tgp_laser_code"] = None
|
||||
if "weapon_laser_code" not in state:
|
||||
state["weapon_laser_code"] = None
|
||||
self.__dict__.update(state)
|
||||
|
||||
def assign_tgp_laser_code(self, code: LaserCode) -> None:
|
||||
if self.tgp_laser_code is not None:
|
||||
|
||||
@@ -95,13 +95,6 @@ class FlightMembers(IFlightRoster):
|
||||
# across all flight members.
|
||||
member.loadout = loadout
|
||||
|
||||
def use_same_livery_for_all_members(self) -> None:
|
||||
if not self.members:
|
||||
return
|
||||
livery = self.members[0].livery
|
||||
for member in self.members[1:]:
|
||||
member.livery = livery
|
||||
|
||||
def use_distinct_loadouts_for_each_member(self) -> None:
|
||||
for member in self.members:
|
||||
member.loadout = member.loadout.clone()
|
||||
|
||||
@@ -6,13 +6,13 @@ from typing import Type
|
||||
from game.ato.flightplans.ibuilder import IBuilder
|
||||
from game.ato.flightplans.patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from game.utils import Distance, Heading, Speed, knots, meters, nautical_miles
|
||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||
|
||||
|
||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.game.settings.desired_awacs_mission_duration
|
||||
return timedelta(hours=4)
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
@@ -70,7 +70,10 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_patrol_altitude
|
||||
if self.flight.unit_type.patrol_altitude is not None:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(25000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
@@ -87,7 +90,6 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:
|
||||
|
||||
@@ -49,7 +49,6 @@ class AirAssaultLayout(FormationAttackLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
@@ -75,6 +74,13 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
)
|
||||
return tot - travel_time
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint is self.tot_waypoint:
|
||||
return self.tot
|
||||
elif waypoint is self.layout.ingress:
|
||||
return self.ingress_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@@ -82,6 +88,10 @@ class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
def ctld_target_zone_radius(self) -> Distance:
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
@@ -101,11 +111,12 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
)
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_cruise_altitude
|
||||
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
if self.flight.is_hercules or self.flight.departure.cptype in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
@@ -122,21 +133,13 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
self._generate_ctld_pickup(),
|
||||
)
|
||||
)
|
||||
pickup.alt = altitude
|
||||
pickup.alt = heli_alt
|
||||
pickup_position = pickup.position
|
||||
|
||||
ingress = (
|
||||
builder.ingress(
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
)
|
||||
if not self.flight.is_hercules
|
||||
else builder.ingress(
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
self.package.waypoints.initial,
|
||||
self.package.target,
|
||||
)
|
||||
ingress = builder.ingress(
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT,
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
)
|
||||
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
@@ -156,13 +159,15 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
drop_pos = tgt.position.point_from_heading(heading, 1200)
|
||||
drop_off_zone = MissionTarget("Dropoff zone", drop_pos)
|
||||
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
|
||||
if dz:
|
||||
dz.alt = heli_alt
|
||||
|
||||
return AirAssaultLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
pickup=pickup,
|
||||
nav_to=builder.nav_path(
|
||||
pickup_position,
|
||||
ingress.position,
|
||||
self.package.waypoints.ingress,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
@@ -179,10 +184,9 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=None,
|
||||
join=builder.join(self.package.waypoints.ingress),
|
||||
join=builder.join(ingress.position),
|
||||
split=builder.split(self.flight.arrival.position),
|
||||
refuel=None,
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Optional
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Distance
|
||||
from game.utils import feet, Distance
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
@@ -92,7 +92,6 @@ class AirliftLayout(StandardLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
@@ -133,11 +132,12 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
"Cannot plan transport mission for flight with no cargo."
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_cruise_altitude
|
||||
heli_alt = feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
altitude = heli_alt if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
pickup_ascent = None
|
||||
pickup_descent = None
|
||||
pickup = None
|
||||
@@ -201,11 +201,9 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
|
||||
return_ascent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
(
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position
|
||||
),
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
@@ -213,11 +211,9 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
return_descent = self._create_ascent_or_descent(
|
||||
builder,
|
||||
self.flight.arrival.position,
|
||||
(
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position
|
||||
),
|
||||
cargo.next_stop.position
|
||||
if cargo.next_stop != self.flight.arrival
|
||||
else cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
@@ -250,7 +246,6 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from .formationattack import (
|
||||
FormationAttackBuilder,
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import nautical_miles
|
||||
|
||||
|
||||
class ArmedReconFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.tot_waypoint.position],
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[ArmedReconFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
return self._build(FlightWaypointType.INGRESS_ARMED_RECON)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> ArmedReconFlightPlan:
|
||||
return ArmedReconFlightPlan(self.flight, self.layout())
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import Distance, Speed
|
||||
from game.utils import Distance, Speed, feet
|
||||
from .capbuilder import CapBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
@@ -18,7 +19,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.game.settings.desired_barcap_mission_duration
|
||||
return self.flight.coalition.doctrine.cap_duration
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
@@ -40,9 +41,14 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||
|
||||
start_pos, end_pos = self.cap_racetrack_for_objective(location, barcap=True)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
patrol_alt = builder.get_patrol_altitude
|
||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
return PatrollingLayout(
|
||||
@@ -58,7 +64,6 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:
|
||||
|
||||
@@ -35,12 +35,7 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
closest_airfield = airfield
|
||||
break
|
||||
else:
|
||||
for airfield in closest_cache.closest_airfields:
|
||||
if airfield.captured != self.is_player:
|
||||
closest_airfield = airfield
|
||||
break
|
||||
else:
|
||||
raise PlanningError("Could not find any enemy airfields")
|
||||
raise PlanningError("Could not find any enemy airfields")
|
||||
|
||||
heading = Heading.from_degrees(
|
||||
location.position.heading_between_point(closest_airfield.position)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater import FrontLine
|
||||
from game.utils import Distance, Speed, kph, dcs_to_shapely_point
|
||||
from game.utils import nautical_miles
|
||||
from game.utils import feet, nautical_miles
|
||||
from .ibuilder import IBuilder
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
@@ -37,7 +37,6 @@ class CasLayout(PatrollingLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
||||
@@ -105,7 +104,11 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
||||
ingress_egress_altitude = builder.get_combat_altitude
|
||||
ingress_egress_altitude = (
|
||||
self.doctrine.ingress_altitude
|
||||
if not is_helo
|
||||
else feet(self.coalition.game.settings.heli_combat_alt_agl)
|
||||
)
|
||||
use_agl_patrol_altitude = is_helo
|
||||
|
||||
ip_solver = IpSolver(
|
||||
@@ -125,12 +128,16 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
ingress_point_shapely.x, ingress_point_shapely.y
|
||||
)
|
||||
|
||||
patrol_start_waypoint = builder.cas(patrol_start, ingress_egress_altitude)
|
||||
patrol_start_waypoint = builder.nav(
|
||||
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_start_waypoint.name = "FLOT START"
|
||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
||||
patrol_start_waypoint.description = "FLOT boundary"
|
||||
|
||||
patrol_end_waypoint = builder.cas(patrol_end, ingress_egress_altitude)
|
||||
patrol_end_waypoint = builder.nav(
|
||||
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
|
||||
)
|
||||
patrol_end_waypoint.name = "FLOT END"
|
||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
||||
patrol_end_waypoint.description = "FLOT boundary"
|
||||
@@ -160,7 +167,6 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
|
||||
|
||||
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class CustomLayout(Layout):
|
||||
custom_waypoints: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.custom_waypoints
|
||||
@@ -60,16 +62,6 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
arrival = (
|
||||
self.layout.custom_waypoints[-1]
|
||||
if self.layout.custom_waypoints
|
||||
else self.layout.departure
|
||||
)
|
||||
return_time = self.total_time_between_waypoints(self.tot_waypoint, arrival)
|
||||
return self.mission_departure_time + return_time
|
||||
|
||||
|
||||
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||
def __init__(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Type
|
||||
|
||||
from .airassault import AirAssaultLayout
|
||||
@@ -12,7 +11,6 @@ from .formationattack import (
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ..packagewaypoints import PackageWaypoints
|
||||
from ...utils import feet
|
||||
|
||||
|
||||
@@ -21,42 +19,23 @@ class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def split_time(self) -> datetime:
|
||||
if self.package.primary_flight and self.package.primary_flight.flight_plan:
|
||||
return self.package.primary_flight.flight_plan.mission_departure_time
|
||||
else:
|
||||
return super().split_time
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
non_formation_escort = False
|
||||
if self.package.waypoints is None:
|
||||
self.package.waypoints = PackageWaypoints.create(
|
||||
self.package, self.coalition, dump_debug_info=False
|
||||
)
|
||||
if self.package.primary_flight:
|
||||
departure = self.package.primary_flight.flight_plan.layout.departure
|
||||
self.package.waypoints.join = departure.position.lerp(
|
||||
self.package.target.position, 0.2
|
||||
)
|
||||
non_formation_escort = True
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
ingress, target = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target
|
||||
)
|
||||
if non_formation_escort:
|
||||
target.position = self.package.waypoints.join
|
||||
ingress.only_for_player = True
|
||||
target.only_for_player = True
|
||||
hold = None
|
||||
if not (self.flight.is_helo or non_formation_escort):
|
||||
if not self.flight.is_helo:
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
join_pos = (
|
||||
WaypointBuilder.perturb(self.package.waypoints.ingress, feet(500))
|
||||
self.package.waypoints.ingress
|
||||
if self.flight.is_helo
|
||||
else self.package.waypoints.join
|
||||
)
|
||||
@@ -64,9 +43,12 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
|
||||
split = builder.split(self._get_split())
|
||||
|
||||
ingress_alt = self.doctrine.ingress_altitude
|
||||
is_helo = builder.flight.is_helo
|
||||
heli_alt = feet(self.coalition.game.settings.heli_combat_alt_agl)
|
||||
initial = builder.escort_hold(
|
||||
target.position if is_helo else self.package.waypoints.initial,
|
||||
min(heli_alt, ingress_alt) if is_helo else ingress_alt,
|
||||
)
|
||||
|
||||
pf = self.package.primary_flight
|
||||
@@ -81,10 +63,15 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
join = builder.join(ascent.position)
|
||||
if layout.pickup and layout.drop_off_ascent:
|
||||
join = builder.join(layout.drop_off_ascent.position)
|
||||
elif layout.pickup:
|
||||
join = builder.join(layout.pickup.position)
|
||||
split = builder.split(layout.arrival.position)
|
||||
if layout.drop_off:
|
||||
initial = builder.escort_hold(
|
||||
layout.drop_off.position,
|
||||
min(feet(200), ingress_alt)
|
||||
if builder.flight.is_helo
|
||||
else ingress_alt,
|
||||
)
|
||||
|
||||
refuel = self._build_refuel(builder)
|
||||
@@ -93,13 +80,13 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
nav_to = builder.nav_path(
|
||||
hold.position if hold else departure.position,
|
||||
join.position,
|
||||
builder.get_cruise_altitude,
|
||||
self.doctrine.ingress_altitude,
|
||||
)
|
||||
|
||||
nav_from = builder.nav_path(
|
||||
refuel.position if refuel else split.position,
|
||||
self.flight.arrival.position,
|
||||
builder.get_cruise_altitude,
|
||||
self.doctrine.ingress_altitude,
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
@@ -116,7 +103,6 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:
|
||||
|
||||
@@ -24,7 +24,6 @@ class FerryLayout(StandardLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
@@ -61,14 +60,14 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
f"{self.flight.departure}"
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
altitude = (
|
||||
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if altitude_is_agl
|
||||
else builder.get_patrol_altitude
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
return FerryLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
@@ -81,7 +80,6 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:
|
||||
|
||||
@@ -5,7 +5,6 @@ MissionPlanner. Those only plan basic information like the objective, aircraft
|
||||
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
||||
generating the waypoints for the mission.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
@@ -35,7 +34,6 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class Layout(ABC):
|
||||
departure: FlightWaypoint
|
||||
custom_waypoints: list[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> list[FlightWaypoint]:
|
||||
@@ -106,6 +104,19 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
#
|
||||
# Plus, it's a loiter point so there's no reason to hurry.
|
||||
factor = 0.75
|
||||
elif (
|
||||
self.flight.is_helo
|
||||
and (
|
||||
a.waypoint_type == FlightWaypointType.JOIN
|
||||
or "INGRESS" in a.waypoint_type.name
|
||||
or a.waypoint_type == FlightWaypointType.CUSTOM
|
||||
)
|
||||
and self.package.primary_flight
|
||||
and not self.package.primary_flight.flight_plan.is_airassault
|
||||
):
|
||||
# Helicopter flights should be slowed down between JOIN & INGRESS
|
||||
# to allow the escort to keep up while engaging targets along the way.
|
||||
factor = 0.50
|
||||
# TODO: Adjust if AGL.
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
@@ -256,9 +267,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
if self.flight.client_count:
|
||||
return timedelta(
|
||||
minutes=self.flight.coalition.game.settings.player_startup_time
|
||||
)
|
||||
return timedelta(minutes=10)
|
||||
else:
|
||||
# The AI doesn't seem to have a real startup procedure.
|
||||
return timedelta(minutes=2)
|
||||
@@ -296,10 +305,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
raise NotImplementedError
|
||||
|
||||
@self_type_guard
|
||||
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
||||
return False
|
||||
|
||||
@@ -3,12 +3,10 @@ from __future__ import annotations
|
||||
from typing import Any, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.theater.controlpoint import NavalControlPoint
|
||||
from .aewc import AewcFlightPlan
|
||||
from .airassault import AirAssaultFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
from .antiship import AntiShipFlightPlan
|
||||
from .armedrecon import ArmedReconFlightPlan
|
||||
from .bai import BaiFlightPlan
|
||||
from .barcap import BarCapFlightPlan
|
||||
from .cas import CasFlightPlan
|
||||
@@ -20,10 +18,8 @@ from .ocaaircraft import OcaAircraftFlightPlan
|
||||
from .ocarunway import OcaRunwayFlightPlan
|
||||
from .packagerefueling import PackageRefuelingFlightPlan
|
||||
from .planningerror import PlanningError
|
||||
from .pretensecargo import PretenseCargoFlightPlan
|
||||
from .sead import SeadFlightPlan
|
||||
from .seadsweep import SeadSweepFlightPlan
|
||||
from .shiprecoverytanker import RecoveryTankerFlightPlan
|
||||
from .strike import StrikeFlightPlan
|
||||
from .sweep import SweepFlightPlan
|
||||
from .tarcap import TarCapFlightPlan
|
||||
@@ -64,9 +60,6 @@ class FlightPlanBuilderTypes:
|
||||
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
|
||||
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
|
||||
FlightType.RECOVERY: RecoveryTankerFlightPlan.builder_type(),
|
||||
}
|
||||
try:
|
||||
return builder_dict[flight.flight_type]
|
||||
|
||||
@@ -33,7 +33,8 @@ class FormationLayout(LoiterLayout, ABC):
|
||||
class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]: ...
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
...
|
||||
|
||||
@property
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
@@ -63,10 +64,8 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
|
||||
if (
|
||||
speed := self.package.formation_speed(self.flight.is_helo)
|
||||
) and b in self.package_speed_waypoints:
|
||||
return speed
|
||||
if self.package.formation_speed and b in self.package_speed_waypoints:
|
||||
return self.package.formation_speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@@ -76,11 +75,13 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def join_time(self) -> datetime: ...
|
||||
def join_time(self) -> datetime:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def split_time(self) -> datetime: ...
|
||||
def split_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.join:
|
||||
|
||||
@@ -11,7 +11,7 @@ from dcs import Point
|
||||
|
||||
from game.flightplan import HoldZoneGeometry
|
||||
from game.theater import MissionTarget
|
||||
from game.utils import nautical_miles, Speed, feet
|
||||
from game.utils import Speed, meters, nautical_miles, feet
|
||||
from .flightplan import FlightPlan
|
||||
from .formation import FormationFlightPlan, FormationLayout
|
||||
from .ibuilder import IBuilder
|
||||
@@ -39,9 +39,8 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
speed = self.package.formation_speed(self.flight.is_helo)
|
||||
assert speed is not None
|
||||
return speed
|
||||
assert self.package.formation_speed is not None
|
||||
return self.package.formation_speed
|
||||
return super().speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@@ -54,7 +53,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
"TARGET AREA",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position,
|
||||
feet(0),
|
||||
meters(0),
|
||||
"RADIO",
|
||||
)
|
||||
|
||||
@@ -158,7 +157,6 @@ class FormationAttackLayout(FormationLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
||||
@@ -193,7 +191,6 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
join_pos = self.package.waypoints.join
|
||||
if self.flight.is_helo:
|
||||
join_pos = self.package.waypoints.ingress
|
||||
join_pos = WaypointBuilder.perturb(join_pos, feet(500))
|
||||
join = builder.join(join_pos)
|
||||
split = builder.split(self._get_split())
|
||||
refuel = self._build_refuel(builder)
|
||||
@@ -212,10 +209,14 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
if self.flight.flight_type == FlightType.STRIKE:
|
||||
hdg = self.package.target.position.heading_between_point(ingress.position)
|
||||
pos = ingress.position.point_from_heading(hdg, nautical_miles(10).meters)
|
||||
lineup = builder.nav(pos, builder.get_combat_altitude)
|
||||
lineup = builder.nav(pos, self.flight.coalition.doctrine.ingress_altitude)
|
||||
|
||||
is_helo = self.flight.is_helo
|
||||
ingress_egress_altitude = builder.get_combat_altitude
|
||||
ingress_egress_altitude = (
|
||||
self.doctrine.ingress_altitude
|
||||
if not is_helo
|
||||
else feet(self.coalition.game.settings.heli_combat_alt_agl)
|
||||
)
|
||||
use_agl_ingress_egress = is_helo
|
||||
|
||||
return FormationAttackLayout(
|
||||
@@ -243,7 +244,6 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def _build_refuel(self, builder: WaypointBuilder) -> Optional[FlightWaypoint]:
|
||||
@@ -288,8 +288,6 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||
return builder.sead_area(location)
|
||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||
return builder.oca_strike_area(location)
|
||||
elif flight.flight_type == FlightType.ARMED_RECON:
|
||||
return builder.armed_recon_area(location)
|
||||
else:
|
||||
return builder.strike_area(location)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
def __init__(self, flight: Flight) -> None:
|
||||
self.flight = flight
|
||||
self._flight_plan: FlightPlanT | None = None
|
||||
self.settings = self.flight.coalition.game.settings
|
||||
|
||||
def get_or_build(self) -> FlightPlanT:
|
||||
if self._flight_plan is None:
|
||||
@@ -60,7 +59,8 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||
return self.flight.departure.theater
|
||||
|
||||
@abstractmethod
|
||||
def build(self, dump_debug_info: bool = False) -> FlightPlanT: ...
|
||||
def build(self, dump_debug_info: bool = False) -> FlightPlanT:
|
||||
...
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
|
||||
@@ -26,7 +26,8 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def push_time(self) -> datetime: ...
|
||||
def push_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.hold:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Type
|
||||
|
||||
from dcs import Point
|
||||
|
||||
from game.utils import Distance, Heading, meters
|
||||
from game.utils import Distance, Heading, feet, meters
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
@@ -98,7 +98,11 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_patrol_altitude
|
||||
tanker_type = self.flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
@@ -115,7 +119,6 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:
|
||||
|
||||
@@ -31,7 +31,6 @@ class PatrollingLayout(StandardLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
|
||||
@@ -80,9 +79,6 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
return self.patrol_end_time
|
||||
return None
|
||||
|
||||
def takeoff_time(self) -> datetime:
|
||||
return self.patrol_start_time - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
|
||||
@property
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.layout.patrol_start, self.layout.patrol_end}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
from .ferry import FerryLayout
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .standard import StandardFlightPlan, StandardLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ...theater import OffMapSpawn
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
PRETENSE_CARGO_FLIGHT_DISTANCE = 100000
|
||||
PRETENSE_CARGO_FLIGHT_HEADING_RANGE = 20
|
||||
|
||||
|
||||
class PretenseCargoFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
# TOT planning isn't really useful for ferries. They're behind the front
|
||||
# lines so no need to wait for escorts or for other missions to complete.
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class Builder(IBuilder[PretenseCargoFlightPlan, FerryLayout]):
|
||||
def layout(self) -> FerryLayout:
|
||||
# Find the spawn location for off-map transport planes
|
||||
distance_to_flot = 0.0
|
||||
heading_from_flot = 0.0
|
||||
offmap_transport_cp_id = self.flight.departure.id
|
||||
for front_line_cp in self.coalition.game.theater.controlpoints:
|
||||
if isinstance(front_line_cp, OffMapSpawn):
|
||||
continue
|
||||
for front_line in self.coalition.game.theater.conflicts():
|
||||
if front_line_cp.captured == self.flight.coalition.player:
|
||||
if (
|
||||
front_line_cp.position.distance_to_point(front_line.position)
|
||||
> distance_to_flot
|
||||
):
|
||||
distance_to_flot = front_line_cp.position.distance_to_point(
|
||||
front_line.position
|
||||
)
|
||||
heading_from_flot = front_line.position.heading_between_point(
|
||||
front_line_cp.position
|
||||
)
|
||||
offmap_transport_cp_id = front_line_cp.id
|
||||
offmap_transport_cp = self.coalition.game.theater.find_control_point_by_id(
|
||||
offmap_transport_cp_id
|
||||
)
|
||||
offmap_heading = random.randrange(
|
||||
int(heading_from_flot - PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
|
||||
int(heading_from_flot + PRETENSE_CARGO_FLIGHT_HEADING_RANGE),
|
||||
)
|
||||
offmap_transport_spawn = offmap_transport_cp.position.point_from_heading(
|
||||
offmap_heading, PRETENSE_CARGO_FLIGHT_DISTANCE
|
||||
)
|
||||
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
altitude = (
|
||||
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if altitude_is_agl
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
ferry_layout = FerryLayout(
|
||||
departure=builder.join(offmap_transport_spawn),
|
||||
nav_to=builder.nav_path(
|
||||
offmap_transport_spawn,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
ferry_layout.departure = builder.join(offmap_transport_spawn)
|
||||
ferry_layout.nav_to.append(builder.join(offmap_transport_spawn))
|
||||
ferry_layout.nav_from.append(builder.join(offmap_transport_spawn))
|
||||
return ferry_layout
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> PretenseCargoFlightPlan:
|
||||
return PretenseCargoFlightPlan(self.flight, self.layout())
|
||||
@@ -1,15 +1,10 @@
|
||||
from abc import ABC
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import Speed, knots, Distance, meters
|
||||
from game.utils import Distance, Speed, knots, meters
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
|
||||
|
||||
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
|
||||
@property
|
||||
def patrol_speed(self) -> Speed:
|
||||
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
||||
|
||||
@@ -27,7 +27,6 @@ class RtbLayout(StandardLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
@@ -66,13 +65,13 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
current_position = self.flight.state.estimate_position()
|
||||
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude_is_agl = self.flight.is_helo
|
||||
altitude = (
|
||||
feet(self.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if altitude_is_agl
|
||||
else builder.get_patrol_altitude
|
||||
else self.flight.unit_type.preferred_patrol_altitude
|
||||
)
|
||||
builder = WaypointBuilder(self.flight)
|
||||
abort_point = builder.nav(
|
||||
current_position, current_altitude, altitude_reference == "RADIO"
|
||||
)
|
||||
@@ -92,7 +91,6 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:
|
||||
|
||||
@@ -8,12 +8,10 @@ from .formationattack import (
|
||||
FormationAttackFlightPlan,
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .uizonedisplay import UiZoneDisplay, UiZone
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import nautical_miles
|
||||
|
||||
|
||||
class SeadSweepFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
class SeadSweepFlightPlan(FormationAttackFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -21,14 +19,6 @@ class SeadSweepFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=2)
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
return UiZone(
|
||||
[self.tot_waypoint.position],
|
||||
nautical_miles(
|
||||
self.flight.coalition.game.settings.sead_sweep_engagement_range_distance
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadSweepFlightPlan, FormationAttackLayout]):
|
||||
def layout(self) -> FormationAttackLayout:
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Type
|
||||
|
||||
from game.ato.flightplans.ibuilder import IBuilder
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
from .. import FlightWaypoint
|
||||
from ...utils import knots
|
||||
|
||||
|
||||
class RecoveryTankerFlightPlan(RefuelingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.departure
|
||||
|
||||
|
||||
class Builder(IBuilder[RecoveryTankerFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
station_time = self.coalition.game.settings.desired_tanker_on_station_time
|
||||
time_to_landing = station_time.total_seconds()
|
||||
hdg = (self.coalition.game.conditions.weather.wind.at_0m.direction + 180) % 360
|
||||
recovery_ship = self.package.target.position.point_from_heading(
|
||||
hdg, time_to_landing * knots(20).meters_per_second
|
||||
)
|
||||
recovery_tanker = builder.recovery_tanker(recovery_ship)
|
||||
patrol_end = builder.race_track_end(recovery_tanker.position, altitude)
|
||||
patrol_end.only_for_player = True # avoid generating the waypoints
|
||||
|
||||
return PatrollingLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, recovery_ship, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
recovery_ship, self.flight.arrival.position, altitude
|
||||
),
|
||||
patrol_start=recovery_tanker,
|
||||
patrol_end=patrol_end,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan:
|
||||
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
|
||||
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
||||
@@ -71,9 +70,6 @@ class StandardLayout(Layout, ABC):
|
||||
elif waypoint in self.nav_from:
|
||||
self.nav_from.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.custom_waypoints:
|
||||
self.custom_waypoints.remove(waypoint)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -87,10 +83,3 @@ class StandardFlightPlan(FlightPlan[LayoutT], ABC):
|
||||
others are guaranteed to have certain properties like departure and arrival points,
|
||||
potentially a divert field, and a bullseye
|
||||
"""
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
return_time = self.total_time_between_waypoints(
|
||||
self.tot_waypoint, self.layout.arrival
|
||||
)
|
||||
return self.mission_departure_time + return_time
|
||||
|
||||
@@ -11,7 +11,6 @@ from .formationattack import (
|
||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||
from .waypointbuilder import StrikeTarget
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...theater.theatergroup import SceneryUnit
|
||||
|
||||
|
||||
class StrikeFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -29,10 +28,7 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
|
||||
|
||||
targets: list[StrikeTarget] = []
|
||||
for idx, unit in enumerate(location.strike_targets):
|
||||
name = unit.type.id
|
||||
if isinstance(unit, SceneryUnit):
|
||||
name = unit.name
|
||||
targets.append(StrikeTarget(f"{name} #{idx}", unit))
|
||||
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
|
||||
|
||||
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ class SweepLayout(LoiterLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
@@ -105,27 +104,26 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
start, end = builder.sweep(start_pos, target, altitude)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point())
|
||||
|
||||
return SweepLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
hold=hold,
|
||||
nav_to=builder.nav_path(hold.position, start.position, altitude),
|
||||
nav_to=builder.nav_path(
|
||||
hold.position, start.position, self.doctrine.ingress_altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
end.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
self.doctrine.ingress_altitude,
|
||||
),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def _hold_point(self) -> Point:
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import Distance, Speed
|
||||
from game.utils import Distance, Speed, feet
|
||||
from .capbuilder import CapBuilder
|
||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
@@ -30,7 +31,6 @@ class TarCapLayout(PatrollingLayout):
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
yield from self.custom_waypoints
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint == self.refuel:
|
||||
@@ -95,9 +95,14 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||
def layout(self) -> TarCapLayout:
|
||||
location = self.package.target
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
patrol_alt = builder.get_patrol_altitude
|
||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
@@ -123,7 +128,6 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.utils import Heading, meters, nautical_miles
|
||||
from game.utils import Heading, feet, meters, nautical_miles
|
||||
from .ibuilder import IBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
@@ -14,6 +15,10 @@ class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
|
||||
|
||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
@@ -53,7 +58,11 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
|
||||
altitude = builder.get_patrol_altitude
|
||||
tanker_type = self.flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
altitude = feet(21000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
@@ -70,7 +79,6 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan:
|
||||
|
||||
@@ -14,4 +14,5 @@ class UiZone:
|
||||
|
||||
class UiZoneDisplay(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def ui_zone(self) -> UiZone: ...
|
||||
def ui_zone(self) -> UiZone:
|
||||
...
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
@@ -18,7 +17,6 @@ from dcs.mapping import Point, Vector2
|
||||
|
||||
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.data.weapons import WeaponType
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
@@ -26,13 +24,11 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
from game.utils import Distance, meters, nautical_miles, feet
|
||||
|
||||
AGL_TRANSITION_ALT = 5000
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.transfers import MultiGroupTransport
|
||||
from game.theater.theatergroup import TheaterGroup
|
||||
from game.ato.flight import Flight
|
||||
|
||||
|
||||
@@ -55,34 +51,11 @@ class WaypointBuilder:
|
||||
self.navmesh = coalition.nav_mesh
|
||||
self.targets = targets
|
||||
self._bullseye = coalition.bullseye
|
||||
self.settings = self.flight.coalition.game.settings
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return self.flight.is_helo
|
||||
|
||||
@property
|
||||
def get_patrol_altitude(self) -> Distance:
|
||||
return self.get_altitude(self.flight.unit_type.preferred_patrol_altitude)
|
||||
|
||||
@property
|
||||
def get_cruise_altitude(self) -> Distance:
|
||||
return self.get_altitude(self.flight.unit_type.preferred_cruise_altitude)
|
||||
|
||||
@property
|
||||
def get_combat_altitude(self) -> Distance:
|
||||
return self.get_altitude(self.flight.unit_type.preferred_combat_altitude)
|
||||
|
||||
def get_altitude(self, alt: Distance) -> Distance:
|
||||
randomized_alt = feet(round(alt.feet + self.flight.plane_altitude_offset))
|
||||
altitude = max(
|
||||
self.doctrine.min_combat_altitude,
|
||||
min(self.doctrine.max_combat_altitude, randomized_alt),
|
||||
)
|
||||
return (
|
||||
feet(self.settings.heli_combat_alt_agl) if self.flight.is_helo else altitude
|
||||
)
|
||||
|
||||
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
|
||||
@@ -99,7 +72,9 @@ class WaypointBuilder:
|
||||
"NAV",
|
||||
FlightWaypointType.NAV,
|
||||
position,
|
||||
self.get_cruise_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.rendezvous_altitude,
|
||||
description="Enter theater",
|
||||
pretty_name="Enter theater",
|
||||
)
|
||||
@@ -126,7 +101,9 @@ class WaypointBuilder:
|
||||
"NAV",
|
||||
FlightWaypointType.NAV,
|
||||
position,
|
||||
self.get_cruise_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.rendezvous_altitude,
|
||||
description="Exit theater",
|
||||
pretty_name="Exit theater",
|
||||
)
|
||||
@@ -152,10 +129,14 @@ class WaypointBuilder:
|
||||
return None
|
||||
|
||||
position = divert.position
|
||||
altitude_type: AltitudeReference = "BARO"
|
||||
altitude_type: AltitudeReference
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
altitude = self.get_cruise_altitude
|
||||
altitude_type = "RADIO" if self.is_helo else altitude_type
|
||||
altitude = (
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = meters(0)
|
||||
altitude_type = "RADIO"
|
||||
@@ -185,15 +166,16 @@ class WaypointBuilder:
|
||||
|
||||
def hold(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"HOLD",
|
||||
FlightWaypointType.LOITER,
|
||||
position,
|
||||
# TODO: dedicated altitude setting for holding
|
||||
self.get_cruise_altitude if self.is_helo else self.get_combat_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Wait until push time",
|
||||
pretty_name="Hold",
|
||||
@@ -201,14 +183,16 @@ class WaypointBuilder:
|
||||
|
||||
def join(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"JOIN",
|
||||
FlightWaypointType.JOIN,
|
||||
position,
|
||||
self.get_cruise_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Rendezvous with package",
|
||||
pretty_name="Join",
|
||||
@@ -216,14 +200,16 @@ class WaypointBuilder:
|
||||
|
||||
def refuel(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_cruise_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"REFUEL",
|
||||
FlightWaypointType.REFUEL,
|
||||
position,
|
||||
self.get_cruise_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Refuel from tanker",
|
||||
pretty_name="Refuel",
|
||||
@@ -231,14 +217,16 @@ class WaypointBuilder:
|
||||
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"SPLIT",
|
||||
FlightWaypointType.SPLIT,
|
||||
position,
|
||||
self.get_combat_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Depart from package",
|
||||
pretty_name="Split",
|
||||
@@ -250,14 +238,7 @@ class WaypointBuilder:
|
||||
position: Point,
|
||||
objective: MissionTarget,
|
||||
) -> FlightWaypoint:
|
||||
alt = self.get_combat_altitude
|
||||
if ingress_type in [
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
|
||||
FlightWaypointType.INGRESS_ARMED_RECON,
|
||||
]:
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
|
||||
alt = self.doctrine.ingress_altitude
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.flight.is_hercules:
|
||||
alt_type = "RADIO"
|
||||
@@ -266,19 +247,15 @@ class WaypointBuilder:
|
||||
if self.is_helo
|
||||
else feet(1000)
|
||||
)
|
||||
elif alt.feet <= AGL_TRANSITION_ALT:
|
||||
alt_type = "RADIO"
|
||||
|
||||
heading = objective.position.heading_between_point(position)
|
||||
|
||||
return FlightWaypoint(
|
||||
"INGRESS",
|
||||
ingress_type,
|
||||
(
|
||||
objective.position.point_from_heading(heading, nautical_miles(5).meters)
|
||||
if self.is_helo
|
||||
else position
|
||||
),
|
||||
objective.position.point_from_heading(heading, nautical_miles(5).meters)
|
||||
if self.is_helo
|
||||
else position,
|
||||
alt,
|
||||
alt_type,
|
||||
description=f"INGRESS on {objective.name}",
|
||||
@@ -286,29 +263,18 @@ class WaypointBuilder:
|
||||
targets=objective.strike_targets,
|
||||
)
|
||||
|
||||
def _adjust_altitude_for_clouds(self, alt: Distance) -> Distance:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
max_alt = feet(math.inf)
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
max_alt = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
return min(alt, max_alt)
|
||||
|
||||
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"EGRESS",
|
||||
FlightWaypointType.EGRESS,
|
||||
position,
|
||||
self.get_combat_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description=f"EGRESS from {target.name}",
|
||||
pretty_name=f"EGRESS from {target.name}",
|
||||
@@ -331,11 +297,7 @@ class WaypointBuilder:
|
||||
return FlightWaypoint(
|
||||
target.name,
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
(
|
||||
target.target.ground_object.position
|
||||
if isinstance(target.target, TheaterGroup)
|
||||
else target.target.position
|
||||
),
|
||||
target.target.position,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=description,
|
||||
@@ -350,35 +312,16 @@ class WaypointBuilder:
|
||||
return self._target_area(f"STRIKE {target.name}", target)
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return self._target_area(
|
||||
f"SEAD on {target.name}",
|
||||
target,
|
||||
altitude=self.get_combat_altitude,
|
||||
alt_type=alt_type,
|
||||
altitude=self.doctrine.ingress_altitude,
|
||||
alt_type="BARO",
|
||||
)
|
||||
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"DEAD on {target.name}", target)
|
||||
|
||||
def armed_recon_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
# Force AI aircraft to fly towards target area
|
||||
alt = self.get_combat_altitude
|
||||
alt = self._adjust_altitude_for_clouds(alt)
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or alt.feet <= AGL_TRANSITION_ALT:
|
||||
alt_type = "RADIO"
|
||||
return self._target_area(
|
||||
f"ARMED RECON {target.name}",
|
||||
target,
|
||||
altitude=alt,
|
||||
alt_type=alt_type,
|
||||
flyover=True,
|
||||
)
|
||||
|
||||
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
|
||||
|
||||
@@ -423,25 +366,14 @@ class WaypointBuilder:
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def cas(self, position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
weather = self.flight.coalition.game.conditions.weather
|
||||
if weather.clouds and (
|
||||
weather.clouds.preset
|
||||
and "overcast" in weather.clouds.preset.description.lower()
|
||||
or weather.clouds.density > 5
|
||||
):
|
||||
altitude = meters(
|
||||
max(feet(500).meters, weather.clouds.base - feet(500).meters)
|
||||
)
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
"CAS",
|
||||
FlightWaypointType.CAS,
|
||||
position,
|
||||
(
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else max(meters(1000), altitude)
|
||||
),
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else meters(1000),
|
||||
"RADIO",
|
||||
description="Provide CAS",
|
||||
pretty_name="CAS",
|
||||
@@ -455,13 +387,11 @@ class WaypointBuilder:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
baro: AltitudeReference = "BARO"
|
||||
return FlightWaypoint(
|
||||
"RACETRACK START",
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
position,
|
||||
altitude,
|
||||
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
description="Orbit between this point and the next point",
|
||||
pretty_name="Race-track start",
|
||||
)
|
||||
@@ -474,13 +404,11 @@ class WaypointBuilder:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
baro: AltitudeReference = "BARO"
|
||||
return FlightWaypoint(
|
||||
"RACETRACK END",
|
||||
FlightWaypointType.PATROL,
|
||||
position,
|
||||
altitude,
|
||||
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
description="Orbit between this point and the previous point",
|
||||
pretty_name="Race-track end",
|
||||
)
|
||||
@@ -508,39 +436,42 @@ class WaypointBuilder:
|
||||
start: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
baro: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"ORBIT",
|
||||
FlightWaypointType.LOITER,
|
||||
start,
|
||||
altitude,
|
||||
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
description="Anchor and hold at this point",
|
||||
pretty_name="Orbit",
|
||||
)
|
||||
|
||||
def sead_search(self, target: MissionTarget) -> FlightWaypoint:
|
||||
hold = self._sead_search_point(target)
|
||||
baro: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"SEAD Search",
|
||||
FlightWaypointType.NAV,
|
||||
hold,
|
||||
self.get_combat_altitude,
|
||||
"RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type="BARO",
|
||||
description="Anchor and search from this point",
|
||||
pretty_name="SEAD Search",
|
||||
)
|
||||
|
||||
def sead_sweep(self, target: MissionTarget) -> FlightWaypoint:
|
||||
hold = self._sead_search_point(target)
|
||||
baro: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"SEAD Sweep",
|
||||
FlightWaypointType.NAV,
|
||||
hold,
|
||||
self.get_combat_altitude,
|
||||
"RADIO" if self.get_combat_altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type="BARO",
|
||||
description="Anchor and search from this point",
|
||||
pretty_name="SEAD Sweep",
|
||||
)
|
||||
@@ -559,13 +490,8 @@ class WaypointBuilder:
|
||||
self.flight.coalition.game.settings.sead_threat_buffer_min_distance
|
||||
).meters
|
||||
if target.strike_targets:
|
||||
factor = (
|
||||
0.8
|
||||
if self.flight.any_member_has_weapon_of_type(WeaponType.ARM)
|
||||
else 1.1
|
||||
)
|
||||
threat_range = (
|
||||
factor * max([x.threat_range for x in target.strike_targets]).meters
|
||||
1.1 * max([x.threat_range for x in target.strike_targets]).meters
|
||||
)
|
||||
hdg = target.position.heading_between_point(ingress)
|
||||
hold = target.position.point_from_heading(
|
||||
@@ -573,16 +499,15 @@ class WaypointBuilder:
|
||||
)
|
||||
return hold
|
||||
|
||||
def escort_hold(self, start: Point) -> FlightWaypoint:
|
||||
def escort_hold(self, 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.
|
||||
"""
|
||||
altitude = self.get_combat_altitude
|
||||
|
||||
alt_type: Literal["BARO", "RADIO"] = "BARO"
|
||||
if self.is_helo or altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
@@ -603,13 +528,11 @@ class WaypointBuilder:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
baro: AltitudeReference = "BARO"
|
||||
return FlightWaypoint(
|
||||
"SWEEP START",
|
||||
FlightWaypointType.INGRESS_SWEEP,
|
||||
position,
|
||||
altitude,
|
||||
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
description="Proceed to the target and engage enemy aircraft",
|
||||
pretty_name="Sweep start",
|
||||
)
|
||||
@@ -622,13 +545,11 @@ class WaypointBuilder:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the sweep in meters.
|
||||
"""
|
||||
baro: AltitudeReference = "BARO"
|
||||
return FlightWaypoint(
|
||||
"SWEEP END",
|
||||
FlightWaypointType.EGRESS,
|
||||
position,
|
||||
altitude,
|
||||
"RADIO" if altitude.feet <= AGL_TRANSITION_ALT else baro,
|
||||
description="End of sweep",
|
||||
pretty_name="Sweep end",
|
||||
)
|
||||
@@ -657,7 +578,7 @@ class WaypointBuilder:
|
||||
target: The mission target.
|
||||
"""
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if self.is_helo:
|
||||
alt_type = "RADIO"
|
||||
|
||||
# This would preferably be no points at all, and instead the Escort task
|
||||
@@ -671,7 +592,9 @@ class WaypointBuilder:
|
||||
"TARGET",
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position,
|
||||
self.get_combat_altitude,
|
||||
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
|
||||
if self.is_helo
|
||||
else self.doctrine.ingress_altitude,
|
||||
alt_type,
|
||||
description="Escort the package",
|
||||
pretty_name="Target area",
|
||||
@@ -693,18 +616,17 @@ class WaypointBuilder:
|
||||
pretty_name="Pick-up zone",
|
||||
)
|
||||
|
||||
def dropoff_zone(self, drop_off: MissionTarget) -> FlightWaypoint:
|
||||
@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)
|
||||
"""
|
||||
alt = self.get_combat_altitude if self.flight.is_helo else meters(0)
|
||||
|
||||
return FlightWaypoint(
|
||||
"DROPOFFZONE",
|
||||
FlightWaypointType.DROPOFF_ZONE,
|
||||
drop_off.position,
|
||||
alt,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Drop off cargo at {drop_off.name}",
|
||||
pretty_name="Drop-off zone",
|
||||
@@ -738,7 +660,7 @@ class WaypointBuilder:
|
||||
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
|
||||
"""
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if altitude_is_agl or altitude.feet <= AGL_TRANSITION_ALT:
|
||||
if altitude_is_agl:
|
||||
alt_type = "RADIO"
|
||||
|
||||
return FlightWaypoint(
|
||||
@@ -802,22 +724,8 @@ class WaypointBuilder:
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point, deviation: Distance = nautical_miles(1)) -> Point:
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return point + Vector2(x_adj, y_adj)
|
||||
|
||||
@staticmethod
|
||||
def recovery_tanker(position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"RECOVERY",
|
||||
FlightWaypointType.RECOVERY_TANKER,
|
||||
position,
|
||||
feet(6000),
|
||||
alt_type,
|
||||
description="Recovery tanker for aircraft carriers",
|
||||
pretty_name="Recovery",
|
||||
only_for_player=True, # for visual purposes in Retribution only
|
||||
)
|
||||
|
||||
@@ -22,10 +22,6 @@ class FlightRoster(IFlightRoster):
|
||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
||||
yield from self.pilots
|
||||
|
||||
@property
|
||||
def player_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
|
||||
def pilot_at(self, idx: int) -> Pilot | None:
|
||||
return self.pilots[idx]
|
||||
|
||||
|
||||
@@ -60,12 +60,14 @@ class FlightState(ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def cancelable(self) -> bool: ...
|
||||
def cancelable(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None: ...
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@property
|
||||
def in_flight(self) -> bool:
|
||||
@@ -96,14 +98,17 @@ class FlightState(ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_waiting_for_start(self) -> bool: ...
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def estimate_position(self) -> Point: ...
|
||||
def estimate_position(self) -> Point:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def spawn_type(self) -> StartType: ...
|
||||
def spawn_type(self) -> StartType:
|
||||
...
|
||||
|
||||
def a2a_commit_region(self) -> Optional[ThreatPoly]:
|
||||
return None
|
||||
|
||||
@@ -57,13 +57,16 @@ class InFlight(FlightState, ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def estimate_position(self) -> Point: ...
|
||||
def estimate_position(self) -> Point:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def estimate_altitude(self) -> tuple[Distance, str]: ...
|
||||
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def estimate_speed(self) -> Speed: ...
|
||||
def estimate_speed(self) -> Speed:
|
||||
...
|
||||
|
||||
def estimate_fuel_at_current_waypoint(self) -> float:
|
||||
initial_fuel = super().estimate_fuel()
|
||||
|
||||
@@ -51,7 +51,7 @@ class Takeoff(AtDeparture):
|
||||
if (
|
||||
self.flight.client_count > 0
|
||||
and self.settings.player_mission_interrupts_sim_at
|
||||
in [StartType.COLD, StartType.WARM, StartType.RUNWAY]
|
||||
is [StartType.COLD, StartType.WARM, StartType.RUNWAY]
|
||||
):
|
||||
logging.info(
|
||||
f"Interrupting simulation because {self.flight} has players and has "
|
||||
|
||||
@@ -25,7 +25,7 @@ class Uninitialized(FlightState):
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
return True
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
def estimate_position(self) -> Point:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
|
||||
@@ -58,9 +58,6 @@ class FlightType(Enum):
|
||||
FERRY = "Ferry"
|
||||
AIR_ASSAULT = "Air Assault"
|
||||
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
||||
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
|
||||
ARMED_RECON = "Armed Recon"
|
||||
RECOVERY = "Recovery"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -96,7 +93,6 @@ class FlightType(Enum):
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.AIR_ASSAULT,
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.ARMED_RECON,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -108,7 +104,6 @@ class FlightType(Enum):
|
||||
return {
|
||||
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
|
||||
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
|
||||
FlightType.ARMED_RECON: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BAI: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.BARCAP: AirEntity.FIGHTER,
|
||||
FlightType.CAS: AirEntity.ATTACK_STRIKE,
|
||||
@@ -118,7 +113,6 @@ class FlightType(Enum):
|
||||
FlightType.INTERCEPTION: AirEntity.FIGHTER,
|
||||
FlightType.OCA_AIRCRAFT: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.OCA_RUNWAY: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.RECOVERY: AirEntity.TANKER,
|
||||
FlightType.REFUELING: AirEntity.TANKER,
|
||||
FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||
FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||
@@ -127,6 +121,5 @@ class FlightType(Enum):
|
||||
FlightType.SWEEP: AirEntity.FIGHTER,
|
||||
FlightType.TARCAP: AirEntity.FIGHTER,
|
||||
FlightType.TRANSPORT: AirEntity.UTILITY,
|
||||
FlightType.PRETENSE_CARGO: AirEntity.UTILITY,
|
||||
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
|
||||
}.get(self, AirEntity.UNSPECIFIED)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal, TYPE_CHECKING, Any, Dict, Optional
|
||||
from typing import Literal, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
|
||||
@@ -57,17 +56,3 @@ class FlightWaypoint:
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(id(self))
|
||||
|
||||
def __deepcopy__(self, memo: Optional[Dict[int, Any]] = None) -> FlightWaypoint:
|
||||
obj = FlightWaypoint(self.name, self.waypoint_type, self.position)
|
||||
for attr in dir(self):
|
||||
if attr == "control_point":
|
||||
obj.control_point = self.control_point
|
||||
elif attr == "targets":
|
||||
obj.targets = self.targets
|
||||
elif "__" in attr or attr not in obj.__dataclass_fields__:
|
||||
continue
|
||||
else:
|
||||
attr_copy = deepcopy(getattr(self, attr))
|
||||
setattr(obj, attr, attr_copy)
|
||||
return obj
|
||||
|
||||
@@ -51,5 +51,3 @@ class FlightWaypointType(IntEnum):
|
||||
INGRESS_AIR_ASSAULT = 31
|
||||
INGRESS_ANTI_SHIP = 32
|
||||
INGRESS_SEAD_SWEEP = 33
|
||||
INGRESS_ARMED_RECON = 34
|
||||
RECOVERY_TANKER = 35 # Tanker recovery point
|
||||
|
||||
@@ -9,28 +9,31 @@ if TYPE_CHECKING:
|
||||
|
||||
class IFlightRoster(ABC):
|
||||
@abstractmethod
|
||||
def iter_pilots(self) -> Iterator[Pilot | None]: ...
|
||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def pilot_at(self, idx: int) -> Pilot | None: ...
|
||||
def pilot_at(self, idx: int) -> Pilot | None:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def squadron(self) -> Squadron: ...
|
||||
def squadron(self) -> Squadron:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def max_size(self) -> int: ...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def player_count(self) -> int: ...
|
||||
def max_size(self) -> int:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def resize(self, new_size: int) -> None: ...
|
||||
def resize(self, new_size: int) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: ...
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> None: ...
|
||||
def clear(self) -> None:
|
||||
...
|
||||
|
||||
@@ -137,15 +137,11 @@ class Loadout:
|
||||
continue
|
||||
name = payload["name"]
|
||||
pylons = payload["pylons"]
|
||||
try:
|
||||
yield Loadout(
|
||||
name,
|
||||
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
||||
date=None,
|
||||
)
|
||||
except KeyError:
|
||||
# invalid loadout
|
||||
continue
|
||||
yield Loadout(
|
||||
name,
|
||||
{p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()},
|
||||
date=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def valid_payload(pylons: Dict[int, Dict[str, str]]) -> bool:
|
||||
@@ -166,11 +162,9 @@ class Loadout:
|
||||
# last - the first element in the tuple will be tried first, then the second,
|
||||
# etc.
|
||||
loadout_names = {
|
||||
t: (
|
||||
[f"Liberation {t.value}", f"Retribution {t.value}"]
|
||||
if prefer_liberation_payloads()
|
||||
else [f"Retribution {t.value}", f"Liberation {t.value}"]
|
||||
)
|
||||
t: [f"Liberation {t.value}", f"Retribution {t.value}"]
|
||||
if prefer_liberation_payloads()
|
||||
else [f"Retribution {t.value}", f"Liberation {t.value}"]
|
||||
for t in FlightType
|
||||
}
|
||||
legacy_names = {
|
||||
@@ -210,7 +204,6 @@ class Loadout:
|
||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.ARMED_RECON].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||
# DEAD also falls back to BAI.
|
||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||
|
||||
@@ -54,7 +54,8 @@ class Package(RadioFrequencyContainer):
|
||||
def has_players(self) -> bool:
|
||||
return any(flight.client_count for flight in self.flights)
|
||||
|
||||
def formation_speed(self, is_helo: bool) -> Optional[Speed]:
|
||||
@property
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@@ -65,10 +66,7 @@ class Package(RadioFrequencyContainer):
|
||||
"""
|
||||
speeds = []
|
||||
for flight in self.flights:
|
||||
if (
|
||||
isinstance(flight.flight_plan, FormationFlightPlan)
|
||||
and flight.is_helo == is_helo
|
||||
):
|
||||
if isinstance(flight.flight_plan, FormationFlightPlan):
|
||||
speeds.append(flight.flight_plan.best_flight_formation_speed)
|
||||
if not speeds:
|
||||
return None
|
||||
@@ -181,14 +179,12 @@ class Package(RadioFrequencyContainer):
|
||||
FlightType.DEAD,
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.AIR_ASSAULT,
|
||||
FlightType.ARMED_RECON,
|
||||
FlightType.SEAD,
|
||||
FlightType.SEAD_SWEEP,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.FERRY,
|
||||
FlightType.RECOVERY,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.SEAD_ESCORT,
|
||||
@@ -239,14 +235,6 @@ class Package(RadioFrequencyContainer):
|
||||
clone.time_over_target = deepcopy(package.time_over_target)
|
||||
for f in package.flights:
|
||||
cf = Flight.clone_flight(f)
|
||||
cf.flight_plan.layout = deepcopy(f.flight_plan.layout)
|
||||
cf.package = clone
|
||||
clone.add_flight(cf)
|
||||
return clone
|
||||
|
||||
def all_flights_waiting_for_start(self) -> bool:
|
||||
"""Returns True if all flights in the package are waiting for start."""
|
||||
for flight in self.flights:
|
||||
if not flight.state.is_waiting_for_start:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class PackageWaypoints:
|
||||
join: Point
|
||||
ingress: Point
|
||||
@@ -38,7 +38,7 @@ class PackageWaypoints:
|
||||
dcs_to_shapely_point(origin.position),
|
||||
dcs_to_shapely_point(package.target.position),
|
||||
coalition.doctrine,
|
||||
coalition.opponent.threat_zone.air_defenses,
|
||||
coalition.opponent.threat_zone.all,
|
||||
)
|
||||
ip_solver.set_debug_properties(
|
||||
waypoint_debug_directory() / "IP", coalition.game.theater.terrain
|
||||
@@ -51,8 +51,10 @@ class PackageWaypoints:
|
||||
ingress_point_shapely.x, ingress_point_shapely.y
|
||||
)
|
||||
|
||||
tgt_point = package.target.position
|
||||
initial_point = PackageWaypoints.get_initial_point(ingress_point, tgt_point)
|
||||
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,
|
||||
@@ -77,11 +79,3 @@ class PackageWaypoints:
|
||||
WaypointBuilder.perturb(join_point),
|
||||
refuel_point,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_initial_point(ingress_point: Point, tgt_point: Point) -> Point:
|
||||
hdg = tgt_point.heading_between_point(ingress_point)
|
||||
# Generate a waypoint randomly between 7 & 9 NM
|
||||
dist = nautical_miles(random.random() * 2 + 7).meters
|
||||
initial_point = tgt_point.point_from_heading(hdg, dist)
|
||||
return initial_point
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach
|
||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .flight import Flight
|
||||
@@ -30,8 +30,8 @@ class GroundSpeed:
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
cruise_mach = max_speed.mach() * (0.7 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude)
|
||||
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
|
||||
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Support for working with DCS group callsigns."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple, Optional
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import yaml
|
||||
from packaging.version import Version
|
||||
@@ -22,7 +22,6 @@ from .campaignairwingconfig import CampaignAirWingConfig
|
||||
from .campaigncarrierconfig import CampaignCarrierConfig
|
||||
from .campaigngroundconfig import TgoConfig
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
from ..factions import FACTIONS, Faction
|
||||
|
||||
PERF_FRIENDLY = 0
|
||||
PERF_MEDIUM = 1
|
||||
@@ -92,16 +91,6 @@ class Campaign:
|
||||
f"Invalid value for recommended_start_date in {path}: {start_date_raw}"
|
||||
)
|
||||
|
||||
player_faction = data.get("recommended_player_faction", "USA 2005")
|
||||
if isinstance(player_faction, dict):
|
||||
faction_name = cls.register_faction(campaign_file.name, player_faction)
|
||||
player_faction = faction_name if faction_name else "USA 2005"
|
||||
|
||||
enemy_faction = data.get("recommended_enemy_faction", "Russia 1990")
|
||||
if isinstance(enemy_faction, dict):
|
||||
faction_name = cls.register_faction(campaign_file.name, enemy_faction)
|
||||
enemy_faction = faction_name if faction_name else "Russia 1990"
|
||||
|
||||
return cls(
|
||||
data["name"],
|
||||
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
|
||||
@@ -109,8 +98,8 @@ class Campaign:
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
(version.major, version.minor),
|
||||
player_faction,
|
||||
enemy_faction,
|
||||
data.get("recommended_player_faction", "USA 2005"),
|
||||
data.get("recommended_enemy_faction", "Russia 1990"),
|
||||
start_date,
|
||||
start_time,
|
||||
data.get("recommended_player_money", DEFAULT_BUDGET),
|
||||
@@ -124,19 +113,6 @@ class Campaign:
|
||||
data.get("settings", {}),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_faction(
|
||||
cls, filename: str, player_faction: dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
f = Faction.from_dict(player_faction)
|
||||
FACTIONS.factions[f.name] = f
|
||||
logging.info(f"Loaded faction from campaign: {filename}")
|
||||
return f.name
|
||||
except Exception:
|
||||
logging.exception(f"Unable to load faction from campaign: {filename}")
|
||||
return None
|
||||
|
||||
def load_theater(self, advanced_iads: bool) -> ConflictTheater:
|
||||
t = TheaterLoader(self.data["theater"].lower()).load()
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||
@@ -73,31 +71,13 @@ class CampaignAirWingConfig:
|
||||
cls, data: dict[Union[str, int], Any], theater: ConflictTheater
|
||||
) -> CampaignAirWingConfig:
|
||||
by_location: dict[ControlPoint, list[SquadronConfig]] = defaultdict(list)
|
||||
carriers = theater.find_carriers()
|
||||
lhas = theater.find_lhas()
|
||||
for base_id, squadron_configs in data.items():
|
||||
base: Optional[ControlPoint] = None
|
||||
if isinstance(base_id, int):
|
||||
base = theater.find_control_point_by_airport_id(base_id)
|
||||
else:
|
||||
try:
|
||||
base = theater.control_point_named(base_id)
|
||||
except:
|
||||
if base_id == "Red CV":
|
||||
base = next((c for c in carriers if not c.captured), None)
|
||||
elif base_id == "Blue CV":
|
||||
base = next((c for c in carriers if c.captured), None)
|
||||
elif base_id == "Red LHA":
|
||||
base = next((l for l in lhas if not l.captured), None)
|
||||
elif base_id == "Blue LHA":
|
||||
base = next((l for l in lhas if l.captured), None)
|
||||
base = theater.control_point_named(base_id)
|
||||
|
||||
for squadron_data in squadron_configs:
|
||||
if base is None:
|
||||
logging.warning(
|
||||
f"Skipping squadron config for unknown base: {base_id}"
|
||||
)
|
||||
else:
|
||||
by_location[base].append(SquadronConfig.from_data(squadron_data))
|
||||
by_location[base].append(SquadronConfig.from_data(squadron_data))
|
||||
|
||||
return CampaignAirWingConfig(by_location)
|
||||
|
||||
@@ -28,15 +28,15 @@ class CampaignCarrierConfig:
|
||||
by_original_name: dict[str, CarrierConfig]
|
||||
|
||||
@classmethod
|
||||
def from_campaign_data(cls, data: dict[str, Any]) -> CampaignCarrierConfig:
|
||||
def from_campaign_data(
|
||||
cls, data: dict[str, Any]
|
||||
) -> CampaignCarrierConfig:
|
||||
by_original_name: dict[str, CarrierConfig] = defaultdict()
|
||||
for original_name, carrier_config_data in data.items():
|
||||
try:
|
||||
carrier_config = CarrierConfig.from_data(carrier_config_data)
|
||||
by_original_name[original_name] = carrier_config
|
||||
except KeyError:
|
||||
logging.warning(
|
||||
f"Skipping invalid carrier config for '{original_name}'"
|
||||
)
|
||||
logging.warning(f"Skipping invalid carrier config for '{original_name}'")
|
||||
|
||||
return CampaignCarrierConfig(by_original_name)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.squadrons import Squadron
|
||||
@@ -102,10 +101,9 @@ class DefaultSquadronAssigner:
|
||||
if aircraft not in self.coalition.faction.all_aircrafts:
|
||||
return None
|
||||
|
||||
lo = self.coalition.faction.liveries_overrides
|
||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||
if squadron_def is not None and (
|
||||
squadron_def.livery is not None or squadron_def.livery_set is not None
|
||||
):
|
||||
if squadron_def is not None and lo.get(aircraft) is None:
|
||||
return squadron_def
|
||||
|
||||
# No premade squadron available for this aircraft that meets the requirements,
|
||||
@@ -126,14 +124,11 @@ class DefaultSquadronAssigner:
|
||||
def find_squadron_for_airframe(
|
||||
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
choices = []
|
||||
for squadron in self.air_wing.squadron_defs[aircraft]:
|
||||
if not squadron.claimed and self.squadron_compatible_with(
|
||||
squadron, task, control_point
|
||||
):
|
||||
choices.append(squadron)
|
||||
if choices:
|
||||
return random.choice(choices)
|
||||
return squadron
|
||||
return None
|
||||
|
||||
def find_squadron_by_name(
|
||||
|
||||
@@ -9,7 +9,7 @@ from uuid import UUID
|
||||
from dcs import Mission
|
||||
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
|
||||
from dcs.country import Country
|
||||
from dcs.planes import F_15C, A_10A, AJS37, C_130
|
||||
from dcs.planes import F_15C, A_10A, AJS37
|
||||
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
||||
from dcs.statics import Fortification, Warehouse
|
||||
from dcs.terrain import Airport
|
||||
@@ -43,7 +43,6 @@ class MizCampaignLoader:
|
||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||
GROUND_SPAWN_UNIT_TYPE = A_10A.id
|
||||
GROUND_SPAWN_ROADBASE_UNIT_TYPE = AJS37.id
|
||||
GROUND_SPAWN_LARGE_UNIT_TYPE = C_130.id
|
||||
|
||||
CV_UNIT_TYPE = Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_Tarawa.id
|
||||
@@ -57,7 +56,7 @@ class MizCampaignLoader:
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.hy_launcher.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
||||
|
||||
COMMAND_CENTER_UNIT_TYPE = Fortification._Command_Center.id
|
||||
CONNECTION_NODE_UNIT_TYPE = Fortification.Comms_tower_M.id
|
||||
@@ -74,25 +73,23 @@ class MizCampaignLoader:
|
||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.Hawk_ln.id,
|
||||
AirDefence.S_75M_Volhov.id,
|
||||
AirDefence.x_5p73_s_125_ln.id,
|
||||
AirDefence.NASAMS_LN_B.id,
|
||||
AirDefence.NASAMS_LN_C.id,
|
||||
AirDefence.X_5p73_s_125_ln.id,
|
||||
}
|
||||
|
||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.M1097_Avenger.id,
|
||||
AirDefence.rapier_fsa_launcher.id,
|
||||
AirDefence.x_2S6_Tunguska.id,
|
||||
AirDefence.Rapier_fsa_launcher.id,
|
||||
AirDefence.X_2S6_Tunguska.id,
|
||||
AirDefence.Strela_1_9P31.id,
|
||||
}
|
||||
|
||||
AAA_UNIT_TYPES = {
|
||||
AirDefence.flak18.id,
|
||||
AirDefence.Flak18.id,
|
||||
AirDefence.Vulcan.id,
|
||||
AirDefence.ZSU_23_4_Shilka.id,
|
||||
}
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.x_1L13_EWR.id
|
||||
EWR_UNIT_TYPE = AirDefence.X_1L13_EWR.id
|
||||
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
||||
|
||||
@@ -238,12 +235,6 @@ class MizCampaignLoader:
|
||||
if group.units[0].type == self.GROUND_SPAWN_ROADBASE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ground_spawns_large(self) -> Iterator[PlaneGroup]:
|
||||
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
|
||||
if group.units[0].type == self.GROUND_SPAWN_LARGE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ground_spawns(self) -> Iterator[PlaneGroup]:
|
||||
for group in itertools.chain(self.blue.plane_group, self.red.plane_group):
|
||||
@@ -543,10 +534,6 @@ class MizCampaignLoader:
|
||||
closest, distance = self.objective_info(plane_group)
|
||||
self._add_ground_spawn(closest.ground_spawns_roadbase, plane_group)
|
||||
|
||||
for plane_group in self.ground_spawns_large:
|
||||
closest, distance = self.objective_info(plane_group)
|
||||
self._add_ground_spawn(closest.ground_spawns_large, plane_group)
|
||||
|
||||
for plane_group in self.ground_spawns:
|
||||
closest, distance = self.objective_info(plane_group)
|
||||
self._add_ground_spawn(closest.ground_spawns, plane_group)
|
||||
@@ -595,18 +582,9 @@ class MizCampaignLoader:
|
||||
self.add_preset_locations()
|
||||
self.add_supply_routes()
|
||||
self.add_shipping_lanes()
|
||||
self.add_rebel_zones()
|
||||
|
||||
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
||||
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
||||
for z in zones:
|
||||
self.mission.triggers.zones().remove(z)
|
||||
return [(z.position, z.radius) for z in zones]
|
||||
|
||||
def add_rebel_zones(self) -> None:
|
||||
zones = [
|
||||
t for t in self.mission.triggers.zones() if t.name.startswith("Rebels")
|
||||
]
|
||||
for z in zones:
|
||||
self.mission.triggers.zones().remove(z)
|
||||
self.theater.add_rebel_zones(zones)
|
||||
|
||||
@@ -23,8 +23,6 @@ class SquadronDefGenerator:
|
||||
def generate_for_task(
|
||||
self, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
settings = control_point.coalition.game.settings
|
||||
squadron_random_chance = settings.squadron_random_chance
|
||||
aircraft_choice: Optional[AircraftType] = None
|
||||
for aircraft in AircraftType.priority_list_for_task(task):
|
||||
if aircraft not in self.faction.all_aircrafts:
|
||||
@@ -32,9 +30,9 @@ class SquadronDefGenerator:
|
||||
if not control_point.can_operate(aircraft):
|
||||
continue
|
||||
aircraft_choice = aircraft
|
||||
# squadron_random_chance percent chance to keep looking for an aircraft that isn't as far up the
|
||||
# 50/50 chance to keep looking for an aircraft that isn't as far up the
|
||||
# priority list to maintain some unit variety.
|
||||
if squadron_random_chance >= random.randint(1, 100):
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
|
||||
if aircraft_choice is None:
|
||||
@@ -49,7 +47,6 @@ class SquadronDefGenerator:
|
||||
role="Flying Squadron",
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
livery_set=[],
|
||||
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
|
||||
radio_presets={},
|
||||
operating_bases=OperatingBases.default_for_aircraft(aircraft),
|
||||
|
||||
@@ -8,11 +8,10 @@ from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.ato.traveltime import TotEstimator
|
||||
from game.theater import MissionTarget, NavalControlPoint
|
||||
from game.theater import MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
from game.ato import Package
|
||||
|
||||
|
||||
class MissionScheduler:
|
||||
@@ -41,13 +40,6 @@ class MissionScheduler:
|
||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||
]
|
||||
|
||||
previous_aewc_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
|
||||
|
||||
max_simultaneous_recovery_tankers = 2 # TODO: make configurable
|
||||
carrier_etas: dict[MissionTarget, list[datetime]] = defaultdict(list)
|
||||
max_carrier_simultaneous_barcaps = 2 # TODO: make configurable
|
||||
carrier_barcaps: dict[MissionTarget, int] = defaultdict(int)
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5 * 60,
|
||||
@@ -55,8 +47,6 @@ class MissionScheduler:
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.coalition.ato.packages:
|
||||
if package.primary_task is FlightType.RECOVERY:
|
||||
continue
|
||||
tot = TotEstimator(package).earliest_tot(now)
|
||||
if package.primary_task in dca_types:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
@@ -68,27 +58,14 @@ class MissionScheduler:
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = self._get_departure_time(package)
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
is_naval_cp = isinstance(package.target, NavalControlPoint)
|
||||
count = carrier_barcaps[package.target]
|
||||
if count >= max_carrier_simultaneous_barcaps - 1 and is_naval_cp:
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
carrier_barcaps[package.target] = 0
|
||||
elif is_naval_cp:
|
||||
carrier_barcaps[package.target] += 1
|
||||
elif not is_naval_cp:
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap(now)
|
||||
elif package.primary_task is FlightType.AEWC:
|
||||
last = previous_aewc_end_time[package.target]
|
||||
package.time_over_target = tot if tot > last else last
|
||||
departure_time = self._get_departure_time(package)
|
||||
if departure_time is None:
|
||||
continue
|
||||
previous_aewc_end_time[package.target] = departure_time
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
@@ -97,34 +74,3 @@ class MissionScheduler:
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
for f in package.flights:
|
||||
if f.departure.is_fleet and not f.is_helo:
|
||||
carrier_etas[f.departure].append(
|
||||
f.flight_plan.landing_time - timedelta(minutes=10)
|
||||
)
|
||||
|
||||
# division by 2 is meant to provide some leeway to avoid filtering out too many ETAs
|
||||
duration = self.coalition.game.settings.desired_tanker_on_station_time / 2
|
||||
|
||||
for cp in carrier_etas:
|
||||
filtered: list[datetime] = []
|
||||
for eta in sorted(carrier_etas[cp]):
|
||||
count = len([t for t in filtered if eta < t + duration])
|
||||
if count < max_simultaneous_recovery_tankers:
|
||||
filtered.append(eta)
|
||||
carrier_etas[cp] = filtered
|
||||
for package in [
|
||||
p
|
||||
for p in self.coalition.ato.packages
|
||||
if p.primary_task is FlightType.RECOVERY
|
||||
]:
|
||||
if carrier_etas[package.target]:
|
||||
package.time_over_target = carrier_etas[package.target].pop(0)
|
||||
|
||||
@staticmethod
|
||||
def _get_departure_time(package: Package) -> datetime | None:
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAP/AEWC
|
||||
if departure_time is None:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
return departure_time
|
||||
|
||||
@@ -15,7 +15,6 @@ from game.theater import (
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
ParkingType,
|
||||
NavalControlPoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
@@ -148,9 +147,6 @@ class ObjectiveFinder:
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
# Off-map spawn locations don't need protection.
|
||||
continue
|
||||
if isinstance(cp, NavalControlPoint):
|
||||
yield cp # always consider CVN/LHA as vulnerable
|
||||
continue
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airbase_threat_range = self.game.settings.airbase_threat_range
|
||||
if (
|
||||
@@ -171,6 +167,24 @@ class ObjectiveFinder:
|
||||
yield cp
|
||||
break
|
||||
|
||||
def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over enemy CPs that are vulnerable to Air Assault.
|
||||
Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups.
|
||||
"""
|
||||
for cp in self.enemy_control_points():
|
||||
include = True
|
||||
for tgo in cp.connected_objectives:
|
||||
if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters:
|
||||
continue
|
||||
for u in tgo.units:
|
||||
if u.is_vehicle and u.alive:
|
||||
include = False
|
||||
break
|
||||
if not include:
|
||||
break
|
||||
if include:
|
||||
yield cp
|
||||
|
||||
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
||||
parking_type = ParkingType()
|
||||
parking_type.include_rotary_wing = True
|
||||
@@ -250,9 +264,6 @@ class ObjectiveFinder:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return closest
|
||||
|
||||
def friendly_naval_control_points(self) -> Iterator[ControlPoint]:
|
||||
return (cp for cp in self.friendly_control_points() if cp.is_fleet)
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (
|
||||
@@ -261,20 +272,17 @@ class ObjectiveFinder:
|
||||
if not c.is_friendly(self.is_player)
|
||||
)
|
||||
|
||||
def prioritized_points(self) -> list[ControlPoint]:
|
||||
def prioritized_unisolated_points(self) -> list[ControlPoint]:
|
||||
prioritized = []
|
||||
capturable_later = []
|
||||
isolated = []
|
||||
for cp in self.game.theater.control_points_for(not self.is_player):
|
||||
if cp.is_isolated:
|
||||
isolated.append(cp)
|
||||
continue
|
||||
if cp.has_active_frontline:
|
||||
prioritized.append(cp)
|
||||
else:
|
||||
capturable_later.append(cp)
|
||||
prioritized.extend(self._targets_by_range(capturable_later))
|
||||
prioritized.extend(self._targets_by_range(isolated))
|
||||
return prioritized
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
|
||||
from game.utils import nautical_miles
|
||||
from ..ato import FlightType
|
||||
from ..ato.flight import Flight
|
||||
from ..ato.package import Package
|
||||
from ..ato.starttype import StartType
|
||||
@@ -39,7 +38,7 @@ class PackageBuilder:
|
||||
self.laser_code_registry = laser_code_registry
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight, ignore_range: bool) -> bool:
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
"""Allocates aircraft for the given flight and adds them to the package.
|
||||
|
||||
If no suitable aircraft are available, False is returned. If the failed
|
||||
@@ -47,25 +46,15 @@ class PackageBuilder:
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
target = self.package.target
|
||||
heli = False
|
||||
pf = self.package.primary_flight
|
||||
if pf:
|
||||
target = (
|
||||
pf.departure
|
||||
if pf.flight_type
|
||||
in [FlightType.AEWC, FlightType.REFUELING, FlightType.RECOVERY]
|
||||
else target
|
||||
)
|
||||
heli = pf.is_helo
|
||||
heli = pf.is_helo if pf else False
|
||||
squadron = self.air_wing.best_squadron_for(
|
||||
target,
|
||||
self.package.target,
|
||||
plan.task,
|
||||
plan.num_aircraft,
|
||||
heli,
|
||||
this_turn=True,
|
||||
preferred_type=plan.preferred_type,
|
||||
ignore_range=ignore_range,
|
||||
)
|
||||
if squadron is None:
|
||||
return False
|
||||
@@ -86,16 +75,6 @@ class PackageBuilder:
|
||||
member.assign_tgp_laser_code(
|
||||
self.laser_code_registry.alloc_laser_code()
|
||||
)
|
||||
# If this is a client flight, set the start_type again to match the configured default
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1567
|
||||
if (
|
||||
squadron.location.required_aircraft_start_type is None
|
||||
and flight.roster is not None
|
||||
and flight.roster.player_count > 0
|
||||
):
|
||||
flight.start_type = (
|
||||
squadron.coalition.game.settings.default_start_type_client
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
|
||||
@@ -81,21 +81,13 @@ class PackageFulfiller:
|
||||
builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
purchase_multiplier: int,
|
||||
ignore_range: bool = False,
|
||||
) -> None:
|
||||
target = mission.location
|
||||
pf = builder.package.primary_flight
|
||||
if (
|
||||
pf
|
||||
and pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
|
||||
and flight.task is FlightType.ESCORT
|
||||
):
|
||||
target = pf.departure
|
||||
if not builder.plan_flight(flight, ignore_range):
|
||||
if not builder.plan_flight(flight):
|
||||
pf = builder.package.primary_flight
|
||||
heli = pf.is_helo if pf else False
|
||||
missing_types.add(flight.task)
|
||||
purchase_order = AircraftProcurementRequest(
|
||||
near=target,
|
||||
near=mission.location,
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft * purchase_multiplier,
|
||||
heli=heli,
|
||||
@@ -140,28 +132,12 @@ class PackageFulfiller:
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def can_plan_escort(self, type: EscortType) -> bool:
|
||||
if type == EscortType.AirToAir:
|
||||
return self.air_wing_can_plan(FlightType.ESCORT)
|
||||
elif type == EscortType.Sead:
|
||||
for task in [
|
||||
FlightType.SEAD,
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.SEAD_SWEEP,
|
||||
]:
|
||||
if self.air_wing_can_plan(task):
|
||||
return True
|
||||
elif type == EscortType.Refuel:
|
||||
return self.air_wing_can_plan(FlightType.REFUELING)
|
||||
return False
|
||||
|
||||
def plan_mission(
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
purchase_multiplier: int,
|
||||
now: datetime,
|
||||
tracer: MultiEventTracer,
|
||||
ignore_range: bool = False,
|
||||
) -> Optional[Package]:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
builder = PackageBuilder(
|
||||
@@ -199,7 +175,6 @@ class PackageFulfiller:
|
||||
builder,
|
||||
missing_types,
|
||||
purchase_multiplier,
|
||||
ignore_range,
|
||||
)
|
||||
|
||||
if missing_types:
|
||||
@@ -229,9 +204,7 @@ class PackageFulfiller:
|
||||
# This list was generated from the not None set, so this should be
|
||||
# impossible.
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type] and self.can_plan_escort(
|
||||
escort.escort_type
|
||||
):
|
||||
if needed_escorts[escort.escort_type]:
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(
|
||||
mission, escort, builder, missing_types, purchase_multiplier
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from collections.abc import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.armedrecon import PlanArmedRecon
|
||||
from game.commander.tasks.primitive.bai import PlanBai
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
@@ -11,7 +10,3 @@ class AttackBattlePositions(CompoundTask[TheaterState]):
|
||||
for battle_positions in state.enemy_battle_positions.values():
|
||||
for battle_position in battle_positions.in_priority_order:
|
||||
yield [PlanBai(battle_position)]
|
||||
# Only plan against the 2 most important CPs
|
||||
for cp in state.control_point_priority_queue[:2]:
|
||||
if not cp.is_fleet:
|
||||
yield [PlanArmedRecon(cp)]
|
||||
|
||||
@@ -8,4 +8,8 @@ from game.htn import CompoundTask, Method
|
||||
class AttackShips(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for ship in state.enemy_ships:
|
||||
yield [PlanAntiShip(ship)]
|
||||
# Ammo depots are targeted based on the needs of the front line by
|
||||
# ReduceEnemyFrontLineCapacity. No reason to target them before that front
|
||||
# line is active.
|
||||
if ship.has_live_radar_sam:
|
||||
yield [PlanAntiShip(ship)]
|
||||
|
||||
@@ -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
|
||||
@@ -22,6 +23,7 @@ class CaptureBase(CompoundTask[TheaterState]):
|
||||
yield [DestroyEnemyGroundUnits(self.front_line)]
|
||||
if self.worth_destroying_ammo_depots(state):
|
||||
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
|
||||
yield [PlanAirAssault(self.enemy_cp(state))]
|
||||
|
||||
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
||||
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
||||
|
||||
@@ -2,7 +2,6 @@ from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.capturebase import CaptureBase
|
||||
from game.commander.tasks.primitive.airassault import PlanAirAssault
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
@@ -12,5 +11,3 @@ class CaptureBases(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for front in state.active_front_lines:
|
||||
yield [CaptureBase(front)]
|
||||
for cp in state.vulnerable_control_points:
|
||||
yield [PlanAirAssault(cp)]
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Union
|
||||
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
||||
from game.commander.tasks.primitive.dead import PlanDead
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.groups import GroupTask
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||
|
||||
@@ -13,25 +12,12 @@ class DegradeIads(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for air_defense in state.threatening_air_defenses:
|
||||
yield [self.plan_against(air_defense)]
|
||||
|
||||
prioritized_air_defenses = sorted(
|
||||
[
|
||||
tgo
|
||||
for tgo in state.enemy_air_defenses
|
||||
if tgo.task in [GroupTask.LORAD, GroupTask.MERAD]
|
||||
],
|
||||
key=lambda x: (state.priority_cp.distance_to(x) if state.priority_cp else 0)
|
||||
- x.max_threat_range().meters,
|
||||
)
|
||||
|
||||
for air_defense in prioritized_air_defenses:
|
||||
yield [self.plan_against(air_defense)]
|
||||
for detector in state.detecting_air_defenses:
|
||||
yield [self.plan_against(detector)]
|
||||
|
||||
@staticmethod
|
||||
def plan_against(
|
||||
target: Union[IadsGroundObject, NavalGroundObject],
|
||||
target: Union[IadsGroundObject, NavalGroundObject]
|
||||
) -> Union[PlanDead, PlanAntiShip]:
|
||||
if isinstance(target, IadsGroundObject):
|
||||
return PlanDead(target)
|
||||
|
||||
@@ -14,7 +14,6 @@ from game.commander.tasks.compound.interdictreinforcements import (
|
||||
InterdictReinforcements,
|
||||
)
|
||||
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
||||
from game.commander.tasks.compound.recoverysupport import RecoverySupport
|
||||
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
@@ -29,10 +28,9 @@ class PlanNextAction(CompoundTask[TheaterState]):
|
||||
yield [ProtectAirSpace()]
|
||||
yield [DefendBases()]
|
||||
yield [InterdictReinforcements()]
|
||||
yield [DegradeIads()]
|
||||
yield [AttackBattlePositions()]
|
||||
yield [CaptureBases()]
|
||||
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||
yield [AttackBuildings()]
|
||||
yield [AttackShips()]
|
||||
yield [DegradeIads()]
|
||||
yield [RecoverySupport()] # for recovery tankers
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
from collections.abc import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.recovery import PlanRecovery
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class RecoverySupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanRecoverySupport()]
|
||||
|
||||
|
||||
class PlanRecoverySupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.recovery_targets:
|
||||
yield [PlanRecovery(target)]
|
||||
@@ -21,7 +21,8 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stance(self) -> CombatStance: ...
|
||||
def stance(self) -> CombatStance:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def management_allowed(state: TheaterState) -> bool:
|
||||
@@ -48,7 +49,8 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def have_sufficient_front_line_advantage(self) -> bool: ...
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
def ground_force_balance(self) -> float:
|
||||
|
||||
@@ -14,9 +14,8 @@ from game.commander.missionproposals import EscortType, ProposedFlight, Proposed
|
||||
from game.commander.packagefulfiller import PackageFulfiller
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.groups import GroupTask
|
||||
from game.settings import AutoAtoBehavior
|
||||
from game.theater import MissionTarget, ControlPoint
|
||||
from game.theater import MissionTarget
|
||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, meters
|
||||
|
||||
@@ -51,22 +50,14 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
return False
|
||||
return self.fulfill_mission(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
seen: set[ControlPoint] = set()
|
||||
if not self.package:
|
||||
return
|
||||
for f in self.package.flights:
|
||||
if f.departure.is_fleet and not f.is_helo and f.departure not in seen:
|
||||
state.recovery_targets[f.departure] += f.count
|
||||
seen.add(f.departure)
|
||||
|
||||
def execute(self, coalition: Coalition) -> None:
|
||||
if self.package is None:
|
||||
raise RuntimeError("Attempted to execute failed package planning task")
|
||||
coalition.ato.add_package(self.package)
|
||||
|
||||
@abstractmethod
|
||||
def propose_flights(self) -> None: ...
|
||||
def propose_flights(self) -> None:
|
||||
...
|
||||
|
||||
def propose_flight(
|
||||
self,
|
||||
@@ -111,14 +102,8 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
state.context.settings,
|
||||
)
|
||||
with state.context.tracer.trace(f"{color} {self.flights[0].task} planning"):
|
||||
asap = False
|
||||
if (
|
||||
not state.context.coalition.ato.has_awacs_package
|
||||
and FlightType.AEWC in [f.task for f in self.flights]
|
||||
):
|
||||
asap = True
|
||||
self.package = fulfiller.plan_mission(
|
||||
ProposedMission(self.target, self.flights, asap=asap),
|
||||
ProposedMission(self.target, self.flights),
|
||||
self.purchase_multiplier,
|
||||
state.context.now,
|
||||
state.context.tracer,
|
||||
@@ -136,9 +121,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
target_ranges: list[
|
||||
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
||||
] = []
|
||||
all_iads: Iterator[Union[IadsGroundObject, NavalGroundObject]] = (
|
||||
itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
||||
)
|
||||
all_iads: Iterator[
|
||||
Union[IadsGroundObject, NavalGroundObject]
|
||||
] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
||||
for target in all_iads:
|
||||
distance = meters(target.distance_to(self.target))
|
||||
if range_type is RangeType.Detection:
|
||||
@@ -165,16 +150,6 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
@staticmethod
|
||||
def corrective_factor_for_type(
|
||||
target: IadsGroundObject | NavalGroundObject,
|
||||
) -> float:
|
||||
return (
|
||||
1.0
|
||||
if target.task in [GroupTask.LORAD, GroupTask.MERAD]
|
||||
else 0.5 if target.task == GroupTask.AAA else 0.9
|
||||
)
|
||||
|
||||
def iter_detecting_iads(
|
||||
self, state: TheaterState
|
||||
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
||||
@@ -198,31 +173,19 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
|
||||
if not ignore_iads:
|
||||
for iads_threat in self.iter_iads_threats(state):
|
||||
weighted = self._get_weighted_threat_range(iads_threat, state)
|
||||
if weighted < meters(0):
|
||||
# Only consider blue faction flights threatened.
|
||||
# Red might still plan missions into hostile territory,
|
||||
# depending on the aggressiveness setting.
|
||||
if (
|
||||
state.context.coalition.player
|
||||
or random.randint(1, 100)
|
||||
> state.context.coalition.game.settings.opfor_autoplanner_aggressiveness
|
||||
):
|
||||
threatened = True
|
||||
if iads_threat not in state.threatening_air_defenses:
|
||||
state.threatening_air_defenses.append(iads_threat)
|
||||
return not threatened
|
||||
|
||||
def _get_weighted_threat_range(
|
||||
self,
|
||||
iads_threat: Union[IadsGroundObject | NavalGroundObject],
|
||||
state: TheaterState,
|
||||
) -> Distance:
|
||||
distance = meters(iads_threat.distance_to(self.target))
|
||||
settings = state.context.coalition.game.settings
|
||||
margin = 100 - (
|
||||
settings.ownfor_autoplanner_aggressiveness
|
||||
if state.context.coalition.player
|
||||
else settings.opfor_autoplanner_aggressiveness
|
||||
)
|
||||
threat_range = iads_threat.max_threat_range() * (margin / 100)
|
||||
corrective_factor = self.corrective_factor_for_type(iads_threat)
|
||||
threat_range *= corrective_factor
|
||||
distance_to_threat = distance - threat_range
|
||||
return distance_to_threat
|
||||
|
||||
def get_flight_size(self) -> int:
|
||||
settings = self.target.coalition.game.settings
|
||||
weights = [
|
||||
|
||||
@@ -22,11 +22,9 @@ class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.aewc_targets.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AEWC, 1)
|
||||
self.propose_flight(FlightType.ESCORT, 2)
|
||||
|
||||
@property
|
||||
def asap(self) -> bool:
|
||||
|
||||
@@ -11,7 +11,7 @@ from game.theater import ControlPoint
|
||||
@dataclass
|
||||
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.vulnerable_control_points or self.target.is_fleet:
|
||||
if self.target not in state.vulnerable_control_points:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
@@ -19,8 +19,8 @@ class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_control_points.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AIR_ASSAULT, self.get_flight_size())
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.AIR_ASSAULT, size)
|
||||
self.propose_common_escorts()
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from game.utils import meters
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
self.target not in state.threatening_air_defenses
|
||||
and not self.target.is_naval_control_point
|
||||
):
|
||||
if self.target not in state.threatening_air_defenses:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||
return False
|
||||
@@ -24,15 +21,8 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_ship(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
if self.target.max_detection_range() > meters(0):
|
||||
size = 4 # attempt to saturate ship's air-defences
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
if self.target.max_detection_range() > meters(0):
|
||||
self.propose_flight(FlightType.SEAD, 2, EscortType.Sead)
|
||||
|
||||
@@ -20,7 +20,6 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_shipping.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanArmedRecon(PackagePlanningTask[ControlPoint]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.control_point_priority_queue:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.control_point_priority_queue.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ARMED_RECON, self.get_flight_size())
|
||||
self.propose_common_escorts()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user