4 Commits
dev ... v1.3.1

Author SHA1 Message Date
Raffson
18ff480c07 Update changelog 2024-03-17 01:25:25 +01:00
Raffson
19fd79c25c Avoid crash with incompatible save on startup 2024-03-17 01:25:24 +01:00
Raffson
f992310a36 Fix save-compat issue 2024-03-17 00:57:26 +01:00
Raffson
edcadde368 Update version to 1.3.1 2024-03-17 00:53:57 +01:00
1605 changed files with 34247 additions and 246721 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.4.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.4.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,10 +1,10 @@
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:
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: "16"
cache: npm

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@v5
uses: actions/setup-python@v2
with:
python-version: "3.11"
python-version: "3.10"
cache: pip
- name: Install environment

View File

@@ -12,7 +12,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true
@@ -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

@@ -7,18 +7,19 @@ jobs:
name: Black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
src: "."
options: "--check"
version: "23.9.1"
mypy:
name: Type checking
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true

View File

@@ -14,7 +14,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true
@@ -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

View File

@@ -5,7 +5,7 @@ jobs:
name: Python tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
submodules: true
@@ -24,7 +24,7 @@ jobs:
name: Typescript tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js

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

@@ -68,7 +68,7 @@ Our plans for future releases can be found on our
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
Each planned release has a Project, and the page for that project has columns for to do,
in progress, and done. Items in the Done column are in the
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
for that release. Items in the To do column are planned to be added to that release.
## Resources
@@ -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,115 +1,3 @@
# Retribution v1.5.0
## Features/Improvements
* **[Campaigns]** Ability to define invisible FOBs
* **[Campaigns]** Ability to define influence zones for Control Points
* **[Plugins]** Improvements to AI support for EW Script 2.0
* **[Plugins]** Add moose as a base plugin
* **[Config]** New preference setting to trigger the first-start window on every start (could help in scenarios multiple Retribution instances need to run concurrently)
* **[Modding]** Update Grinelli Designs F-22A Mod to 2.0.0 (May 2025)
* **[Campaign Design]** Added support for Germany Cold War terrain by Ugra Media
* **[Options]** New option to control EPLRS-task injection in mission generator
* **[Modding]** Add Tornado F3 ADV (v1.0, requires FC3 fix)
* **[Modding]** Add Military Aircraft Mod 1.7.2 (See VSN Discord)
* **[Payload Editor]** Add Sniper POD
* **[Modding]** Update SU-30 mod to v2.8.04 Beta + CWS 3.72
* **[Plugins]** EW Script - DEAD added to applicable flight types.
* **[Plugins]** EW Script - Offensive Jamming restricted to aircraft with ALQ99/249 pods, or "has_built_in_jamming: true" in aircarft yaml (AI and Player)
* **[Modding]** Update to CJS Super Hornet Mod v2.4.4
* **[Engine]** Support for DCS 2.9.20.15010, including MiG-29A Fulcrum support.
* **[Campaigns]** Ability to define neutral FOBs and Airfields
* **[Modding]** Add Airboss Moose Module
* **[AirWing]** Use aircraft display names for easier differentiation between modules
## Fixes
* **[Flight Plans]** Fixed bugs wrt planning escort flights
* **[Flight Plans]** Added AntiShipStrike as a fallback task for OCA/Aircraft to fix a bug where the S-3B could not do OCA/Aircraft
* **[Squadrons]** Fixed a bug where loading an air wing config would not properly load all squadrons
* **[Flight Plans]** Fixed a bug where SEAD flights would fire one ARM and RTB
* **[Plugins]** EW Script - Fix radar detection routine.
* **[Campaign]** Fixed a bug where sinking a destroyer in a carrier group would cause squadrons to be removed from the carrier
* **[Engine]** Fixed a bug with transfers to Helipads
* **[Engine]** Fixed a bug with parking allocation
* **[Flight Plans]** Fixed a bug where divert airfield was broken for opfor
* **[Engine]** Fixed a bug with state.json loading wrt transfers
* **[Engine]** Fixed a bug wrt pretense generation and moose script conflicts
* **[Engine]** Fixed a bug where frontline debriefing was not properly calculated
* **[Mission Generation]** Fixed an issue where blue and red units spawned next to each other on frontlines
# Retribution v1.4.1 (hotfix)
## Fixes
* **[Plugins]** Avoid injecting jamming-tasks for player-controller aircraft when EW-Jamming plugin is disabled
# 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.
@@ -160,7 +48,6 @@
* **[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)

