1 Commits

Author SHA1 Message Date
Raffson
87b630f8d5 Initial attempt 2024-02-26 00:00:27 +01:00
1184 changed files with 17063 additions and 101566 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ jobs:
with:
src: "."
options: "--check"
version: "23.9.1"
mypy:
name: Type checking

View File

@@ -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
View File

@@ -22,6 +22,5 @@ env/
/logs/
/resources/logging.yaml
/resources/plugins/pretense/pretense_output.lua
*.psd

View File

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

View File

@@ -2,7 +2,7 @@
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/dcsliberation)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Download](https://img.shields.io/github/downloads/dcs-retribution/dcs-retribution/total?label=Download)](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 !

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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": [

View File

@@ -466,7 +466,6 @@ export type Game = {
navmeshes: NavMeshes;
map_center?: LatLng;
unculled_zones: UnculledZone[];
map_zones: MapZones;
};
export type MapZones = {
inclusion: LatLng[][][];

View File

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

View File

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

View File

@@ -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,
});

View File

@@ -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",
});

View File

@@ -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}
/>
);
}

View File

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

View File

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

View File

@@ -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} />

View File

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

View File

@@ -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,
});

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
"""Objective adjacency lists."""
from __future__ import annotations
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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__(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,4 +14,5 @@ class UiZone:
class UiZoneDisplay(abc.ABC):
@abc.abstractmethod
def ui_zone(self) -> UiZone: ...
def ui_zone(self) -> UiZone:
...

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:
...

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
"""Support for working with DCS group callsigns."""
import logging
import re
from typing import Any

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity,
)
from game.commander.tasks.primitive.airassault import PlanAirAssault
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [

View File

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

View File

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

View File

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

View File

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

View File

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