28622
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": "^28.3.2",
"@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

@@ -182,7 +182,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 +355,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 +397,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

@@ -60,7 +60,7 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
useEffect(() => {
const waypoint = props.waypoint;
marker.current?.setTooltipContent(
`${props.number-1} ${waypoint.name}<br />` +
`${props.number} ${waypoint.name}<br />` +
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
waypoint.timing
);

View File

@@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Retribution"
copyright = "2025, DCS Retribution Team"
copyright = "2023, DCS Retribution Team"
author = "DCS Retribution Team"
release = "1.5.0"
release = "1.3.1"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -40,7 +40,7 @@ class ArmedForces:
# all layouts which are usable by the given faction.
for layout in LAYOUTS.layouts:
if layout.generic and layout.usable_by_faction(faction):
# Creates a faction compatible ForceGroup
# Creates a faction compatible GorceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
# Add all preset groups afterwards to prevent them being merged with generics

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:
@@ -258,15 +248,8 @@ class ForceGroup:
if unit_count == 0:
# No units to be created so dont create a theater group for them
return
# If the control point is neutral, don't create any units at all
if ground_object.control_point.captured.is_neutral:
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:
@@ -321,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")
@@ -384,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

@@ -35,7 +35,6 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents
from game.sim.simulationresults import SimulationResults
from game.squadrons import Squadron, Pilot
from game.theater.player import Player
from game.transfers import TransferOrder
from game.data.weapons import WeaponType
from .flightmember import FlightMember
@@ -90,7 +89,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
@@ -175,7 +173,7 @@ class Flight(
self.roster = FlightMembers.from_roster(self, self.roster)
@property
def blue(self) -> Player:
def blue(self) -> bool:
return self.squadron.player
@property
@@ -276,6 +274,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(
@@ -286,7 +286,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

@@ -12,7 +12,7 @@ from game.utils import Distance, Heading, Speed, 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:

View File

@@ -75,6 +75,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 +89,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
@@ -125,18 +136,10 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
pickup.alt = altitude
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)
@@ -162,7 +165,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
pickup=pickup,
nav_to=builder.nav_path(
pickup_position,
ingress.position,
self.package.waypoints.ingress,
altitude,
altitude_is_agl,
),
@@ -179,7 +182,7 @@ 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(),

View File

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

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

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

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

@@ -125,12 +125,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"

View File

@@ -60,16 +60,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,8 +11,6 @@ from .formationattack import (
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ..packagewaypoints import PackageWaypoints
from ...utils import feet
class EscortFlightPlan(FormationAttackFlightPlan):
@@ -21,48 +18,23 @@ class EscortFlightPlan(FormationAttackFlightPlan):
def builder_type() -> Type[Builder]:
return Builder
@property
def split_time(self) -> datetime:
# Avoid infinite recursion when this escort flight is itself the primary flight
# This can happen when only escort flights remain in a package
if (
self.package.primary_flight
and self.package.primary_flight != self.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
)
@@ -87,6 +59,8 @@ 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(

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
@@ -106,6 +105,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 +268,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 +306,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
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",
)
@@ -193,7 +192,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)
@@ -288,8 +286,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

@@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints
if TYPE_CHECKING:
from game.coalition import Coalition
from game.data.doctrine import Doctrine
from game.theater import ConflictTheater, Player
from game.theater import ConflictTheater
from game.threatzones import ThreatZones
from ..flight import Flight
from ..package import Package
@@ -38,7 +38,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
self._generate_package_waypoints_if_needed(dump_debug_info)
self._flight_plan = self.build(dump_debug_info)
except NavMeshError as ex:
color = "blue" if self.flight.squadron.player.is_blue else "red"
color = "blue" if self.flight.squadron.player else "red"
raise PlanningError(
f"Could not plan {color} {self.flight.flight_type.value} from "
f"{self.flight.departure} to {self.package.target}"
@@ -60,7 +60,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:
@@ -71,7 +72,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
return self.flight.coalition
@property
def is_player(self) -> Player:
def is_player(self) -> bool:
return self.coalition.player
@property

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

@@ -80,9 +80,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

@@ -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
@@ -87,10 +86,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

@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import timedelta
from typing import Type
from game.utils import Heading, meters, nautical_miles
@@ -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:

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,13 @@ 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
@@ -251,13 +249,6 @@ class WaypointBuilder:
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_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules:
alt_type = "RADIO"
@@ -274,11 +265,9 @@ class WaypointBuilder:
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,19 +275,6 @@ 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:
@@ -331,11 +307,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,
@@ -364,21 +336,6 @@ class WaypointBuilder:
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 +380,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",
@@ -559,13 +505,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(
@@ -698,13 +639,14 @@ class WaypointBuilder:
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)
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else meters(0)
return FlightWaypoint(
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
drop_off.position,
alt,
altitude,
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop-off zone",
@@ -802,22 +744,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
@@ -19,10 +19,8 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .campaigncarrierconfig import CampaignCarrierConfig
from .campaigngroundconfig import TgoConfig
from .mizcampaignloader import MizCampaignLoader
from ..factions import FACTIONS, Faction
PERF_FRIENDLY = 0
PERF_MEDIUM = 1
@@ -92,16 +90,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 +97,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 +112,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()
@@ -165,13 +140,6 @@ class Campaign:
return CampaignAirWingConfig({})
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
def load_carrier_config(self) -> CampaignCarrierConfig:
try:
carrier_data = self.data["carriers"]
except KeyError:
return CampaignCarrierConfig({})
return CampaignCarrierConfig.from_campaign_data(carrier_data)
def load_ground_forces_config(self) -> TgoConfig:
ground_forces = self.data.get("ground_forces", {})
if not ground_forces:

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING, Union
@@ -25,7 +24,6 @@ class SquadronConfig:
name: Optional[str]
nickname: Optional[str]
female_pilot_percentage: Optional[int]
aircraft_type: Optional[str]
@property
def auto_assignable(self) -> set[FlightType]:
@@ -51,7 +49,6 @@ class SquadronConfig:
data.get("name", None),
data.get("nickname", None),
data.get("female_pilot_percentage", None),
data.get("aircraft_type", None),
)
@staticmethod
@@ -74,30 +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:
logging.warning(
f"Control point {base_id} not found, trying to match by full name"
)
if not base:
try:
base = theater.control_point_by_full_name(base_id)
except KeyError:
logging.error(f"Control point {base_id} not found, skipping")
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

@@ -1,42 +0,0 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING
from game.dcs.shipunittype import ShipUnitType
if TYPE_CHECKING:
pass
@dataclass(frozen=True)
class CarrierConfig:
preferred_name: str
preferred_type: ShipUnitType
@classmethod
def from_data(cls, data: dict[str, Any]) -> CarrierConfig:
return CarrierConfig(
str(data["preferred_name"]), ShipUnitType.named(data["preferred_type"])
)
@dataclass(frozen=True)
class CampaignCarrierConfig:
by_original_name: dict[str, CarrierConfig]
@classmethod
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}'"
)
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
@@ -60,7 +59,7 @@ class DefaultSquadronAssigner:
) -> Optional[SquadronDef]:
for preferred_aircraft in config.aircraft:
squadron_def = self.find_preferred_squadron(
preferred_aircraft, config.aircraft_type, config.primary, control_point
preferred_aircraft, config.primary, control_point
)
if squadron_def is not None:
return squadron_def
@@ -78,11 +77,7 @@ class DefaultSquadronAssigner:
)
def find_preferred_squadron(
self,
preferred_aircraft: str,
aircraft_type: Optional[str],
task: FlightType,
control_point: ControlPoint,
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]:
# Attempt to find a squadron with the name in the request.
squadron_def = self.find_squadron_by_name(
@@ -94,7 +89,7 @@ class DefaultSquadronAssigner:
# If the name didn't match a squadron available to this coalition, try to find
# an aircraft with the matching name that meets the requirements.
try:
aircraft = AircraftType.named(aircraft_type or preferred_aircraft)
aircraft = AircraftType.named(preferred_aircraft)
except KeyError:
logging.warning(
"%s is neither a compatible squadron or a known aircraft type, "
@@ -106,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,
@@ -130,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,14 +9,13 @@ 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
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
from game.point_with_heading import PointWithHeading
from game.positioned import Positioned
from game.profiling import logged_duration
@@ -25,8 +24,6 @@ from game.theater.controlpoint import (
Airfield,
Carrier,
ControlPoint,
ControlPointType,
Player,
Fob,
Lha,
OffMapSpawn,
@@ -46,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
@@ -56,13 +52,11 @@ class MizCampaignLoader:
FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id
NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id
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
@@ -79,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
@@ -125,13 +117,9 @@ class MizCampaignLoader:
def control_point_from_airport(
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
) -> ControlPoint:
if airport.dynamic_spawn:
starting_coalition = Player.NEUTRAL
elif airport.is_blue():
starting_coalition = Player.BLUE
else:
starting_coalition = Player.RED
cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones)
cp = Airfield(
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
)
# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
@@ -139,54 +127,42 @@ class MizCampaignLoader:
return cp
def country(self, blue: Player) -> Country:
if blue.is_blue:
country = self.mission.country(self.BLUE_COUNTRY.name)
else:
country = self.mission.country(self.RED_COUNTRY.name)
def country(self, blue: bool) -> Country:
country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
)
# Should be guaranteed because we initialized them.
assert country
return country
@property
def blue(self) -> Country:
return self.country(blue=Player.BLUE)
return self.country(blue=True)
@property
def red(self) -> Country:
return self.country(blue=Player.RED)
return self.country(blue=False)
def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]:
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
def carriers(self, blue: Player) -> Iterator[ShipGroup]:
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE:
yield group
def lhas(self, blue: Player) -> Iterator[ShipGroup]:
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
def fobs(self, blue: Player) -> Iterator[VehicleGroup]:
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE:
yield group
def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE:
yield group
@property
def neutral_fobs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE:
yield group
@property
def ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group:
@@ -259,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):
@@ -293,22 +263,16 @@ class MizCampaignLoader:
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@cached_property
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
return ControlPointInfluenceRadius.from_trigger_zones(
self.mission.triggers._zones
)
@cached_property
def control_points(self) -> dict[UUID, ControlPoint]:
control_points = {}
for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red() or airport.is_neutral():
if airport.is_blue() or airport.is_red():
ctld_zones = self.get_ctld_zones(airport.name)
control_point = self.control_point_from_airport(airport, ctld_zones)
control_points[control_point.id] = control_point
for blue in (Player.RED, Player.BLUE):
for blue in (False, True):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(
str(group.name), group.position, self.theater, starts_blue=blue
@@ -339,47 +303,17 @@ class MizCampaignLoader:
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
for fob in self.invisible_fobs(blue):
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=blue,
ctld_zones=ctld_zones,
is_invisible=True,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
for fob in self.neutral_fobs:
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=Player.NEUTRAL,
ctld_zones=ctld_zones,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
if self.cp_influence_zones:
for cp in control_points.values():
for influence_radius in self.cp_influence_zones:
if cp.full_name == influence_radius.cp_name:
cp.influence_radius = influence_radius
return control_points
@property
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=Player.BLUE).vehicle_group:
for group in self.country(blue=True).vehicle_group:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group
@property
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
for group in self.country(blue=Player.BLUE).ship_group:
for group in self.country(blue=True).ship_group:
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
yield group
@@ -403,7 +337,7 @@ class MizCampaignLoader:
@property
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=Player.BLUE).vehicle_group:
for group in self.country(blue=True).vehicle_group:
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
yield group
@@ -528,59 +462,7 @@ class MizCampaignLoader:
def objective_info(
self, near: Positioned, allow_naval: bool = False
) -> Tuple[ControlPoint, Distance]:
zones_containing_point = [
z
for z in self.cp_influence_zones
if point_in_zone(z.zone_def, near.position)
]
# Ensure we only consider naval control points if allow_naval is True
candidates = [
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
]
if not allow_naval:
candidates = [
cp
for cp in candidates
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
if candidates:
closest = min(
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
# If no zones contain the point, find the closest control point without an influence radius
if not allow_naval:
fallback_candidates = [
cp
for cp in self.theater.controlpoints
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
else:
fallback_candidates = self.theater.controlpoints
fallback_candidates = [
cp for cp in fallback_candidates if not cp.influence_radius
]
if not fallback_candidates:
raise RuntimeError(
f"All control points have an influence zone but no zones contain {near} at {near.position}"
)
closest = min(
fallback_candidates,
key=lambda cp: cp.position.distance_to_point(near.position),
)
closest = self.theater.closest_control_point(near.position, allow_naval)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
@@ -652,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)
@@ -704,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

@@ -17,7 +17,6 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi
from game.profiling import MultiEventTracer, logged_duration
from game.squadrons import AirWing
from game.theater.bullseye import Bullseye
from game.theater.player import Player
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from game.threatzones import ThreatZones
from game.transfers import PendingTransfers
@@ -33,7 +32,7 @@ if TYPE_CHECKING:
class Coalition:
def __init__(
self, game: Game, faction: Faction, budget: float, player: Player
self, game: Game, faction: Faction, budget: float, player: bool
) -> None:
self.game = game
self.player = player
@@ -69,11 +68,9 @@ class Coalition:
@property
def coalition_id(self) -> int:
if self.player.is_blue:
if self.player:
return 2
elif self.player.is_red:
return 1
return 0
return 1
@property
def opponent(self) -> Coalition:
@@ -98,23 +95,12 @@ class Coalition:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
del state["_threat_zone"]
del state["_navmesh"]
del state["faker"]
# TODO: Figure out why this is needed after adding neutral point support
if state["player"] != Player.NEUTRAL:
del state["_threat_zone"]
del state["_navmesh"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
@@ -217,7 +203,7 @@ class Coalition:
squadron.refund_orders()
def plan_missions(self, now: datetime) -> None:
color = "Blue" if self.player.is_blue else "Red"
color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
@@ -234,7 +220,7 @@ class Coalition:
# to ground forces and 1400 to aircraft. After that the budget will be spent
# proportionally based on how much is already invested.
if self.player.is_blue:
if self.player:
manage_runways = self.game.settings.automate_runway_repair
manage_front_line = self.game.settings.automate_front_line_reinforcements
manage_aircraft = self.game.settings.automate_aircraft_reinforcements

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

@@ -1,6 +1,5 @@
from __future__ import annotations
import itertools
import math
import operator
from collections.abc import Iterable, Iterator
@@ -16,8 +15,6 @@ from game.theater import (
MissionTarget,
OffMapSpawn,
ParkingType,
NavalControlPoint,
Player,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -37,7 +34,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
class ObjectiveFinder:
"""Identifies potential objectives for the mission planner."""
def __init__(self, game: Game, is_player: Player) -> None:
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
self.is_player = is_player
@@ -150,13 +147,10 @@ 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 (
self.is_player.is_red
not self.is_player
and randint(1, 100)
> self.game.settings.opfor_autoplanner_aggressiveness
):
@@ -173,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
@@ -218,7 +230,7 @@ class ObjectiveFinder:
def farthest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is farthest from any threats."""
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
threat_zones = self.game.threat_zone_for(not self.is_player)
farthest = None
max_distance = meters(0)
@@ -236,7 +248,7 @@ class ObjectiveFinder:
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
@@ -252,54 +264,25 @@ 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 (
c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL
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(self.is_player.opponent):
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
def air_assault_targets(self) -> list[ControlPoint]:
"""Returns control points suitable for air assault missions, including neutral bases."""
prioritized = []
capturable_later = []
isolated = []
combined_control_points = itertools.chain(
self.game.theater.control_points_for(self.is_player.opponent),
self.game.theater.control_points_for(Player.NEUTRAL),
)
for cp in combined_control_points:
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
@@ -47,19 +46,10 @@ 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,
@@ -86,16 +76,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

@@ -43,9 +43,7 @@ class PackageFulfiller:
@property
def is_player(self) -> bool:
if self.coalition.player.is_blue:
return True
return False
return self.coalition.player
@property
def ato(self) -> AirTaskingOrder:
@@ -85,19 +83,12 @@ class PackageFulfiller:
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):
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,
@@ -142,21 +133,6 @@ 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,
@@ -231,9 +207,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,10 +7,11 @@ 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
from game.theater import ControlPoint, FrontLine, Player
from game.theater import ControlPoint, FrontLine
@dataclass(frozen=True)
@@ -22,16 +23,17 @@ 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)
def units_deployable(self, state: TheaterState, player: Player) -> int:
def units_deployable(self, state: TheaterState, player: bool) -> int:
cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp))
return cp.deployable_front_line_units_with(len(ammo_depots))
def unit_cap(self, state: TheaterState, player: Player) -> int:
def unit_cap(self, state: TheaterState, player: bool) -> int:
cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp))
return cp.front_line_capacity_with(len(ammo_depots))

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

@@ -11,23 +11,23 @@ from game.theater import FrontLine
if TYPE_CHECKING:
from game.coalition import Coalition
from game.theater.player import Player
class FrontLineStanceTask(TheaterCommanderTask, ABC):
def __init__(self, front_line: FrontLine, player: Player) -> None:
def __init__(self, front_line: FrontLine, player: bool) -> None:
self.front_line = front_line
self.friendly_cp = self.front_line.control_point_friendly_to(player)
self.enemy_cp = self.front_line.control_point_hostile_to(player)
@property
@abstractmethod
def stance(self) -> CombatStance: ...
def stance(self) -> CombatStance:
...
@staticmethod
def management_allowed(state: TheaterState) -> bool:
return (
not state.context.coalition.player.is_blue
not state.context.coalition.player
or state.context.settings.automate_front_line_stance
)
@@ -49,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
@@ -45,28 +44,20 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player.is_blue
state.context.coalition.player
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
):
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,
@@ -102,7 +93,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
return 1
def fulfill_mission(self, state: TheaterState) -> bool:
color = "blue" if state.context.coalition.player.is_blue else "red"
color = "blue" if state.context.coalition.player else "red"
self.propose_flights()
fulfiller = PackageFulfiller(
state.context.coalition,
@@ -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.is_blue
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

@@ -12,7 +12,7 @@ from game.theater import MissionTarget
class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player.is_blue
state.context.coalition.player
and not state.context.settings.auto_ato_behavior_awacs
):
return False
@@ -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()

View File

@@ -19,7 +19,6 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def apply_effects(self, state: TheaterState) -> None:
state.eliminate_battle_position(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
tgt_count = self.target.alive_unit_count

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from random import randint
from game.ato.flighttype import FlightType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
@@ -19,7 +21,6 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
def apply_effects(self, state: TheaterState) -> None:
state.barcaps_needed[self.target] -= 1
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()

View File

@@ -6,7 +6,7 @@ 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 import FrontLine, Player
from game.theater import FrontLine
@dataclass
@@ -19,18 +19,18 @@ class PlanCas(PackagePlanningTask[FrontLine]):
# An exception is made for turn zero since that's not being truly planned, but
# just to determine what missions should be planned on turn 1 (when there *will*
# be ground units) and what aircraft should be ordered.
player = state.context.coalition.player.opponent
enemy_cp = self.target.control_point_friendly_to(player)
enemy_cp = self.target.control_point_friendly_to(
player=not state.context.coalition.player
)
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
return False
return super().preconditions_met(state)
def apply_effects(self, state: TheaterState) -> None:
state.vulnerable_front_lines.remove(self.target)
super().apply_effects(state)
def propose_flights(self) -> None:
size = self.get_flight_size()
self.propose_flight(FlightType.CAS, size)
self.propose_flight(FlightType.TARCAP, 2, EscortType.AirToAir)
self.propose_flight(FlightType.SEAD_SWEEP, 2)
self.propose_flight(FlightType.SEAD_SWEEP, 2, EscortType.Sead)

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