diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9d0da6a6..71d5cf3c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,15 +18,34 @@ jobs:
- name: Install environment
run: |
- py -m venv ./venv
+ python -m venv ./venv
- name: Install dependencies
run: |
./venv/scripts/activate
- pip install -r requirements.txt
+ python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
+ - name: mypy game
+ run: |
+ ./venv/scripts/activate
+ mypy game
+
+ - name: mypy gen
+ run: |
+ ./venv/scripts/activate
+ mypy gen
+
+ - name: mypy theater
+ run: |
+ ./venv/scripts/activate
+ mypy theater
+
+ - name: update build number
+ run: |
+ [IO.File]::WriteAllLines($pwd.path + "\buildnumber", $env:GITHUB_RUN_NUMBER)
+
- name: Build binaries
run: |
./venv/scripts/activate
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5fad7bff..21806ed4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,15 +20,30 @@ jobs:
- name: Install environment
run: |
- py -m venv ./venv
+ python -m venv ./venv
- name: Install dependencies
run: |
./venv/scripts/activate
- pip install -r requirements.txt
+ python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
+ - name: mypy game
+ run: |
+ ./venv/scripts/activate
+ mypy game
+
+ - name: mypy gen
+ run: |
+ ./venv/scripts/activate
+ mypy gen
+
+ - name: mypy theater
+ run: |
+ ./venv/scripts/activate
+ mypy theater
+
- name: Build binaries
run: |
./venv/scripts/activate
diff --git a/.gitignore b/.gitignore
index 8c7d79f3..e53921ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@ logs.txt
dist/**
a.py
resources/tools/a.miz
-tests/**
# User-specific stuff
.idea/
@@ -16,9 +15,8 @@ tests/**
/liberation_preferences.json
/state.json
-logs/liberation.log
+logs/
qt_ui/logs/liberation.log
*.psd
-resources/scripts/plugins/*
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index d8c82a7e..00000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
- "vsintellicode.python.completionsEnabled": true
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index d29644a7..4d51bd19 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,10 @@

## About DCS Liberation
-DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player semi dynamic campaign.
+DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
-
+
## Downloads
diff --git a/changelog.md b/changelog.md
index e2c69706..df66d42d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,46 @@
+# 2.2.0
+
+## Features/Improvements :
+* **[Campaign Generator]** Added early warning radar generation
+* **[Campaign Generator]** Added scud launcher sites
+* **[Cheat Menu]** Added ability to capture base from mission planner
+* **[Cheat Menu]** Added ability to show red ATO
+* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
+* **[Factions]** Cold War / Middle eastern factions will use Flak sites
+* **[Flight Planner]** Flight planner overhaul, with package and TOT system
+* **[Flight Planner]** Pick runways and ascent/descent based on headwind
+* **[Map]** Added polygon debug mode display
+* **[Map]** Highlight the selected flight path on the map
+* **[Map]** Improved SAM display settings
+* **[Map]** Improved flight plan display settings
+* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
+* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
+* **[Moddability]** Custom campaigns can be designed through json files
+* **[Moddability]** LUA plugins can now be injected into Liberation missions.
+* **[Moddability]** Optional Skynet IADS lua plugin now included
+* **[New Game]** Starting budget can be freely selected
+* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
+* **[UI]** Add double and right click actions to many UI elements.
+* **[UI]** Add polygon drawing mode for map background
+* **[UI]** Added a warning if you press takeoff with no player enabled flights
+* **[UI]** Packages and flights now visible in the main window sidebar
+* **[Units/Factions]** Added bombers to some coalitions
+* **[Units/Factions]** Added support for SU-57 mod by Cubanace
+* **[Units]** Added Freya EWR sites to german WW2 factions
+* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
+* **[Units]** Added support for new P-47 variants
+
+## Fixes :
+* **[Campaign Generator]** Big airbases could end up without any airbase defense.
+* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
+* **[Flight Planner]** Fix waypoint alitudes for helicopters
+* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
+* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
+* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
+* **[Radios]** Fix A-10C radio
+* **[UI]** Many missing unit icons were added
+* **[UI]** Missing TER weapons in custom payload now selectable.
+
# 2.1.5
## Features/Improvements :
@@ -10,9 +53,7 @@
# 2.1.4
## Fixes :
-* **[UI]** Fixed an issue that prevent generating the mission (take off button no working) on old savegames.
-
-# 2.1.3
+* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
## Features/Improvements :
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions
@@ -276,4 +317,4 @@ Sorry :(
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
-* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
\ No newline at end of file
+* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
diff --git a/game/__init__.py b/game/__init__.py
index 126a7d24..c651b19d 100644
--- a/game/__init__.py
+++ b/game/__init__.py
@@ -1,2 +1,3 @@
from .game import Game
-from . import db
\ No newline at end of file
+from . import db
+from .version import VERSION
diff --git a/game/data/building_data.py b/game/data/building_data.py
index bd6ab666..ab2555c3 100644
--- a/game/data/building_data.py
+++ b/game/data/building_data.py
@@ -1,10 +1,11 @@
import inspect
import dcs
-DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
+DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
-WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
-WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
+WW2_FREE = ['fuel', 'factory', 'ware']
+WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
+WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
diff --git a/game/data/cap_capabilities_db.py b/game/data/cap_capabilities_db.py
index eb367238..1ee3f075 100644
--- a/game/data/cap_capabilities_db.py
+++ b/game/data/cap_capabilities_db.py
@@ -1,4 +1,24 @@
-from dcs.planes import *
+from dcs.planes import (
+ Bf_109K_4,
+ C_101CC,
+ FW_190A8,
+ FW_190D9,
+ F_5E_3,
+ F_86F_Sabre,
+ I_16,
+ L_39ZA,
+ MiG_15bis,
+ MiG_19P,
+ MiG_21Bis,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW
+)
+
from pydcs_extensions.a4ec.a4ec import A_4E_C
"""
@@ -23,6 +43,8 @@ GUNFIGHTERS = [
P_51D_30_NA,
P_51D,
P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
diff --git a/game/data/doctrine.py b/game/data/doctrine.py
index 866ae897..8d3e1a91 100644
--- a/game/data/doctrine.py
+++ b/game/data/doctrine.py
@@ -1,95 +1,111 @@
+from dataclasses import dataclass
+from datetime import timedelta
+
from game.utils import nm_to_meter, feet_to_meter
-MODERN_DOCTRINE = {
- "GENERATORS": {
- "CAS": True,
- "CAP": True,
- "SEAD": True,
- "STRIKE": True,
- "ANTISHIP": True,
- },
+@dataclass(frozen=True)
+class Doctrine:
+ cas: bool
+ cap: bool
+ sead: bool
+ strike: bool
+ antiship: bool
- "STRIKE_MAX_RANGE": 1500000,
- "SEAD_MAX_RANGE": 1500000,
+ strike_max_range: int
+ sead_max_range: int
- "CAP_EVERY_X_MINUTES": 20,
- "CAS_EVERY_X_MINUTES": 30,
- "SEAD_EVERY_X_MINUTES": 40,
- "STRIKE_EVERY_X_MINUTES": 40,
+ rendezvous_altitude: int
+ join_distance: int
+ split_distance: int
+ ingress_egress_distance: int
+ ingress_altitude: int
+ egress_altitude: int
- "INGRESS_EGRESS_DISTANCE": nm_to_meter(45),
- "INGRESS_ALT": feet_to_meter(20000),
- "EGRESS_ALT": feet_to_meter(20000),
- "PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)),
- "PATTERN_ALTITUDE": feet_to_meter(5000),
+ min_patrol_altitude: int
+ max_patrol_altitude: int
+ pattern_altitude: int
- "CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)),
- "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)),
- "CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)),
+ cap_duration: timedelta
+ cap_min_track_length: int
+ cap_max_track_length: int
+ cap_min_distance_from_cp: int
+ cap_max_distance_from_cp: int
- "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
-}
+ cas_duration: timedelta
-COLDWAR_DOCTRINE = {
- "GENERATORS": {
- "CAS": True,
- "CAP": True,
- "SEAD": True,
- "STRIKE": True,
- "ANTISHIP": True,
- },
+MODERN_DOCTRINE = Doctrine(
+ cap=True,
+ cas=True,
+ sead=True,
+ strike=True,
+ antiship=True,
+ strike_max_range=1500000,
+ sead_max_range=1500000,
+ rendezvous_altitude=feet_to_meter(25000),
+ join_distance=nm_to_meter(20),
+ split_distance=nm_to_meter(20),
+ ingress_egress_distance=nm_to_meter(45),
+ ingress_altitude=feet_to_meter(20000),
+ egress_altitude=feet_to_meter(20000),
+ min_patrol_altitude=feet_to_meter(15000),
+ max_patrol_altitude=feet_to_meter(33000),
+ pattern_altitude=feet_to_meter(5000),
+ cap_duration=timedelta(minutes=30),
+ cap_min_track_length=nm_to_meter(15),
+ cap_max_track_length=nm_to_meter(40),
+ cap_min_distance_from_cp=nm_to_meter(10),
+ cap_max_distance_from_cp=nm_to_meter(40),
+ cas_duration=timedelta(minutes=30),
+)
- "STRIKE_MAX_RANGE": 1500000,
- "SEAD_MAX_RANGE": 1500000,
+COLDWAR_DOCTRINE = Doctrine(
+ cap=True,
+ cas=True,
+ sead=True,
+ strike=True,
+ antiship=True,
+ strike_max_range=1500000,
+ sead_max_range=1500000,
+ rendezvous_altitude=feet_to_meter(22000),
+ join_distance=nm_to_meter(10),
+ split_distance=nm_to_meter(10),
+ ingress_egress_distance=nm_to_meter(30),
+ ingress_altitude=feet_to_meter(18000),
+ egress_altitude=feet_to_meter(18000),
+ min_patrol_altitude=feet_to_meter(10000),
+ max_patrol_altitude=feet_to_meter(24000),
+ pattern_altitude=feet_to_meter(5000),
+ cap_duration=timedelta(minutes=30),
+ cap_min_track_length=nm_to_meter(12),
+ cap_max_track_length=nm_to_meter(24),
+ cap_min_distance_from_cp=nm_to_meter(8),
+ cap_max_distance_from_cp=nm_to_meter(25),
+ cas_duration=timedelta(minutes=30),
+)
- "CAP_EVERY_X_MINUTES": 20,
- "CAS_EVERY_X_MINUTES": 30,
- "SEAD_EVERY_X_MINUTES": 40,
- "STRIKE_EVERY_X_MINUTES": 40,
-
- "INGRESS_EGRESS_DISTANCE": nm_to_meter(30),
- "INGRESS_ALT": feet_to_meter(18000),
- "EGRESS_ALT": feet_to_meter(18000),
- "PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)),
- "PATTERN_ALTITUDE": feet_to_meter(5000),
-
- "CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)),
- "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)),
- "CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)),
-
- "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
-}
-
-WWII_DOCTRINE = {
-
- "GENERATORS": {
- "CAS": True,
- "CAP": True,
- "SEAD": False,
- "STRIKE": True,
- "ANTISHIP": True,
- },
-
- "STRIKE_MAX_RANGE": 1500000,
- "SEAD_MAX_RANGE": 1500000,
-
- "CAP_EVERY_X_MINUTES": 20,
- "CAS_EVERY_X_MINUTES": 30,
- "SEAD_EVERY_X_MINUTES": 40,
- "STRIKE_EVERY_X_MINUTES": 40,
-
- "INGRESS_EGRESS_DISTANCE": nm_to_meter(7),
- "INGRESS_ALT": feet_to_meter(8000),
- "EGRESS_ALT": feet_to_meter(8000),
- "PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)),
- "PATTERN_ALTITUDE": feet_to_meter(5000),
-
- "CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)),
- "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)),
- "CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)),
-
- "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
-
-}
+WWII_DOCTRINE = Doctrine(
+ cap=True,
+ cas=True,
+ sead=False,
+ strike=True,
+ antiship=True,
+ strike_max_range=1500000,
+ sead_max_range=1500000,
+ join_distance=nm_to_meter(5),
+ split_distance=nm_to_meter(5),
+ rendezvous_altitude=feet_to_meter(10000),
+ ingress_egress_distance=nm_to_meter(7),
+ ingress_altitude=feet_to_meter(8000),
+ egress_altitude=feet_to_meter(8000),
+ min_patrol_altitude=feet_to_meter(4000),
+ max_patrol_altitude=feet_to_meter(15000),
+ pattern_altitude=feet_to_meter(5000),
+ cap_duration=timedelta(minutes=30),
+ cap_min_track_length=nm_to_meter(8),
+ cap_max_track_length=nm_to_meter(18),
+ cap_min_distance_from_cp=nm_to_meter(0),
+ cap_max_distance_from_cp=nm_to_meter(5),
+ cas_duration=timedelta(minutes=30),
+)
diff --git a/game/data/radar_db.py b/game/data/radar_db.py
index c3c9e25a..4e90d56c 100644
--- a/game/data/radar_db.py
+++ b/game/data/radar_db.py
@@ -1,5 +1,26 @@
+from dcs.ships import (
+ CGN_1144_2_Pyotr_Velikiy,
+ CG_1164_Moskva,
+ CVN_70_Carl_Vinson,
+ CVN_71_Theodore_Roosevelt,
+ CVN_72_Abraham_Lincoln,
+ CVN_73_George_Washington,
+ CVN_74_John_C__Stennis,
+ CV_1143_5_Admiral_Kuznetsov,
+ CV_1143_5_Admiral_Kuznetsov_2017,
+ FFG_11540_Neustrashimy,
+ FFL_1124_4_Grisha,
+ FF_1135M_Rezky,
+ FSG_1241_1MP_Molniya,
+ LHA_1_Tarawa,
+ Oliver_Hazzard_Perry_class,
+ Ticonderoga_class,
+ Type_052B_Destroyer,
+ Type_052C_Destroyer,
+ Type_054A_Frigate,
+ USS_Arleigh_Burke_IIa,
+)
from dcs.vehicles import AirDefence
-from dcs.ships import *
UNITS_WITH_RADAR = [
diff --git a/game/db.py b/game/db.py
index f2b94b54..a1794764 100644
--- a/game/db.py
+++ b/game/db.py
@@ -1,77 +1,173 @@
-import typing
-import enum
from datetime import datetime
+from enum import Enum
+from typing import Dict, List, Optional, Tuple, Type, Union
-from dcs.countries import get_by_id, country_dict
-from dcs.vehicles import *
-from dcs.ships import *
-from dcs.planes import *
-from dcs.helicopters import *
-
-from dcs.task import *
-from dcs.unit import *
-from dcs.unittype import *
-from dcs.unitgroup import *
-
-from game.factions.australia_2005 import Australia_2005
-from game.factions.bluefor_coldwar import BLUEFOR_COLDWAR
-from game.factions.bluefor_coldwar_a4 import BLUEFOR_COLDWAR_A4
-from game.factions.bluefor_coldwar_mods import BLUEFOR_COLDWAR_MODS
-from game.factions.canada_2005 import Canada_2005
-from game.factions.china_2010 import China_2010
-from game.factions.france_1995 import France_1995
-from game.factions.france_2005 import France_2005
-from game.factions.france_modded import France_2005_Modded
-from game.factions.germany_1944_easy import Germany_1944_Easy
-from game.factions.germany_1990 import Germany_1990
-from game.factions.insurgent import Insurgent
-from game.factions.insurgent_modded import Insurgent_modded
-from game.factions.iran_2015 import Iran_2015
-from game.factions.israel_1948 import Israel_1948
-from game.factions.israel_1973 import Israel_1973, Israel_1973_NO_WW2_UNITS, Israel_1982
-from game.factions.israel_2000 import Israel_2000
-from game.factions.italy_1990 import Italy_1990
-from game.factions.italy_1990_mb339 import Italy_1990_MB339
-from game.factions.japan_2005 import Japan_2005
-from game.factions.libya_2011 import Libya_2011
-from game.factions.netherlands_1990 import Netherlands_1990
-from game.factions.north_korea_2000 import NorthKorea_2000
-from game.factions.pakistan_2015 import Pakistan_2015
-from game.factions.private_miltary_companies import PMC_WESTERN_B, PMC_RUSSIAN, PMC_WESTERN_A
-from game.factions.russia_1975 import Russia_1975
-from game.factions.germany_1944 import Germany_1944
-from game.factions.india_2010 import India_2010
-from game.factions.russia_1955 import Russia_1955
-from game.factions.russia_1965 import Russia_1965
-from game.factions.russia_1990 import Russia_1990
-from game.factions.russia_2010 import Russia_2010
-from game.factions.spain_1990 import Spain_1990
-from game.factions.sweden_1990 import Sweden_1990
-from game.factions.syria import Syria_2011, Syria_1967, Syria_1967_WW2_Weapons, Syria_1973, Arab_Armies_1948, Syria_1982
-from game.factions.turkey_2005 import Turkey_2005
-from game.factions.uae_2005 import UAE_2005
-from game.factions.uk_1944 import UK_1944
-from game.factions.uk_1990 import UnitedKingdom_1990
-from game.factions.ukraine_2010 import Ukraine_2010
-from game.factions.us_aggressors import US_Aggressors
-from game.factions.usa_1944 import USA_1944, ALLIES_1944
-from game.factions.usa_1955 import USA_1955
-from game.factions.usa_1960 import USA_1960
-from game.factions.usa_1965 import USA_1965
-from game.factions.usa_1990 import USA_1990
-from game.factions.usa_2005 import USA_2005
-from game.factions.bluefor_modern import BLUEFOR_MODERN
+from dcs import Mission
+from dcs.countries import country_dict
+from dcs.country import Country
+from dcs.helicopters import (
+ AH_1W,
+ AH_64A,
+ AH_64D,
+ HelicopterType,
+ Ka_50,
+ Mi_24V,
+ Mi_28N,
+ Mi_8MT,
+ OH_58D,
+ SA342L,
+ SA342M,
+ SA342Minigun,
+ SA342Mistral,
+ UH_1H,
+ UH_60A,
+ helicopter_map,
+)
+from dcs.mapping import Point
+# mypy can't resolve these if they're wildcard imports for some reason.
+from dcs.planes import (
+ AJS37,
+ AV8BNA,
+ A_10A,
+ A_10C,
+ A_10C_2,
+ A_20G,
+ A_50,
+ An_26B,
+ An_30M,
+ B_17G,
+ B_1B,
+ B_52H,
+ Bf_109K_4,
+ C_101CC,
+ C_130,
+ E_3A,
+ FA_18C_hornet,
+ FW_190A8,
+ FW_190D9,
+ F_117A,
+ F_14B,
+ F_15C,
+ F_15E,
+ F_16A,
+ F_16C_50,
+ F_4E,
+ F_5E_3,
+ F_86F_Sabre,
+ F_A_18C,
+ IL_76MD,
+ IL_78M,
+ JF_17,
+ J_11A,
+ Ju_88A4,
+ KC130,
+ KC_135,
+ KJ_2000,
+ L_39C,
+ L_39ZA,
+ MQ_9_Reaper,
+ M_2000C,
+ MiG_15bis,
+ MiG_19P,
+ MiG_21Bis,
+ MiG_23MLD,
+ MiG_25PD,
+ MiG_27K,
+ MiG_29A,
+ MiG_29G,
+ MiG_29S,
+ MiG_31,
+ Mirage_2000_5,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ PlaneType,
+ RQ_1A_Predator,
+ S_3B_Tanker,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW,
+ Su_17M4,
+ Su_24M,
+ Su_24MR,
+ Su_25,
+ Su_25T,
+ Su_25TM,
+ Su_27,
+ Su_30,
+ Su_33,
+ Su_34,
+ Tornado_GR4,
+ Tornado_IDS,
+ Tu_160,
+ Tu_22M3,
+ Tu_95MS,
+ WingLoong_I,
+ Yak_40,
+ plane_map,
+)
+from dcs.ships import (
+ Armed_speedboat,
+ Bulk_cargo_ship_Yakushev,
+ CVN_71_Theodore_Roosevelt,
+ CVN_72_Abraham_Lincoln,
+ CVN_73_George_Washington,
+ CVN_74_John_C__Stennis,
+ CV_1143_5_Admiral_Kuznetsov,
+ CV_1143_5_Admiral_Kuznetsov_2017,
+ Dry_cargo_ship_Ivanov,
+ LHA_1_Tarawa,
+ Tanker_Elnya_160,
+ ship_map,
+)
+from dcs.task import (
+ AWACS,
+ AntishipStrike,
+ CAP,
+ CAS,
+ CargoTransportation,
+ Embarking,
+ Escort,
+ GroundAttack,
+ Intercept,
+ MainTask,
+ Nothing,
+ PinpointStrike,
+ Reconnaissance,
+ Refueling,
+ SEAD,
+ Task,
+ Transport,
+)
+from dcs.terrain.terrain import Airport
+from dcs.unit import Ship, Unit, Vehicle
+from dcs.unitgroup import ShipGroup, StaticGroup
+from dcs.unittype import FlyingType, ShipType, UnitType, VehicleType
+from dcs.vehicles import (
+ AirDefence,
+ Armor,
+ Artillery,
+ Carriage,
+ Infantry,
+ Unarmed,
+ vehicle_map,
+)
+import pydcs_extensions.frenchpack.frenchpack as frenchpack
+from game.factions.faction import Faction
# PATCH pydcs data with MODS
+from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN
-import pydcs_extensions.frenchpack.frenchpack as frenchpack
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
+from pydcs_extensions.su57.su57 import Su_57
plane_map["A-4E-C"] = A_4E_C
plane_map["MB-339PAN"] = MB_339PAN
plane_map["Rafale_M"] = Rafale_M
plane_map["Rafale_A_S"] = Rafale_A_S
+plane_map["Su-57"] = Su_57
vehicle_map["FieldHL"] = frenchpack._FIELD_HIDE
vehicle_map["HARRIERH"] = frenchpack._FIELD_HIDE_SMALL
@@ -148,6 +244,7 @@ PRICES = {
J_11A: 26,
JF_17: 20,
Su_30: 24,
+ Su_57: 40,
SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14,
@@ -212,6 +309,10 @@ PRICES = {
# Bombers
B_52H: 35,
B_1B: 50,
+ F_117A: 100,
+ Tu_160: 50,
+ Tu_22M3: 40,
+ Tu_95MS: 35,
# special
IL_76MD: 30,
@@ -303,24 +404,27 @@ PRICES = {
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G:24,
Armor.MT_Pz_Kpfw_IV_Ausf_H:16,
Armor.HT_Pz_Kpfw_VI_Tiger_I:24,
- Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II:26,
+ Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II:26,
Armor.TD_Jagdpanther_G1: 18,
Armor.TD_Jagdpanzer_IV: 11,
Armor.Sd_Kfz_184_Elefant: 18,
Armor.APC_Sd_Kfz_251:4,
- Armor.IFV_Sd_Kfz_234_2_Puma:8,
+ Armor.AC_Sd_Kfz_234_2_Puma:8,
Armor.MT_M4_Sherman:12,
Armor.MT_M4A4_Sherman_Firefly:16,
Armor.CT_Cromwell_IV:12,
Armor.M30_Cargo_Carrier:2,
Armor.APC_M2A1:4,
- Armor.ST_Centaur_IV: 10,
+ Armor.CT_Centaur_IV: 10,
Armor.HIT_Churchill_VII: 16,
Armor.LAC_M8_Greyhound: 8,
Armor.TD_M10_GMC: 14,
Armor.StuG_III_Ausf__G: 12,
Artillery.M12_GMC: 10,
Artillery.Sturmpanzer_IV_Brummbär: 10,
+ Armor.Daimler_Armoured_Car: 8,
+ Armor.LT_Mk_VII_Tetrarch: 8,
+ Armor.M4_Tractor: 2,
# ship
CV_1143_5_Admiral_Kuznetsov: 100,
@@ -399,12 +503,16 @@ PRICES = {
AirDefence.AAA_Flak_38: 6,
AirDefence.AAA_8_8cm_Flak_36: 8,
AirDefence.AAA_8_8cm_Flak_37: 9,
- AirDefence.AAA_Flak_Vierling_38:6,
+ AirDefence.AAA_Flak_Vierling_38: 5,
AirDefence.AAA_Kdo_G_40: 8,
AirDefence.Flak_Searchlight_37: 4,
AirDefence.Maschinensatz_33: 10,
AirDefence.AAA_8_8cm_Flak_41: 10,
+ AirDefence.EWR_FuMG_401_Freya_LZ: 25,
AirDefence.AAA_Bofors_40mm: 8,
+ AirDefence.AAA_M1_37mm: 7,
+ AirDefence.AAA_M45_Quadmount: 4,
+ AirDefence.AA_gun_QF_3_7: 10,
# FRENCH PACK MOD
frenchpack.AMX_10RCR: 10,
@@ -459,6 +567,7 @@ UNIT_BY_TASK = {
F_5E_3,
Su_27,
Su_33,
+ Su_57,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
@@ -491,49 +600,53 @@ UNIT_BY_TASK = {
SA342Mistral
],
CAS: [
- F_15E,
- F_86F_Sabre,
- MiG_15bis,
- L_39ZA,
- AV8BNA,
+ AH_1W,
+ AH_64A,
+ AH_64D,
AJS37,
+ AV8BNA,
A_10A,
A_10C,
A_10C_2,
- Su_17M4,
- Su_25,
- Su_25T,
- Su_34,
- Ka_50,
- SA342M,
- SA342L,
- SA342Minigun,
- Su_24M,
- Su_24MR,
- AH_64A,
- AH_64D,
- OH_58D,
- B_52H,
- B_1B,
- Tornado_IDS,
- Tornado_GR4,
- UH_1H,
- Mi_8MT,
- Mi_28N,
- Mi_24V,
- MiG_27K,
A_20G,
+ B_17G,
+ B_1B,
+ B_52H,
+ F_117A,
+ F_15E,
+ F_86F_Sabre,
+ Ju_88A4,
+ Ka_50,
+ L_39ZA,
+ MB_339PAN,
+ MQ_9_Reaper,
+ MiG_15bis,
+ MiG_27K,
+ Mi_24V,
+ Mi_28N,
+ Mi_8MT,
+ OH_58D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
- Ju_88A4,
- B_17G,
- MB_339PAN,
- Rafale_A_S,
- WingLoong_I,
- MQ_9_Reaper,
RQ_1A_Predator,
- AH_1W
+ Rafale_A_S,
+ SA342L,
+ SA342M,
+ SA342Minigun,
+ Su_17M4,
+ Su_24M,
+ Su_24MR,
+ Su_25,
+ Su_25T,
+ Su_34,
+ Tornado_GR4,
+ Tornado_IDS,
+ Tu_160,
+ Tu_22M3,
+ Tu_95MS,
+ UH_1H,
+ WingLoong_I,
],
Transport: [
IL_76MD,
@@ -639,13 +752,13 @@ UNIT_BY_TASK = {
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
- Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
+ Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
- Armor.IFV_Sd_Kfz_234_2_Puma,
- Armor.IFV_Sd_Kfz_234_2_Puma,
+ Armor.AC_Sd_Kfz_234_2_Puma,
+ Armor.AC_Sd_Kfz_234_2_Puma,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.CT_Cromwell_IV,
@@ -658,12 +771,12 @@ UNIT_BY_TASK = {
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
- Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
+ Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.TD_Jagdpanther_G1,
Armor.TD_Jagdpanzer_IV,
Armor.Sd_Kfz_184_Elefant,
Armor.APC_Sd_Kfz_251,
- Armor.IFV_Sd_Kfz_234_2_Puma,
+ Armor.AC_Sd_Kfz_234_2_Puma,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.CT_Cromwell_IV,
@@ -672,8 +785,8 @@ UNIT_BY_TASK = {
Armor.M30_Cargo_Carrier,
Armor.APC_M2A1,
Armor.APC_M2A1,
- Armor.ST_Centaur_IV,
- Armor.ST_Centaur_IV,
+ Armor.CT_Centaur_IV,
+ Armor.CT_Centaur_IV,
Armor.HIT_Churchill_VII,
Armor.LAC_M8_Greyhound,
Armor.LAC_M8_Greyhound,
@@ -793,13 +906,13 @@ SAM_CONVERT = {
"""
Units that will always be spawned in the air
"""
-TAKEOFF_BAN = [
+TAKEOFF_BAN: List[Type[FlyingType]] = [
]
"""
Units that will be always spawned in the air if launched from the carrier
"""
-CARRIER_TAKEOFF_BAN = [
+CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
]
@@ -807,98 +920,7 @@ CARRIER_TAKEOFF_BAN = [
Units separated by country.
country : DCS Country name
"""
-FACTIONS = {
-
- "Bluefor Modern": BLUEFOR_MODERN,
- "Bluefor Cold War 1970s": BLUEFOR_COLDWAR,
- "Bluefor Cold War (with A-4)": BLUEFOR_COLDWAR_A4,
- "Bluefor Cold War (with A-4 and MB339)": BLUEFOR_COLDWAR_MODS,
-
- "USA 1955 (WW2 Pack)": USA_1955,
- "USA 1960": USA_1960,
- "USA 1965": USA_1965,
- "USA 1990": USA_1990,
- "USA 2005": USA_2005,
- "USA Aggressors 2005": US_Aggressors,
-
- "Russia 1955": Russia_1955,
- "Russia 1965": Russia_1965,
- "Russia 1975": Russia_1975,
- "Russia 1990": Russia_1990,
- "Russia 2010": Russia_2010,
-
- "France 1995": France_1995,
- "France 2005": France_2005,
- "France 2005 (Modded)": France_2005_Modded,
-
- "Germany 1990": Germany_1990,
-
- "Netherlands 1990": Netherlands_1990,
-
- "United Kingdom 1990": UnitedKingdom_1990,
-
- "Spain 1990": Spain_1990,
-
- "Italy 1990": Italy_1990,
- "Italy 1990 (With MB339)": Italy_1990_MB339,
-
- "Israel 2000": Israel_2000,
- "Israel 1982": Israel_1982,
- "Israel 1973 (WW2 Pack)": Israel_1973,
- "Israel 1973": Israel_1973_NO_WW2_UNITS,
- "Israel 1948": Israel_1948,
-
- "Arab Armies 1982": Syria_1982,
- "Arab Armies 1973": Syria_1973,
- "Arab Armies 1967 (WW2 Pack)": Syria_1967_WW2_Weapons,
- "Arab Armies 1967": Syria_1967,
- "Arab League 1948": Arab_Armies_1948,
-
- "China 2010": China_2010,
-
- "Sweden 1990": Sweden_1990,
-
- "Australia 2005": Australia_2005,
-
- "Canada 2005": Canada_2005,
-
- "Japan 2005": Japan_2005,
-
- "Turkey 2005": Turkey_2005,
-
- "United Arab Emirates 2005": UAE_2005,
-
- "Ukraine 2010": Ukraine_2010,
-
- "India 2010": India_2010,
-
- "Libya 2011": Libya_2011,
-
- "Syria 2011": Syria_2011,
-
-
- "Pakistan 2015": Pakistan_2015,
-
- "Iran 2015": Iran_2015,
-
- "North Korea 2000": NorthKorea_2000,
-
- "Insurgent": Insurgent,
- "Insurgent (Modded)": Insurgent_modded,
-
- "PMC (American)": PMC_WESTERN_A,
- "PMC (American) - MB339": PMC_WESTERN_B,
- "PMC (Russian)": PMC_RUSSIAN,
-
- "Allies 1944 (WW2 Pack)": USA_1944,
- "USA 1944 (WW2 Pack)": ALLIES_1944,
- "UK 1944 (WW2 Pack)": UK_1944,
-
- "Germany 1944 (WW2 Pack)": Germany_1944,
- "Germany 1944 Easy (WW2 Pack)": Germany_1944_Easy,
-
-}
-
+FACTIONS = FactionLoader()
CARRIER_TYPE_BY_PLANE = {
FA_18C_hornet: CVN_74_John_C__Stennis,
@@ -934,11 +956,16 @@ COMMON_OVERRIDE = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
- GroundAttack: "STRIKE"
+ GroundAttack: "STRIKE",
+ Escort: "CAP",
}
-PLANE_PAYLOAD_OVERRIDES = {
+PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
+ B_1B: COMMON_OVERRIDE,
+ B_52H: COMMON_OVERRIDE,
+ F_117A: COMMON_OVERRIDE,
+ F_15E: COMMON_OVERRIDE,
FA_18C_hornet: {
CAP: "CAP HEAVY",
Intercept: "CAP HEAVY",
@@ -946,7 +973,8 @@ PLANE_PAYLOAD_OVERRIDES = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
- GroundAttack: "STRIKE"
+ GroundAttack: "STRIKE",
+ Escort: "CAP HEAVY",
},
F_A_18C: {
CAP: "CAP HEAVY",
@@ -955,8 +983,14 @@ PLANE_PAYLOAD_OVERRIDES = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
- GroundAttack: "STRIKE"
+ GroundAttack: "STRIKE",
+ Escort: "CAP HEAVY",
},
+ Tu_160: {
+ PinpointStrike: "Kh-65*12",
+ },
+ Tu_22M3: COMMON_OVERRIDE,
+ Tu_95MS: COMMON_OVERRIDE,
A_10A: COMMON_OVERRIDE,
A_10C: COMMON_OVERRIDE,
A_10C_2: COMMON_OVERRIDE,
@@ -965,7 +999,6 @@ PLANE_PAYLOAD_OVERRIDES = {
F_5E_3: COMMON_OVERRIDE,
F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE,
- F_15E: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE,
JF_17: COMMON_OVERRIDE,
M_2000C: COMMON_OVERRIDE,
@@ -983,6 +1016,7 @@ PLANE_PAYLOAD_OVERRIDES = {
Su_24M:COMMON_OVERRIDE,
Su_30: COMMON_OVERRIDE,
Su_34: COMMON_OVERRIDE,
+ Su_57: COMMON_OVERRIDE,
MiG_23MLD: COMMON_OVERRIDE,
MiG_27K: COMMON_OVERRIDE,
Tornado_GR4: COMMON_OVERRIDE,
@@ -1099,6 +1133,7 @@ CARRIER_CAPABLE = [
AV8BNA,
Su_33,
A_4E_C,
+ Rafale_M,
UH_1H,
Mi_8MT,
@@ -1134,17 +1169,17 @@ LHA_CAPABLE = [
---------- END OF CONFIGURATION SECTION
"""
-UnitsDict = typing.Dict[UnitType, int]
-PlaneDict = typing.Dict[FlyingType, int]
-HeliDict = typing.Dict[HelicopterType, int]
-ArmorDict = typing.Dict[VehicleType, int]
-ShipDict = typing.Dict[ShipType, int]
-AirDefenseDict = typing.Dict[AirDefence, int]
+UnitsDict = Dict[UnitType, int]
+PlaneDict = Dict[FlyingType, int]
+HeliDict = Dict[HelicopterType, int]
+ArmorDict = Dict[VehicleType, int]
+ShipDict = Dict[ShipType, int]
+AirDefenseDict = Dict[AirDefence, int]
-AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]]
-TaskForceDict = typing.Dict[typing.Type[Task], AssignedUnitsDict]
+AssignedUnitsDict = Dict[Type[UnitType], Tuple[int, int]]
+TaskForceDict = Dict[Type[MainTask], AssignedUnitsDict]
-StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]]
+StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
def upgrade_to_supercarrier(unit, name: str):
@@ -1162,7 +1197,8 @@ def upgrade_to_supercarrier(unit, name: str):
else:
return unit
-def unit_task(unit: UnitType) -> Task:
+
+def unit_task(unit: UnitType) -> Optional[Task]:
for task, units in UNIT_BY_TASK.items():
if unit in units:
return task
@@ -1173,10 +1209,12 @@ def unit_task(unit: UnitType) -> Task:
print(unit.name + " cause issue")
return None
-def find_unittype(for_task: Task, country_name: str) -> typing.List[UnitType]:
- return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name]["units"]]
-def find_infantry(country_name: str) -> typing.List[UnitType]:
+def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
+ return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units]
+
+
+def find_infantry(country_name: str) -> List[UnitType]:
inf = [
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Soldier_RPG,
@@ -1191,15 +1229,18 @@ def find_infantry(country_name: str) -> typing.List[UnitType]:
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
]
- return [x for x in inf if x in FACTIONS[country_name]["units"]]
+ return [x for x in inf if x in FACTIONS[country_name].infantry_units]
+
def unit_type_name(unit_type) -> str:
return unit_type.id and unit_type.id or unit_type.name
+
def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
-def unit_type_from_name(name: str) -> UnitType:
+
+def unit_type_from_name(name: str) -> Optional[UnitType]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:
@@ -1232,7 +1273,7 @@ def task_name(task) -> str:
return task.name
-def choose_units(for_task: Task, factor: float, count: int, country: str) -> typing.Collection[UnitType]:
+def choose_units(for_task: Task, factor: float, count: int, country: str) -> List[UnitType]:
suitable_unittypes = find_unittype(for_task, country)
suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()]
suitable_unittypes.sort(key=lambda x: PRICES[x])
@@ -1258,7 +1299,7 @@ def unitdict_merge(a: UnitsDict, b: UnitsDict) -> UnitsDict:
def unitdict_split(unit_dict: UnitsDict, count: int):
- buffer_dict = {}
+ buffer_dict: Dict[UnitType, int] = {}
for unit_type, unit_count in unit_dict.items():
for _ in range(unit_count):
unitdict_append(buffer_dict, unit_type, 1)
@@ -1281,7 +1322,7 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict
return {}
-def assigned_units_split(fd: AssignedUnitsDict) -> typing.Tuple[PlaneDict, PlaneDict]:
+def assigned_units_split(fd: AssignedUnitsDict) -> Tuple[PlaneDict, PlaneDict]:
return {k: v1 for k, (v1, v2) in fd.items()}, {k: v2 for k, (v1, v2) in fd.items()},
@@ -1290,7 +1331,7 @@ def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict:
def assignedunits_split_to_count(dict: AssignedUnitsDict, count: int):
- buffer_dict = {}
+ buffer_dict: Dict[Type[UnitType], Tuple[int, int]] = {}
for unit_type, (unit_count, client_count) in dict.items():
for _ in range(unit_count):
new_count, new_client_count = buffer_dict.get(unit_type, (0, 0))
@@ -1339,6 +1380,7 @@ class DefaultLiveries:
class Default(Enum):
af_standard = ""
+
OH_58D.Liveries = DefaultLiveries
F_16C_50.Liveries = DefaultLiveries
P_51D_30_NA.Liveries = DefaultLiveries
diff --git a/userdata/debriefing.py b/game/debriefing.py
similarity index 91%
rename from userdata/debriefing.py
rename to game/debriefing.py
index 5886270e..6bf15569 100644
--- a/userdata/debriefing.py
+++ b/game/debriefing.py
@@ -24,7 +24,7 @@ class DebriefingDeadUnitInfo:
class Debriefing:
def __init__(self, state_data, game):
- self.base_capture_events = state_data["base_capture_events"]
+ self.state_data = state_data
self.killed_aircrafts = state_data["killed_aircrafts"]
self.killed_ground_units = state_data["killed_ground_units"]
self.weapons_fired = state_data["weapons_fired"]
@@ -87,8 +87,8 @@ class Debriefing:
for i, ground_object in enumerate(cp.ground_objects):
logging.info(unit)
- logging.info(ground_object.string_identifier)
- if ground_object.matches_string_identifier(unit):
+ logging.info(ground_object.group_name)
+ if ground_object.is_same_group(unit):
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
self.dead_buildings.append(unit)
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
@@ -162,6 +162,18 @@ class Debriefing:
logging.info(self.player_dead_buildings_dict)
logging.info(self.enemy_dead_buildings_dict)
+ @property
+ def base_capture_events(self):
+ """Keeps only the last instance of a base capture event for each base ID"""
+ reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
+ last_base_cap_indexes = []
+ for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
+ if base in [x[1] for x in last_base_cap_indexes]:
+ continue
+ else:
+ last_base_cap_indexes.append((idx, base))
+ return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
+
class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check
diff --git a/game/event/event.py b/game/event/event.py
index 8146deb3..8cc4aea7 100644
--- a/game/event/event.py
+++ b/game/event/event.py
@@ -1,24 +1,22 @@
-import typing
+from __future__ import annotations
+
import logging
+import math
+from typing import Dict, List, Optional, Type, TYPE_CHECKING
-from dcs.action import Coalition
-from dcs.unittype import UnitType
-from dcs.task import *
-from dcs.vehicles import AirDefence
+from dcs.mapping import Point
+from dcs.task import Task
from dcs.unittype import UnitType
-from game import *
+from game import db, persistency
+from game.debriefing import Debriefing
from game.infos.information import Information
-from theater import *
-from gen.environmentgen import EnvironmentSettings
-from gen.conflictgen import Conflict
-from game.db import assigned_units_from, unitdict_from
-from theater.start_generator import generate_airbase_defense_group
+from game.operation.operation import Operation
+from gen.ground_forces.combat_stance import CombatStance
+from theater import ControlPoint
-from userdata.debriefing import Debriefing
-from userdata import persistency
-
-import game.db as db
+if TYPE_CHECKING:
+ from ..game import Game
DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000
@@ -28,6 +26,7 @@ MINOR_DEFEAT_INFLUENCE = 0.1
DEFEAT_INFLUENCE = 0.3
STRONG_DEFEAT_INFLUENCE = 0.5
+
class Event:
silent = False
informational = False
@@ -37,17 +36,15 @@ class Event:
game = None # type: Game
location = None # type: Point
from_cp = None # type: ControlPoint
- departure_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
operation = None # type: Operation
difficulty = 1 # type: int
- environment_settings = None # type: EnvironmentSettings
BONUS_BASE = 5
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
self.game = game
- self.departure_cp = None
+ self.departure_cp: Optional[ControlPoint] = None
self.from_cp = from_cp
self.to_cp = target_cp
self.location = location
@@ -59,14 +56,14 @@ class Event:
return self.attacker_name == self.game.player_name
@property
- def enemy_cp(self) -> ControlPoint:
+ def enemy_cp(self) -> Optional[ControlPoint]:
if self.attacker_name == self.game.player_name:
return self.to_cp
else:
return self.departure_cp
@property
- def tasks(self) -> typing.Collection[typing.Type[Task]]:
+ def tasks(self) -> List[Type[Task]]:
return []
@property
@@ -91,18 +88,6 @@ class Event:
def is_successfull(self, debriefing: Debriefing) -> bool:
return self.operation.is_successfull(debriefing)
- def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict):
- if self.is_player_attacking:
- self.departure_cp = cp
- else:
- self.to_cp = cp
-
- def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict):
- if self.is_player_attacking:
- self.departure_cp = cp
- else:
- self.to_cp = cp
-
def generate(self):
self.operation.is_awacs_enabled = self.is_awacs_enabled
self.operation.ca_slots = self.ca_slots
@@ -159,9 +144,13 @@ class Event:
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.is_dead:
continue
-
- if ground_object.matches_string_identifier(destroyed_ground_unit_name):
- logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier))
+
+ if (
+ (ground_object.group_name == destroyed_ground_unit_name)
+ or
+ (ground_object.is_same_group(destroyed_ground_unit_name))
+ ):
+ logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
cp.ground_objects[i].is_dead = True
info = Information("Building destroyed",
@@ -176,7 +165,7 @@ class Event:
"",
self.game.turn)
for i, ground_object in enumerate(cp.ground_objects):
- if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
+ if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
for g in ground_object.groups:
if not hasattr(g, "units_losts"):
g.units_losts = []
@@ -209,29 +198,19 @@ class Event:
if cp.id == id:
if cp.captured and new_owner_coalition != coalition:
- cp.captured = False
+ for_player = False
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
self.game.informations.append(info)
- pname = self.game.enemy_name
captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition:
- cp.captured = True
+ for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
self.game.informations.append(info)
- pname = self.game.player_name
captured_cps.append(cp)
else:
continue
- cp.base.aircraft = {}
- cp.base.armor = {}
-
- airbase_def_id = 0
- for g in cp.ground_objects:
- g.groups = []
- if g.airbase_group and pname != "":
- generate_airbase_defense_group(airbase_def_id, g, pname, self.game, cp)
- airbase_def_id = airbase_def_id + 1
+ cp.capture(self.game, for_player)
for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name)
@@ -253,7 +232,7 @@ class Event:
for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
- delta = 0
+ delta = 0.0
player_won = True
ally_casualties = killed_unit_count_by_cp[cp.id]
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
@@ -376,7 +355,6 @@ class Event:
class UnitsDeliveryEvent(Event):
informational = True
- units = None # type: typing.Dict[UnitType, int]
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
super(UnitsDeliveryEvent, self).__init__(game=game,
@@ -386,12 +364,12 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name,
defender_name=defender_name)
- self.units = {}
+ self.units: Dict[UnitType, int] = {}
def __str__(self):
return "Pending delivery to {}".format(self.to_cp)
- def deliver(self, units: typing.Dict[UnitType, int]):
+ def deliver(self, units: Dict[UnitType, int]):
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py
index e548440f..0046526d 100644
--- a/game/event/frontlineattack.py
+++ b/game/event/frontlineattack.py
@@ -1,12 +1,17 @@
-from game.event import *
+from typing import List, Type
+
+from dcs.task import CAP, CAS, Task
+
+from game import db
from game.operation.frontlineattack import FrontlineAttackOperation
-from userdata.debriefing import Debriefing
+from .event import Event
+from ..debriefing import Debriefing
class FrontlineAttackEvent(Event):
@property
- def tasks(self) -> typing.Collection[typing.Type[Task]]:
+ def tasks(self) -> List[Type[Task]]:
if self.is_player_attacking:
return [CAS, CAP]
else:
@@ -34,6 +39,7 @@ class FrontlineAttackEvent(Event):
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, flights: db.TaskForceDict):
+ assert self.departure_cp is not None
op = FrontlineAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
@@ -41,13 +47,3 @@ class FrontlineAttackEvent(Event):
departure_cp=self.departure_cp,
to_cp=self.to_cp)
self.operation = op
-
- def player_defending(self, flights: db.TaskForceDict):
- op = FrontlineAttackOperation(game=self.game,
- attacker_name=self.attacker_name,
- defender_name=self.defender_name,
- from_cp=self.from_cp,
- departure_cp=self.departure_cp,
- to_cp=self.to_cp)
- self.operation = op
-
diff --git a/game/factions/australia_2005.py b/game/factions/australia_2005.py
deleted file mode 100644
index df4972e6..00000000
--- a/game/factions/australia_2005.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Australia_2005 = {
- "country": "Australia",
- "side": "blue",
- "units": [
- FA_18C_hornet,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- Armor.MBT_M1A2_Abrams,
- Armor.MBT_Leopard_1A3,
- Armor.APC_M113,
- Armor.IFV_LAV_25,
- Armor.IFV_MCV_80,
-
- UH_1H,
- AH_1W, # Standing as EC Tiger
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.Rapier_FSA_Launcher,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.Rapier_FSA_Launcher,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "lhanames": [
- "HMAS Canberra",
- "HMAS Adelaide"
- ], "boat":[
- "ArleighBurkeGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/bluefor_coldwar.py b/game/factions/bluefor_coldwar.py
deleted file mode 100644
index 5db15d73..00000000
--- a/game/factions/bluefor_coldwar.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-BLUEFOR_COLDWAR = {
- "country": "Combined Joint Task Forces Blue",
- "side": "blue",
- "units": [
-
- F_14B,
- F_4E,
- F_5E_3,
- A_10A,
- AJS37,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- SA342M,
- SA342L,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Chaparral_M48,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.AAA_Vulcan_M163,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat": [
- ], "has_jtac": True
-}
diff --git a/game/factions/bluefor_coldwar_a4.py b/game/factions/bluefor_coldwar_a4.py
deleted file mode 100644
index 74983134..00000000
--- a/game/factions/bluefor_coldwar_a4.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.a4ec.a4ec import A_4E_C
-
-BLUEFOR_COLDWAR_A4 = {
- "country": "Combined Joint Task Forces Blue",
- "side": "blue",
- "units": [
-
- F_14B,
- F_4E,
- F_5E_3,
- A_10A,
- AJS37,
- A_4E_C,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- SA342M,
- SA342L,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Chaparral_M48,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.AAA_Vulcan_M163,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat": [
- ], "requirements": {
- "Community A-4E": "https://heclak.github.io/community-a4e-c/",
- }, "has_jtac": True
-}
diff --git a/game/factions/bluefor_coldwar_mods.py b/game/factions/bluefor_coldwar_mods.py
deleted file mode 100644
index aece4e46..00000000
--- a/game/factions/bluefor_coldwar_mods.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.a4ec.a4ec import A_4E_C
-from pydcs_extensions.mb339.mb339 import MB_339PAN
-
-BLUEFOR_COLDWAR_MODS = {
- "country": "USA",
- "side": "blue",
- "units": [
-
- F_14B,
- F_4E,
- F_5E_3,
- A_10A,
- AJS37,
- A_4E_C,
- MB_339PAN,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- SA342M,
- SA342L,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Chaparral_M48,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.AAA_Vulcan_M163,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat": [
- ], "requirements": {
- "MB-339A": "http://www.freccetricolorivirtuali.net/",
- "Community A-4E": "https://heclak.github.io/community-a4e-c/",
- }, "has_jtac": True
-}
diff --git a/game/factions/bluefor_modern.py b/game/factions/bluefor_modern.py
deleted file mode 100644
index 8db0ad89..00000000
--- a/game/factions/bluefor_modern.py
+++ /dev/null
@@ -1,83 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-BLUEFOR_MODERN = {
- "country": "Combined Joint Task Forces Blue",
- "side": "blue",
- "units": [
-
- F_15C,
- F_14B,
- FA_18C_hornet,
- F_16C_50,
- JF_17,
- M_2000C,
- F_5E_3,
- Su_27,
-
- Su_25T,
- A_10A,
- A_10C,
- A_10C_2,
- AV8BNA,
- AJS37,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_64D,
- Ka_50,
- SA342M,
- SA342L,
-
- Armor.MBT_M1A2_Abrams,
- Armor.MBT_Leopard_2,
- Armor.ATGM_M1134_Stryker,
- Armor.IFV_M2A2_Bradley,
- Armor.IFV_Marder,
- Armor.APC_M1043_HMMWV_Armament,
-
- Artillery.MLRS_M270,
- Artillery.SPH_M109_Paladin,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Patriot_EPP_III,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.SAM_Avenger_M1097,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/canada_2005.py b/game/factions/canada_2005.py
deleted file mode 100644
index ea4497ca..00000000
--- a/game/factions/canada_2005.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Canada_2005 = {
- "country": "Canada",
- "side": "blue",
- "units": [
- FA_18C_hornet,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- Armor.MBT_Leopard_1A3,
- Armor.MBT_Leopard_2,
- Armor.IFV_LAV_25,
- Armor.APC_M113,
- Armor.IFV_MCV_80,
-
- UH_1H,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.SAM_Avenger_M1097,
- ], "destroyer": [
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "boat":[
- "ArleighBurkeGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/china_2010.py b/game/factions/china_2010.py
deleted file mode 100644
index 0d98d1b9..00000000
--- a/game/factions/china_2010.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-China_2010 = {
- "country": "China",
- "side": "red",
- "units": [
-
- MiG_21Bis, # Standing as J-7
- Su_30,
- Su_33,
- J_11A,
- JF_17,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- KJ_2000,
-
- Mi_8MT,
- Mi_28N,
-
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9+
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.HQ_7_Self_Propelled_LN,
-
- Armor.ZTZ_96B,
- Armor.MBT_T_55,
- Armor.ZBD_04A,
- Armor.IFV_BMP_1,
- Artillery.MLRS_9A52_Smerch,
- Artillery.SPH_2S9_Nona,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160
- ],
- "shorad":[
- AirDefence.SPAAA_ZSU_23_4_Shilka,
- AirDefence.Rapier_FSA_Launcher, # Standing as PL-9C Shorad
- AirDefence.HQ_7_Self_Propelled_LN
- ], "aircraft_carrier": [
- CV_1143_5_Admiral_Kuznetsov,
- ], "destroyer": [
- Type_052B_Destroyer,
- Type_052C_Destroyer
- ], "cruiser": [
- Type_054A_Frigate,
- ], "helicopter_carrier": [
- Type_071_Amphibious_Transport_Dock,
- ], "lhanames": [
- "Kunlun Shan",
- "Jinggang Shan",
- "Changbai Shan",
- "Yimeng Shan",
- "Longhu Shan",
- "Wuzhi Shan",
- "Wudang Shan"
- ], "carrier_names": [
- "001 Liaoning",
- "002 Shandong",
- ], "boat":[
- "Type54GroupGenerator"
- ],
- "has_jtac": True,
- "jtac_unit": WingLoong_I
-}
\ No newline at end of file
diff --git a/game/factions/faction.py b/game/factions/faction.py
new file mode 100644
index 00000000..5a056bf1
--- /dev/null
+++ b/game/factions/faction.py
@@ -0,0 +1,258 @@
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass, field
+from typing import Optional, Dict, Type, List, Any, cast
+
+import dcs
+from dcs.countries import country_dict
+from dcs.planes import plane_map
+from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
+from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
+
+from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
+from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
+from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
+
+
+@dataclass
+class Faction:
+
+ # Country used by this faction
+ country: str = field(default="")
+
+ # Nice name of the faction
+ name: str = field(default="")
+
+ # List of faction file authors
+ authors: str = field(default="")
+
+ # A description of the faction
+ description: str = field(default="")
+
+ # Available aircraft
+ aircrafts: List[UnitType] = field(default_factory=list)
+
+ # Available awacs aircraft
+ awacs: List[UnitType] = field(default_factory=list)
+
+ # Available tanker aircraft
+ tankers: List[UnitType] = field(default_factory=list)
+
+ # Available frontline units
+ frontline_units: List[VehicleType] = field(default_factory=list)
+
+ # Available artillery units
+ artillery_units: List[VehicleType] = field(default_factory=list)
+
+ # Infantry units used
+ infantry_units: List[VehicleType] = field(default_factory=list)
+
+ # Logistics units used
+ logistics_units: List[VehicleType] = field(default_factory=list)
+
+ # List of units that can be deployed as SHORAD
+ shorads: List[str] = field(default_factory=list)
+
+ # Possible SAMS site generators for this faction
+ sams: List[str] = field(default_factory=list)
+
+ # Possible EWR generators for this faction.
+ ewrs: List[str] = field(default_factory=list)
+
+ # Possible Missile site generators for this faction
+ missiles: List[str] = field(default_factory=list)
+
+ # Required mods or asset packs
+ requirements: Dict[str, str] = field(default_factory=dict)
+
+ # possible aircraft carrier units
+ aircraft_carrier: List[UnitType] = field(default_factory=list)
+
+ # possible helicopter carrier units
+ helicopter_carrier: List[UnitType] = field(default_factory=list)
+
+ # Possible carrier names
+ carrier_names: List[str] = field(default_factory=list)
+
+ # Possible helicopter carrier names
+ helicopter_carrier_names: List[str] = field(default_factory=list)
+
+ # Navy group generators
+ navy_generators: List[str] = field(default_factory=list)
+
+ # Available destroyers
+ destroyers: List[str] = field(default_factory=list)
+
+ # Available cruisers
+ cruisers: List[str] = field(default_factory=list)
+
+ # How many navy group should we try to generate per CP on startup for this faction
+ navy_group_count: int = field(default=1)
+
+ # How many missiles group should we try to generate per CP on startup for this faction
+ missiles_group_count: int = field(default=1)
+
+ # Whether this faction has JTAC access
+ has_jtac: bool = field(default=False)
+
+ # Unit to use as JTAC for this faction
+ jtac_unit: Optional[FlyingType] = field(default=None)
+
+ # doctrine
+ doctrine: Doctrine = field(default=MODERN_DOCTRINE)
+
+ # List of available buildings for this faction
+ building_set: List[str] = field(default_factory=list)
+
+ @classmethod
+ def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
+
+ faction = Faction()
+
+ faction.country = json.get("country", "/")
+ if faction.country not in [c.name for c in country_dict.values()]:
+ raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
+
+ faction.name = json.get("name", "")
+ if not faction.name:
+ raise AssertionError("Faction has no valid name")
+
+ faction.authors = json.get("authors", "")
+ faction.description = json.get("description", "")
+
+ faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
+ faction.awacs = load_all_aircraft(json.get("awacs", []))
+ faction.tankers = load_all_aircraft(json.get("tankers", []))
+
+ faction.frontline_units = load_all_vehicles(
+ json.get("frontline_units", []))
+ faction.artillery_units = load_all_vehicles(
+ json.get("artillery_units", []))
+ faction.infantry_units = load_all_vehicles(
+ json.get("infantry_units", []))
+ faction.logistics_units = load_all_vehicles(
+ json.get("logistics_units", []))
+
+ faction.sams = json.get("sams", [])
+ faction.ewrs = json.get("ewrs", [])
+ faction.shorads = json.get("shorads", [])
+ faction.missiles = json.get("missiles", [])
+ faction.requirements = json.get("requirements", {})
+
+ faction.carrier_names = json.get("carrier_names", [])
+ faction.helicopter_carrier_names = json.get(
+ "helicopter_carrier_names", [])
+ faction.navy_generators = json.get("navy_generators", [])
+ faction.aircraft_carrier = load_all_ships(
+ json.get("aircraft_carrier", []))
+ faction.helicopter_carrier = load_all_ships(
+ json.get("helicopter_carrier", []))
+ faction.destroyers = load_all_ships(json.get("destroyers", []))
+ faction.cruisers = load_all_ships(json.get("cruisers", []))
+ faction.has_jtac = json.get("has_jtac", False)
+ jtac_name = json.get("jtac_unit", None)
+ if jtac_name is not None:
+ faction.jtac_unit = load_aircraft(jtac_name)
+ else:
+ faction.jtac_unit = None
+ faction.navy_group_count = int(json.get("navy_group_count", 1))
+ faction.missiles_group_count = int(json.get("missiles_group_count", 0))
+
+ # Load doctrine
+ doctrine = json.get("doctrine", "modern")
+ if doctrine == "modern":
+ faction.doctrine = MODERN_DOCTRINE
+ elif doctrine == "coldwar":
+ faction.doctrine = COLDWAR_DOCTRINE
+ elif doctrine == "ww2":
+ faction.doctrine = WWII_DOCTRINE
+ else:
+ faction.doctrine = MODERN_DOCTRINE
+
+ # Load the building set
+ building_set = json.get("building_set", "default")
+ if building_set == "default":
+ faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
+ elif building_set == "ww2free":
+ faction.building_set = WW2_FREE
+ elif building_set == "ww2ally":
+ faction.building_set = WW2_ALLIES_BUILDINGS
+ elif building_set == "ww2germany":
+ faction.building_set = WW2_GERMANY_BUILDINGS
+ else:
+ faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
+
+ return faction
+
+ @property
+ def units(self) -> List[UnitType]:
+ return (self.infantry_units + self.aircrafts + self.awacs +
+ self.artillery_units + self.frontline_units +
+ self.tankers + self.logistics_units)
+
+
+def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
+ """
+ Find unit by name
+ :param unit: Unit name as string
+ :param class_repository: Repository of classes (Either a module, a class, or a list of classes)
+ :return: The unit as a PyDCS type
+ """
+ if unit is None:
+ return None
+ elif unit in plane_map.keys():
+ return plane_map[unit]
+ else:
+ for mother_class in class_repository:
+ if getattr(mother_class, unit, None) is not None:
+ return getattr(mother_class, unit)
+ if type(mother_class) is list:
+ for m in mother_class:
+ if m.__name__ == unit:
+ return m
+ logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
+ return None
+
+
+def load_aircraft(name: str) -> Optional[FlyingType]:
+ return cast(Optional[FlyingType], unit_loader(
+ name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
+ ))
+
+
+def load_all_aircraft(data) -> List[FlyingType]:
+ items = []
+ for name in data:
+ item = load_aircraft(name)
+ if item is not None:
+ items.append(item)
+ return items
+
+
+def load_vehicle(name: str) -> Optional[VehicleType]:
+ return cast(Optional[FlyingType], unit_loader(
+ name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
+ ))
+
+
+def load_all_vehicles(data) -> List[VehicleType]:
+ items = []
+ for name in data:
+ item = load_vehicle(name)
+ if item is not None:
+ items.append(item)
+ return items
+
+
+def load_ship(name: str) -> Optional[ShipType]:
+ return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
+
+
+def load_all_ships(data) -> List[ShipType]:
+ items = []
+ for name in data:
+ item = load_ship(name)
+ if item is not None:
+ items.append(item)
+ return items
diff --git a/game/factions/faction_loader.py b/game/factions/faction_loader.py
new file mode 100644
index 00000000..fa4cf04f
--- /dev/null
+++ b/game/factions/faction_loader.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+import json
+import logging
+from pathlib import Path
+from typing import Dict, Iterator, Optional, Type
+
+from game.factions.faction import Faction
+
+FACTION_DIRECTORY = Path("./resources/factions/")
+
+
+class FactionLoader:
+ def __init__(self) -> None:
+ self._factions: Optional[Dict[str, Faction]] = None
+
+ @property
+ def factions(self) -> Dict[str, Faction]:
+ self.initialize()
+ assert self._factions is not None
+ return self._factions
+
+ def initialize(self) -> None:
+ if self._factions is None:
+ self._factions = self.load_factions()
+
+ @classmethod
+ def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
+ files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
+ factions = {}
+
+ for f in files:
+ try:
+ with f.open("r", encoding="utf-8") as fdata:
+ data = json.load(fdata, encoding="utf-8")
+ factions[data["name"]] = Faction.from_json(data)
+ logging.info("Loaded faction : " + str(f))
+ except Exception:
+ logging.exception(f"Unable to load faction : {f}")
+
+ return factions
+
+ def __getitem__(self, name: str) -> Faction:
+ return self.factions[name]
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self.factions.keys())
diff --git a/game/factions/france_1995.py b/game/factions/france_1995.py
deleted file mode 100644
index acf56495..00000000
--- a/game/factions/france_1995.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-France_1995 = {
- "country": "France",
- "side": "blue",
- "units": [
- M_2000C,
- Mirage_2000_5,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- SA342M,
- SA342L,
- SA342Mistral,
-
- Armor.MBT_Leclerc,
- Armor.TPz_Fuchs, # Standing as VAB
- Armor.APC_Cobra, # Standing as VBL
- Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
- Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
- Artillery.MLRS_M270,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Roland_ADS,
- AirDefence.SAM_Hawk_PCP,
- AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
-
- ], "shorad": [
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.SAM_Roland_ADS
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/france_2005.py b/game/factions/france_2005.py
deleted file mode 100644
index b2f4b87a..00000000
--- a/game/factions/france_2005.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-France_2005 = {
- "country": "France",
- "side": "blue",
- "units":[
- M_2000C,
- Mirage_2000_5,
- FA_18C_hornet, # Standing as Rafale M
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- SA342M,
- SA342L,
- SA342Mistral,
-
- Armor.MBT_Leclerc,
- Armor.TPz_Fuchs, # Standing as VAB
- Armor.APC_Cobra, # Standing as VBL
- Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
- Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
- Artillery.MLRS_M270,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Roland_ADS,
- AirDefence.SAM_Hawk_PCP,
- AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
-
- ], "shorad":[
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.SAM_Roland_ADS
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
- ], "helicopter_carrier": [
- LHA_1_Tarawa, # Standing as Mistral Class
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "PA Charles de Gaulle",
- ], "lhanames": [
- "L9013 Mistral",
- "L9014 Tonerre",
- "L9015 Dixmude"
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/france_modded.py b/game/factions/france_modded.py
deleted file mode 100644
index ad0f7de5..00000000
--- a/game/factions/france_modded.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-import pydcs_extensions.frenchpack.frenchpack as frenchpack
-from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
-
-France_2005_Modded = {
- "country": "France",
- "side": "blue",
- "units": [
- M_2000C,
- Mirage_2000_5,
- Rafale_M,
- Rafale_A_S,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- SA342M,
- SA342L,
- SA342Mistral,
-
- Armor.MBT_Leclerc,
- Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
- Artillery.MLRS_M270,
-
- frenchpack.AMX_10RCR,
- frenchpack.AMX_10RCR_SEPAR,
- frenchpack.ERC_90,
- frenchpack.TRM_2000_PAMELA,
- frenchpack.VAB__50,
- frenchpack.VAB_MEPHISTO,
- frenchpack.VAB_T20_13,
- frenchpack.VBL__50,
- frenchpack.VBL_AANF1,
- frenchpack.VBAE_CRAB,
- frenchpack.VBAE_CRAB_MMP,
- frenchpack.AMX_30B2,
- frenchpack.Leclerc_Serie_XXI,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Roland_ADS,
- AirDefence.SAM_Hawk_PCP,
- AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
-
- ], "shorad": [
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.SAM_Roland_ADS
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
- ], "helicopter_carrier": [
- LHA_1_Tarawa, # Standing as Mistral Class
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "PA Charles de Gaulle",
- ], "lhanames": [
- "L9013 Mistral",
- "L9014 Tonerre",
- "L9015 Dixmude"
- ], "boat": [
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "requirements": {
- "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974",
- "RAFALE 2.5.5": "https://www.digitalcombatsimulator.com/fr/files/3307478/",
- }, "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/germany_1944.py b/game/factions/germany_1944.py
deleted file mode 100644
index 9123b350..00000000
--- a/game/factions/germany_1944.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from dcs.planes import *
-from dcs.vehicles import *
-
-from game.data.building_data import WW2_GERMANY_BUILDINGS
-from game.data.doctrine import WWII_DOCTRINE
-
-Germany_1944 = {
- "country": "Third Reich",
- "side": "red",
- "units": [
-
- FW_190A8,
- FW_190D9,
- Bf_109K_4,
- Ju_88A4,
-
- Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
- Armor.MT_Pz_Kpfw_IV_Ausf_H,
- Armor.HT_Pz_Kpfw_VI_Tiger_I,
- Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
- Armor.APC_Sd_Kfz_251,
- Armor.IFV_Sd_Kfz_234_2_Puma,
- Armor.Sd_Kfz_184_Elefant,
- Armor.TD_Jagdpanther_G1,
- Armor.TD_Jagdpanzer_IV,
-
- Artillery.Sturmpanzer_IV_Brummbär,
-
- Unarmed.Sd_Kfz_2,
- Unarmed.Sd_Kfz_7,
- Unarmed.KĂĽbelwagen_82,
-
- Infantry.Infantry_Mauser_98,
- AirDefence.AAA_8_8cm_Flak_36,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "shorad": [
- AirDefence.AAA_8_8cm_Flak_36,
- ],
- "objects": WW2_GERMANY_BUILDINGS,
- "doctrine": WWII_DOCTRINE,
- "boat": ["UBoatGroupGenerator", "SchnellbootGroupGenerator"],
- "boat_count": 2,
- "missiles": ["V1GroupGenerator"],
- "missiles_count": 1
-}
\ No newline at end of file
diff --git a/game/factions/germany_1944_easy.py b/game/factions/germany_1944_easy.py
deleted file mode 100644
index b79d45f0..00000000
--- a/game/factions/germany_1944_easy.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from dcs.planes import *
-from dcs.vehicles import *
-
-from game.data.building_data import WW2_GERMANY_BUILDINGS
-from game.data.doctrine import WWII_DOCTRINE
-
-Germany_1944_Easy = {
- "country": "Third Reich",
- "side": "red",
- "units": [
-
- FW_190A8,
- FW_190D9,
- Bf_109K_4,
- Ju_88A4,
-
- Armor.MT_Pz_Kpfw_IV_Ausf_H,
- Armor.APC_Sd_Kfz_251,
- Armor.IFV_Sd_Kfz_234_2_Puma,
- Artillery.Sturmpanzer_IV_Brummbär,
-
- Unarmed.Sd_Kfz_2,
- Unarmed.Sd_Kfz_7,
- Unarmed.KĂĽbelwagen_82,
-
- Infantry.Infantry_Mauser_98,
- AirDefence.AAA_8_8cm_Flak_36,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "shorad":[
- AirDefence.AAA_8_8cm_Flak_36,
- ],
- "objects": WW2_GERMANY_BUILDINGS,
- "doctrine": WWII_DOCTRINE,
- "boat": ["UBoatGroupGenerator", "SchnellbootGroupGenerator"],
- "boat_count": 1,
- "missiles": ["V1GroupGenerator"],
- "missiles_count": 1
-}
\ No newline at end of file
diff --git a/game/factions/germany_1990.py b/game/factions/germany_1990.py
deleted file mode 100644
index ae1f0668..00000000
--- a/game/factions/germany_1990.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Germany_1990 = {
- "country": "Germany",
- "side": "blue",
- "units":[
- MiG_29G,
- Tornado_IDS,
- F_4E,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- SA342M,
- SA342L,
-
- Armor.TPz_Fuchs,
- Armor.MBT_Leopard_1A3,
- Armor.MBT_Leopard_2,
- Armor.IFV_Marder,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Roland_ADS,
- AirDefence.SAM_Hawk_PCP,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad":[
- AirDefence.SPAAA_Gepard,
- AirDefence.SAM_Roland_ADS,
- ], "boat":[
- "OliverHazardPerryGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/india_2010.py b/game/factions/india_2010.py
deleted file mode 100644
index 2dc756ec..00000000
--- a/game/factions/india_2010.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-India_2010 = {
- "country": "India",
- "side": "blue",
- "units": [
- Mirage_2000_5,
- M_2000C,
- MiG_27K,
- MiG_21Bis,
- MiG_29S,
- Su_30,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- AH_64A,
- Mi_8MT,
-
- Armor.MBT_T_90,
- Armor.MBT_T_72B,
- Armor.IFV_BMP_2,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad":[
- AirDefence.SAM_SA_8_Osa_9A33,
- AirDefence.AAA_ZU_23_Emplacement,
- AirDefence.SPAAA_ZSU_23_4_Shilka,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
- AirDefence.SAM_SA_8_Osa_9A33,
- AirDefence.SAM_SA_19_Tunguska_2S6
- ], "aircraft_carrier": [
- CV_1143_5_Admiral_Kuznetsov,
- ], "destroyer": [
- FSG_1241_1MP_Molniya,
- ], "carrier_names": [
- "INS Vikramaditya"
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator", "MolniyaGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/insurgent.py b/game/factions/insurgent.py
deleted file mode 100644
index d94603c6..00000000
--- a/game/factions/insurgent.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from dcs.vehicles import *
-from dcs.ships import *
-from dcs.planes import *
-from dcs.helicopters import *
-
-Insurgent = {
- "country": "Insurgents",
- "side": "red",
- "units": [
-
- AirDefence.AAA_ZU_23_Insurgent_Closed,
- AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
-
- Armor.APC_Cobra,
- Armor.APC_MTLB,
- Armor.ARV_BRDM_2,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
- Infantry.Infantry_Soldier_Insurgents,
- Infantry.Soldier_RPG,
-
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160
- ]
-}
\ No newline at end of file
diff --git a/game/factions/insurgent_modded.py b/game/factions/insurgent_modded.py
deleted file mode 100644
index 39e1e174..00000000
--- a/game/factions/insurgent_modded.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.frenchpack.frenchpack import DIM__TOYOTA_BLUE, DIM__TOYOTA_DESERT, DIM__TOYOTA_GREEN, \
- DIM__KAMIKAZE
-
-Insurgent_modded = {
- "country": "Insurgents",
- "side": "red",
- "units": [
-
- AirDefence.AAA_ZU_23_Insurgent_Closed,
- AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
-
- DIM__TOYOTA_BLUE,
- DIM__TOYOTA_DESERT,
- DIM__TOYOTA_GREEN,
- DIM__KAMIKAZE,
- Armor.ARV_BRDM_2,
- Armor.APC_Cobra,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
- Infantry.Soldier_AK,
- Infantry.Infantry_Soldier_Insurgents,
-
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160
- ], "requirements": {
- "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974",
- }
-}
\ No newline at end of file
diff --git a/game/factions/iran_2015.py b/game/factions/iran_2015.py
deleted file mode 100644
index 56751a2c..00000000
--- a/game/factions/iran_2015.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Iran_2015 = {
- "country": "Iran",
- "side": "red",
- "units": [
-
- MiG_29A,
- F_4E,
- F_14B,
- F_5E_3,
-
- MiG_21Bis,
- Su_24M,
- Su_25,
- Su_17M4,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- A_50,
-
- Mi_28N,
- Mi_24V,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_SA_2_LN_SM_90,
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.SAM_SA_11_Buk_LN_9A310M1,
-
- Armor.APC_M113,
- Armor.APC_BTR_80,
- Armor.MBT_M60A3_Patton,
- Armor.MBT_T_72B,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
- Infantry.Soldier_AK,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160
- ],
- "shorad":[
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.AAA_ZU_23_Insurgent_Closed
- ], "boat":[
- "GrishaGroupGenerator", "MolniyaGroupGenerator", "KiloSubGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/israel_1948.py b/game/factions/israel_1948.py
deleted file mode 100644
index ffa57fcb..00000000
--- a/game/factions/israel_1948.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Israel_1948 = {
- "country": "Israel",
- "side": "blue",
- "units":[
- SpitfireLFMkIXCW,
- SpitfireLFMkIX,
- P_51D,
- P_51D_30_NA,
- Bf_109K_4, # Standing as Avia S-199
- B_17G,
-
- Armor.MT_M4A4_Sherman_Firefly,
- Armor.APC_M2A1,
- Armor.MT_M4_Sherman,
- Armor.LAC_M8_Greyhound,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_SMLE_No_4_Mk_1,
-
- AirDefence.AAA_Bofors_40mm,
- Armed_speedboat,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "shorad": [
- AirDefence.AAA_Bofors_40mm
- ], "boat": [
- ], "has_jtac": False
-}
\ No newline at end of file
diff --git a/game/factions/israel_1973.py b/game/factions/israel_1973.py
deleted file mode 100644
index 00624ec9..00000000
--- a/game/factions/israel_1973.py
+++ /dev/null
@@ -1,112 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.a4ec.a4ec import A_4E_C
-
-Israel_1973 = {
- "country": "Israel",
- "side": "blue",
- "units":[
- F_4E,
- A_4E_C,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
-
- Armor.MT_M4A4_Sherman_Firefly,
- Armor.APC_M2A1,
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.AAA_Bofors_40mm,
- AirDefence.SAM_Chaparral_M48,
-
- Armed_speedboat,
- ], "requirements": {
- "Community A-4E": "https://heclak.github.io/community-a4e-c/",
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- }, "shorad": [
- AirDefence.SAM_Chaparral_M48,
- AirDefence.AAA_Bofors_40mm
- ], "boat": [
- ], "has_jtac": True
-}
-
-Israel_1973_NO_WW2_UNITS = {
- "country": "Israel",
- "side": "blue",
- "units":[
- F_4E,
- A_4E_C,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Chaparral_M48,
-
- Armed_speedboat,
- ], "requirements": {
- "Community A-4E": "https://heclak.github.io/community-a4e-c/",
- }, "shorad": [
- AirDefence.SAM_Chaparral_M48,
- ], "boat": [
- ], "has_jtac": True
-}
-
-Israel_1982 = {
- "country": "Israel",
- "side": "blue",
- "units":[
- F_4E,
- A_4E_C,
- F_15C,
- F_16A,
- F_16C_50,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_1W,
-
- Armor.APC_M113,
- Armor.MBT_M60A3_Patton,
- Armor.MBT_Merkava_Mk__4,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Chaparral_M48,
-
- Armed_speedboat,
- ], "requirements": {
- "Community A-4E": "https://heclak.github.io/community-a4e-c/",
- }, "shorad": [
- AirDefence.SAM_Chaparral_M48,
- ], "boat": [
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/israel_2000.py b/game/factions/israel_2000.py
deleted file mode 100644
index 6c0db9c0..00000000
--- a/game/factions/israel_2000.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Israel_2000 = {
- "country": "Israel",
- "side": "blue",
- "units":[
- F_16C_50,
- F_15C,
- F_15E,
- F_4E,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- AH_1W,
- AH_64D,
-
- Armor.MBT_Merkava_Mk__4,
- Armor.APC_M113,
- Armor.APC_M1043_HMMWV_Armament,
- Armor.ATGM_M1045_HMMWV_TOW,
- Artillery.SPH_M109_Paladin,
- Artillery.MLRS_M270,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Patriot_EPP_III,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad": [
- AirDefence.SAM_Avenger_M1097
- ], "boat": [
- "ArleighBurkeGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/italy_1990.py b/game/factions/italy_1990.py
deleted file mode 100644
index 2c2175a8..00000000
--- a/game/factions/italy_1990.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Italy_1990 = {
- "country": "Italy",
- "side": "blue",
- "units": [
- Tornado_IDS,
- AV8BNA,
- # MB339,
-
- KC_135,
- S_3B_Tanker,
- C_130,
- E_3A,
-
- AH_1W,
- UH_1H,
-
- Armor.MBT_Leopard_1A3, # OF-40 MBT
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "lhanames": [
- "Giuseppe Garibaldi",
- "Cavour",
- ], "boat":[
- "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/italy_1990_mb339.py b/game/factions/italy_1990_mb339.py
deleted file mode 100644
index 92307749..00000000
--- a/game/factions/italy_1990_mb339.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.mb339.mb339 import MB_339PAN
-
-Italy_1990_MB339 = {
- "country": "Italy",
- "side": "blue",
- "units": [
- Tornado_IDS,
- AV8BNA,
- MB_339PAN,
-
- KC_135,
- S_3B_Tanker,
- C_130,
- E_3A,
-
- AH_1W,
- UH_1H,
-
- Armor.MBT_Leopard_1A3, # OF-40 MBT
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "lhanames": [
- "Giuseppe Garibaldi",
- "Cavour",
- ], "boat": [
- "OliverHazardPerryGroupGenerator"
- ], "requirements": {
- "MB-339A": "http://www.freccetricolorivirtuali.net/",
- }, "has_jtac": True
-}
diff --git a/game/factions/japan_2005.py b/game/factions/japan_2005.py
deleted file mode 100644
index c9657348..00000000
--- a/game/factions/japan_2005.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Japan_2005 = {
- "country": "Japan",
- "side": "blue",
- "units": [
- F_15C, # F-15J/DJ
- F_16C_50, # F-2A/B
- F_4E, # F-4EJ
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- AH_1W,
- AH_64D,
-
- Armor.MBT_Merkava_Mk__4, # Standing as Type 10 MBT
- Armor.MBT_M1A2_Abrams, # Standing as Type 90 MBT
- Armor.IFV_Marder, # Standing as Type 89 IFV
- Armor.TPz_Fuchs, # Standing as Type 96 APC
- Armor.IFV_LAV_25, # Standing as Type 16 or Type 87
- Armor.APC_M1043_HMMWV_Armament,
-
- Artillery.MLRS_M270,
- Artillery.SPH_M109_Paladin, # Standing as Type 99 SPH
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Patriot_EPP_III,
-
- LHA_1_Tarawa,
- ], "shorad": [
- AirDefence.SPAAA_Gepard, # Type 87 SPAG
- ], "helicopter_carrier": [
- LHA_1_Tarawa, # Standing as Hyuga-class helicopter carrier
- ], "destroyer": [
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "lhanames": [
- "Hyuga",
- "Ise",
- ], "boat":[
- "ArleighBurkeGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/libya_2011.py b/game/factions/libya_2011.py
deleted file mode 100644
index 688b4877..00000000
--- a/game/factions/libya_2011.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.vehicles import *
-
-Libya_2011 = {
- "country": "Libya",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_23MLD,
- Su_24M,
- Su_17M4,
- Mi_24V,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
- A_50,
-
- AirDefence.SAM_SA_8_Osa_9A33,
- AirDefence.SAM_SA_2_LN_SM_90,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.HQ_7_Self_Propelled_LN,
-
- Armor.IFV_BMP_1,
- Armor.FDDM_Grad,
- Armor.ARV_BRDM_2,
- Armor.MBT_T_55,
- Armor.MBT_T_72B,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_RPG_16,
- Infantry.Infantry_Soldier_Insurgents
-
- ],
- "shorad":[
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.SAM_SA_8_Osa_9A33,
- ], "boat": [
- "GrishaGroupGenerator", "MolniyaGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/netherlands_1990.py b/game/factions/netherlands_1990.py
deleted file mode 100644
index b32fe7d0..00000000
--- a/game/factions/netherlands_1990.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Netherlands_1990 = {
- "country": "The Netherlands",
- "side": "blue",
- "units": [
- F_16C_50,
- F_5E_3,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- AH_64A,
-
- Armor.APC_M113,
- Armor.MBT_Leopard_1A3,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad": [
- AirDefence.SAM_Avenger_M1097
- ], "boat": [
- "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/factions/north_korea_2000.py b/game/factions/north_korea_2000.py
deleted file mode 100644
index dd588f92..00000000
--- a/game/factions/north_korea_2000.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-NorthKorea_2000 = {
- "country": "North Korea",
- "side": "red",
- "units":[
- MiG_29A,
- Su_25,
- MiG_15bis,
- MiG_21Bis,
- MiG_23MLD,
- MiG_19P,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- A_50,
-
- Mi_8MT,
- Mi_24V,
-
- Armor.MBT_T_55,
- Armor.MBT_T_72B,
- Armor.MBT_T_80U,
- Armor.IFV_BMP_1,
- Armor.APC_BTR_80,
- Armor.ARV_BRDM_2,
-
- Unarmed.Transport_M818,
- Infantry.Soldier_AK,
-
- AirDefence.SAM_SA_2_LN_SM_90,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160
- ],
- "shorad":[
- AirDefence.AAA_ZU_23_Emplacement,
- AirDefence.SPAAA_ZSU_23_4_Shilka
- ],
- "boat": [
- "GrishaGroupGenerator", "MolniyaGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/pakistan_2015.py b/game/factions/pakistan_2015.py
deleted file mode 100644
index 67bfa2aa..00000000
--- a/game/factions/pakistan_2015.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Pakistan_2015 = {
- "country": "Pakistan",
- "side": "blue",
- "units": [
- JF_17,
- F_16C_50,
- MiG_21Bis, # Standing as J-7
- MiG_19P, # Standing as J-6
- IL_78M,
- E_3A,
-
- UH_1H,
- AH_1W,
-
- Armor.MBT_T_80U,
- Armor.MBT_T_55, # Standing as Al-Zarrar / Type 59 MBT
- Armor.ZBD_04A,
- Armor.APC_BTR_80,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_SA_2_LN_SM_90, # Standing as HQ-2
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9
-
- Armed_speedboat,
- ], "shorad": [
- AirDefence.HQ_7_Self_Propelled_LN,
- AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
- AirDefence.AAA_ZU_23_Closed
- ], "boat": [
- "Type54GroupGenerator", "OliverHazardPerryGroupGenerator"
- ],
- "has_jtac": True,
- "jtac_unit": WingLoong_I
-}
diff --git a/game/factions/private_miltary_companies.py b/game/factions/private_miltary_companies.py
deleted file mode 100644
index 4f2860ca..00000000
--- a/game/factions/private_miltary_companies.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from pydcs_extensions.mb339.mb339 import MB_339PAN
-
-PMC_WESTERN_A = {
- "country": "USA",
- "side": "blue",
- "units": [
- C_101CC,
-
- UH_1H,
- Mi_8MT,
- OH_58D,
- SA342M,
-
- Armor.APC_M1043_HMMWV_Armament,
- Armor.IFV_MCV_80,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Avenger_M1097,
-
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "has_jtac": True
-}
-
-PMC_WESTERN_B = {
- "country": "USA",
- "side": "blue",
- "units": [
- MB_339PAN,
- C_101CC,
-
- UH_1H,
- Mi_8MT,
- OH_58D,
- SA342M,
-
- Armor.APC_M1043_HMMWV_Armament,
- Armor.IFV_MCV_80,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Avenger_M1097,
-
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "has_jtac": True,
- "requirements": {
- "MB-339A": "http://www.freccetricolorivirtuali.net/",
- }
-}
-
-PMC_RUSSIAN = {
- "country": "Russia",
- "side": "blue",
- "units": [
- L_39C,
- L_39ZA,
-
- Mi_8MT,
- Mi_24V,
- Ka_50,
-
- Armor.APC_Cobra,
- Armor.APC_BTR_80,
- Armor.ARV_BRDM_2,
-
- Unarmed.Transport_Ural_375,
- Infantry.Paratrooper_AKS,
- Infantry.Paratrooper_RPG_16,
-
- AirDefence.AAA_ZU_23_on_Ural_375,
-
- Armed_speedboat,
- ], "shorad":[
- AirDefence.AAA_ZU_23_on_Ural_375,
- AirDefence.AAA_ZU_23_Closed,
- ], "has_jtac": True
-}
-
diff --git a/game/factions/russia_1955.py b/game/factions/russia_1955.py
deleted file mode 100644
index dcaeec68..00000000
--- a/game/factions/russia_1955.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40
-from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
-from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
-
-Russia_1955 = {
- "country": "Russia",
- "side": "red",
- "units": [
- MiG_15bis,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- AirDefence.AAA_ZU_23_Closed,
- AirDefence.AAA_ZU_23_on_Ural_375,
- Armor.ARV_BRDM_2,
- Armor.FDDM_Grad,
- Armor.APC_MTLB,
- Armor.MBT_T_55,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
-
- # Infantry squad
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
- ]
-}
\ No newline at end of file
diff --git a/game/factions/russia_1965.py b/game/factions/russia_1965.py
deleted file mode 100644
index bc5762c6..00000000
--- a/game/factions/russia_1965.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from dcs.helicopters import Mi_8MT
-from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50
-from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
-from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
-
-Russia_1965 = {
- "country": "Russia",
- "side": "red",
- "units": [
- MiG_15bis,
- MiG_19P,
- MiG_21Bis,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- A_50,
-
- Mi_8MT,
-
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_2_LN_SM_90,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
-
- Armor.ARV_BRDM_2,
- Armor.APC_BTR_80,
- Armor.ARV_BTR_RD,
- Armor.IFV_BMD_1,
- Armor.IFV_BMP_1,
- Armor.MBT_T_55,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
-
- # Infantry squad
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
-
- ],
- "shorad":[
- AirDefence.AAA_ZU_23_Closed
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/russia_1975.py b/game/factions/russia_1975.py
deleted file mode 100644
index db6a50ae..00000000
--- a/game/factions/russia_1975.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from dcs.helicopters import Mi_8MT, Mi_24V
-from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \
- Yak_40, A_50
-from dcs.ships import *
-from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
-
-Russia_1975 = {
- "country": "Russia",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_23MLD,
- MiG_25PD,
- MiG_29A,
-
- Su_17M4,
- Su_24M,
- Su_25,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- A_50,
-
- Mi_8MT,
- Mi_24V,
-
- AirDefence.AAA_ZU_23_Closed,
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
-
- Armor.ARV_BRDM_2,
- Armor.APC_BTR_80,
- Armor.IFV_BMD_1,
- Armor.IFV_BMP_1,
- Armor.MBT_T_55,
-
- Artillery.SPH_2S9_Nona,
- Artillery.SPH_2S1_Gvozdika,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
-
- # Infantry squad
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
-
- ],
- "shorad": [
- AirDefence.AAA_ZU_23_Emplacement,
- AirDefence.SPAAA_ZSU_23_4_Shilka
- ], "aircraft_carrier": [
- CV_1143_5_Admiral_Kuznetsov,
- ], "destroyer": [
- FF_1135M_Rezky,
- ], "cruiser": [
- CGN_1144_2_Pyotr_Velikiy,
- ], "boat": [
- "RussianNavyGroupGenerator", "KiloSubGroupGenerator", "MolniyaGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/russia_1990.py b/game/factions/russia_1990.py
deleted file mode 100644
index ee2758a5..00000000
--- a/game/factions/russia_1990.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Russia_1990 = {
- "country": "Russia",
- "side": "red",
- "units": [
-
- MiG_23MLD,
- MiG_25PD,
- MiG_29A,
- MiG_29S,
- MiG_31,
- Su_27,
-
- Su_24M,
- Su_25,
- Ka_50,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- A_50,
-
- Mi_8MT,
- Mi_24V,
-
- AirDefence.AAA_ZU_23_Closed,
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
-
- Armor.ARV_BRDM_2,
- Armor.APC_BTR_80,
- Armor.IFV_BMD_1,
- Armor.IFV_BMP_1,
- Armor.MBT_T_55,
- Artillery.MLRS_9K57_Uragan_BM_27,
- Artillery.SPH_2S19_Msta,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
-
- # Infantry squad
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
- ],
- "shorad":[
- AirDefence.SAM_SA_9_Strela_1_9P31,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
- AirDefence.SPAAA_ZSU_23_4_Shilka
- ], "carrier_names": [
- "Admiral Kuznetov",
- "Admiral Gorshkov"
- ], "aircraft_carrier": [
- CV_1143_5_Admiral_Kuznetsov,
- ], "destroyer": [
- FF_1135M_Rezky,
- ], "cruiser": [
- FSG_1241_1MP_Molniya,
- ], "boat":[
- "RussianNavyGroupGenerator", "KiloSubGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/russia_2010.py b/game/factions/russia_2010.py
deleted file mode 100644
index cc45f062..00000000
--- a/game/factions/russia_2010.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Russia_2010 = {
- "country": "Russia",
- "side": "red",
- "units": [
-
- Su_27,
- Su_30,
- Su_33,
- MiG_29S,
- MiG_31,
-
- Su_25,
- Su_25T,
- Su_34,
- Su_24M,
- L_39ZA,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
- A_50,
-
- Ka_50,
- Mi_8MT,
- Mi_24V,
- Mi_28N,
-
- AirDefence.SAM_SA_19_Tunguska_2S6,
- AirDefence.SAM_SA_11_Buk_LN_9A310M1,
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
-
- Armor.APC_BTR_80,
- Armor.MBT_T_90,
- Armor.MBT_T_80U,
- Armor.MBT_T_72B,
- Armor.IFV_BMP_1,
- Armor.IFV_BMP_2,
- Armor.IFV_BMP_3,
-
- Artillery.MLRS_9K57_Uragan_BM_27,
- Artillery.SPH_2S19_Msta,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
-
- # Infantry squad
- Infantry.Paratrooper_AKS,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Paratrooper_RPG_16,
- ],
- "shorad":[
- AirDefence.SAM_SA_19_Tunguska_2S6,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3
- ], "aircraft_carrier": [
- CV_1143_5_Admiral_Kuznetsov,
- ], "carrier_names": [
- "Admiral Kuznetov"
- ], "destroyer": [
- FF_1135M_Rezky,
- ], "cruiser": [
- FSG_1241_1MP_Molniya,
- ], "boat": [
- "RussianNavyGroupGenerator", "KiloSubGroupGenerator"
- ]
-}
diff --git a/game/factions/spain_1990.py b/game/factions/spain_1990.py
deleted file mode 100644
index f016cb8a..00000000
--- a/game/factions/spain_1990.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Spain_1990 = {
- "country": "Spain",
- "side": "blue",
- "units": [
- FA_18C_hornet,
- AV8BNA,
- F_5E_3,
- C_101CC,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- Armor.MBT_M60A3_Patton,
- Armor.MBT_Leopard_2,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis, # Standing as Principe de Asturias
- ], "helicopter_carrier": [
- LHA_1_Tarawa, # Standing as Juan Carlos
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "Principe de Asturias",
- ], "lhanames": [
- "Juan Carlos I",
- ], "boat":[
- "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/sweden_1990.py b/game/factions/sweden_1990.py
deleted file mode 100644
index ebc754f9..00000000
--- a/game/factions/sweden_1990.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from dcs.vehicles import *
-from dcs.ships import *
-from dcs.planes import *
-from dcs.helicopters import *
-
-Sweden_1990 = {
- "country": "Sweden",
- "side": "blue",
- "units": [
- AJS37,
-
- UH_1H,
-
- AirDefence.SAM_Hawk_PCP,
-
- Armor.IFV_MCV_80, # Standing as Strf 90
- Armor.MBT_Leopard_2,
- Armor.APC_M1126_Stryker_ICV, # Closest thing available
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
- Infantry.Soldier_AK,
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
- ],
- "shorad": [
- AirDefence.SAM_Avenger_M1097
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/syria.py b/game/factions/syria.py
deleted file mode 100644
index de082320..00000000
--- a/game/factions/syria.py
+++ /dev/null
@@ -1,267 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.vehicles import *
-
-Syria_2011 = {
- "country": "Syria",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_23MLD,
- MiG_25PD,
- MiG_29S,
-
- Su_17M4,
- Su_24M,
-
- L_39ZA,
-
- Mi_24V,
- Mi_8MT,
- SA342M,
- SA342L,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
- A_50,
-
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
- AirDefence.SAM_SA_2_LN_SM_90,
- AirDefence.SAM_SA_8_Osa_9A33,
- AirDefence.SAM_SA_11_Buk_LN_9A310M1,
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
-
- Armor.IFV_BMP_1,
- Armor.IFV_BMP_2,
- Armor.APC_BTR_80,
- Armor.ARV_BRDM_2,
- Armor.APC_MTLB,
- Armor.APC_Cobra,
- Armor.MBT_T_55,
- Armor.MBT_T_72B,
- Armor.MBT_T_90,
- Artillery.MLRS_BM_21_Grad,
- Artillery.MLRS_9K57_Uragan_BM_27,
- Artillery.SPH_2S1_Gvozdika,
- Artillery.SPH_2S9_Nona,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_RPG_16,
- Infantry.Soldier_AK
-
- ],
- "shorad": [
- AirDefence.SAM_SA_8_Osa_9A33,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
- AirDefence.SAM_SA_9_Strela_1_9P31,
- AirDefence.SAM_SA_19_Tunguska_2S6,
- AirDefence.AAA_ZU_23_on_Ural_375,
- ], "boat": [
- "GrishaGroupGenerator", "MolniyaGroupGenerator"
- ]
-}
-
-Syria_1973 = {
- "country": "Syria",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_19P,
- MiG_15bis, # Standing as Mig-17
-
- Su_17M4, # Standing as Su-7
- Mi_8MT,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
- AirDefence.SAM_SA_2_LN_SM_90,
-
- Armor.IFV_BMP_1,
- Armor.APC_MTLB,
- Armor.MBT_T_55,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_RPG_16,
- Infantry.Soldier_AK
-
- ],
- "shorad": [
- AirDefence.AAA_ZU_23_on_Ural_375,
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
-
-
-Syria_1982 = {
- "country": "Syria",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_23MLD,
- MiG_25PD,
- MiG_19P,
-
- Su_17M4, # Standing as Su-7
- Mi_8MT,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- AirDefence.SAM_SA_6_Kub_LN_2P25,
- AirDefence.SAM_SA_3_S_125_LN_5P73,
- AirDefence.SAM_SA_2_LN_SM_90,
-
- Armor.IFV_BMP_1,
- Armor.APC_MTLB,
- Armor.MBT_T_55,
- Armor.MBT_T_72B,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_RPG_16,
- Infantry.Soldier_AK
-
- ],
- "shorad": [
- AirDefence.AAA_ZU_23_on_Ural_375,
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
-
-
-Syria_1967 = {
- "country": "Syria",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_19P,
- MiG_15bis, # Standing as Mig-17
-
- Su_17M4, # Standing as Su-7
- Mi_8MT,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- AirDefence.SAM_SA_2_LN_SM_90,
-
- Armor.ARV_BRDM_2,
- Armor.MBT_T_55,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Paratrooper_RPG_16,
- Infantry.Soldier_AK
-
- ],
- "shorad": [
- AirDefence.AAA_ZU_23_on_Ural_375,
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
-
-Syria_1967_WW2_Weapons = {
- "country": "Syria",
- "side": "red",
- "units": [
-
- MiG_21Bis,
- MiG_19P,
- MiG_15bis, # Standing as Mig-17
-
- Su_17M4, # Standing as Su-7
- Mi_8MT,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
-
- AirDefence.SAM_SA_2_LN_SM_90,
-
- Armor.ARV_BRDM_2,
- Armor.MBT_T_55,
- Armor.MT_Pz_Kpfw_IV_Ausf_H,
- Armor.StuG_III_Ausf__G,
- Armor.TD_Jagdpanzer_IV,
- Artillery.MLRS_BM_21_Grad,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Soldier_RPG,
- Infantry.Soldier_AK
-
- ], "requirements": {
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "shorad": [
- AirDefence.AAA_ZU_23_on_Ural_375,
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
-
-Arab_Armies_1948 = {
- "country": "Syria",
- "side": "red",
- "units": [
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
-
- AirDefence.SAM_SA_2_LN_SM_90,
-
- Armor.MT_M4_Sherman,
- Armor.MT_Pz_Kpfw_IV_Ausf_H,
- Armor.APC_Sd_Kfz_251,
- Armor.IFV_Sd_Kfz_234_2_Puma,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
-
- Infantry.Infantry_SMLE_No_4_Mk_1,
-
- AirDefence.AAA_8_8cm_Flak_36,
-
- ], "requirements": {
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "shorad": [
- AirDefence.AAA_8_8cm_Flak_36,
- ], "boat": [
- "GrishaGroupGenerator"
- ]
-}
diff --git a/game/factions/turkey_2005.py b/game/factions/turkey_2005.py
deleted file mode 100644
index 02b0500d..00000000
--- a/game/factions/turkey_2005.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Turkey_2005 = {
- "country": "Turkey",
- "side": "blue",
- "units":[
- F_16C_50,
- F_4E,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_1W,
-
- Armor.MBT_Leopard_2,
- Armor.MBT_Leopard_1A3,
- Armor.MBT_M60A3_Patton,
- Armor.APC_Cobra,
- Armor.APC_BTR_80,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.SAM_Avenger_M1097,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.AAA_ZU_23_Emplacement,
- AirDefence.SPAAA_ZSU_23_4_Shilka
- ], "boat":[
- "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/uae_2005.py b/game/factions/uae_2005.py
deleted file mode 100644
index cc412f06..00000000
--- a/game/factions/uae_2005.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-UAE_2005 = {
- "country": "United Arab Emirates",
- "side": "blue",
- "units":[
- M_2000C,
- Mirage_2000_5,
- F_16C_50,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- AH_64D,
-
- Armor.MBT_Leclerc,
- Armor.IFV_BMP_3,
- Armor.TPz_Fuchs,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.Rapier_FSA_Launcher,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "boat":[
- "OliverHazardPerryGroupGenerator"
- ],
- "has_jtac": True,
- "jtac_unit": WingLoong_I
-}
\ No newline at end of file
diff --git a/game/factions/uk_1944.py b/game/factions/uk_1944.py
deleted file mode 100644
index 7798d714..00000000
--- a/game/factions/uk_1944.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from game.data.building_data import WW2_ALLIES_BUILDINGS
-from game.data.doctrine import WWII_DOCTRINE
-
-UK_1944 = {
- "country": "UK",
- "side": "blue",
- "units": [
- P_51D,
- P_51D_30_NA,
- P_47D_30,
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
- A_20G,
- B_17G,
-
- Armor.MT_M4A4_Sherman_Firefly,
- Armor.MT_M4_Sherman,
- Armor.APC_M2A1,
- Armor.CT_Cromwell_IV,
- Armor.ST_Centaur_IV,
- Armor.HIT_Churchill_VII,
-
- Infantry.Infantry_SMLE_No_4_Mk_1,
-
- LS_Samuel_Chase,
- LST_Mk_II,
- LCVP__Higgins_boat,
-
- Unarmed.CCKW_353,
- AirDefence.AAA_Bofors_40mm,
- ], "shorad":[
- AirDefence.AAA_Bofors_40mm,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "objects": WW2_ALLIES_BUILDINGS,
- "doctrine": WWII_DOCTRINE,
- "boat": ["WW2LSTGroupGenerator"],
- "boat_count": 1
-}
\ No newline at end of file
diff --git a/game/factions/uk_1990.py b/game/factions/uk_1990.py
deleted file mode 100644
index f2bcc57a..00000000
--- a/game/factions/uk_1990.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-UnitedKingdom_1990 = {
- "country": "UK",
- "side": "blue",
- "units":[
- AV8BNA, # Standing as BAE Harrier 2
- Tornado_GR4,
- F_4E,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- SA342M,
- AH_64A,
-
- Armor.MBT_Challenger_II,
- Armor.IFV_MCV_80,
- Armor.APC_M1043_HMMWV_Armament,
- Armor.ATGM_M1045_HMMWV_TOW,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.Rapier_FSA_Launcher,
- AirDefence.SAM_Avenger_M1097, # Standing as Starstreak
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- ], "cruiser": [
- Ticonderoga_class,
- ], "lhanames": [
- "HMS Invincible",
- "HMS Illustrious",
- "HMS Ark Royal",
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/ukraine_2010.py b/game/factions/ukraine_2010.py
deleted file mode 100644
index cd5149b6..00000000
--- a/game/factions/ukraine_2010.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-Ukraine_2010 = {
- "country": "Ukraine",
- "side": "blue",
- "units": [
- Su_25,
- Su_25T,
- Su_24M,
- Su_27,
- MiG_29S,
- L_39ZA,
-
- IL_76MD,
- IL_78M,
- An_26B,
- An_30M,
- Yak_40,
- A_50,
-
- Mi_8MT,
- Mi_24V,
-
- AirDefence.SAM_SA_3_S_125_LN_5P73,
- AirDefence.SAM_SA_11_Buk_LN_9A310M1,
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
-
- Armor.APC_M1043_HMMWV_Armament,
- Armor.IFV_BMP_3,
- Armor.IFV_BMP_2,
- Armor.APC_BTR_80,
- Armor.MBT_T_80U,
- Armor.MBT_T_72B,
-
- Unarmed.Transport_Ural_375,
- Unarmed.Transport_UAZ_469,
- Infantry.Soldier_AK,
- CV_1143_5_Admiral_Kuznetsov,
- Bulk_cargo_ship_Yakushev,
- Dry_cargo_ship_Ivanov,
- Tanker_Elnya_160,
- ],
- "shorad":[
- AirDefence.SAM_SA_19_Tunguska_2S6,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
- AirDefence.AAA_ZU_23_on_Ural_375
- ], "boat":[
- "GrishaGroupGenerator"
- ]
-}
\ No newline at end of file
diff --git a/game/factions/us_aggressors.py b/game/factions/us_aggressors.py
deleted file mode 100644
index ab4e6ffd..00000000
--- a/game/factions/us_aggressors.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-US_Aggressors = {
- "country": "USAF Aggressors",
- "side": "red",
- "units": [
-
- F_15C,
- F_5E_3,
- FA_18C_hornet,
- F_16C_50,
- Su_27,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_64D,
- Ka_50,
- SA342M,
- SA342L,
-
- Armor.MBT_M1A2_Abrams,
- Armor.MBT_Leopard_2,
- Armor.ATGM_M1134_Stryker,
- Armor.IFV_M2A2_Bradley,
- Armor.APC_M1043_HMMWV_Armament,
-
- Artillery.MLRS_M270,
- Artillery.SPH_M109_Paladin,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Patriot_EPP_III,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.SAM_Avenger_M1097,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ]
-}
diff --git a/game/factions/usa_1944.py b/game/factions/usa_1944.py
deleted file mode 100644
index 29e07372..00000000
--- a/game/factions/usa_1944.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-from game.data.building_data import WW2_ALLIES_BUILDINGS
-from game.data.doctrine import WWII_DOCTRINE
-
-USA_1944 = {
- "country": "USA",
- "side": "blue",
- "units": [
- P_51D,
- P_51D_30_NA,
- P_47D_30,
- A_20G,
- B_17G,
-
- Armor.MT_M4_Sherman,
- Armor.M30_Cargo_Carrier,
- Armor.APC_M2A1,
- Armor.LAC_M8_Greyhound,
- Armor.TD_M10_GMC,
- Artillery.M12_GMC,
-
- Infantry.Infantry_M1_Garand,
-
- LS_Samuel_Chase,
- LST_Mk_II,
- LCVP__Higgins_boat,
-
- Unarmed.CCKW_353,
- AirDefence.AAA_Bofors_40mm,
- ], "shorad":[
- AirDefence.AAA_Bofors_40mm,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "objects": WW2_ALLIES_BUILDINGS,
- "doctrine": WWII_DOCTRINE,
- "boat": ["WW2LSTGroupGenerator"],
- "boat_count": 2
-}
-
-ALLIES_1944 = {
- "country": "USA",
- "side": "blue",
- "units": [
- P_51D,
- P_51D_30_NA,
- P_47D_30,
- SpitfireLFMkIX,
- SpitfireLFMkIXCW,
- A_20G,
- B_17G,
-
- Armor.MT_M4_Sherman,
- Armor.MT_M4A4_Sherman_Firefly,
- Armor.CT_Cromwell_IV,
- Armor.M30_Cargo_Carrier,
- Armor.APC_M2A1,
- Armor.CT_Cromwell_IV,
- Armor.ST_Centaur_IV,
- Armor.HIT_Churchill_VII,
- Armor.LAC_M8_Greyhound,
- Armor.TD_M10_GMC,
- Artillery.M12_GMC,
-
- Infantry.Infantry_M1_Garand,
- Infantry.Infantry_SMLE_No_4_Mk_1,
-
- LS_Samuel_Chase,
- LST_Mk_II,
- LCVP__Higgins_boat,
-
- Unarmed.CCKW_353,
- AirDefence.AAA_Bofors_40mm,
- ], "shorad":[
- AirDefence.AAA_Bofors_40mm,
- ],"requirements":{
- "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/",
- },
- "objects": WW2_ALLIES_BUILDINGS,
- "doctrine": WWII_DOCTRINE,
- "boat": ["WW2LSTGroupGenerator"],
- "boat_count": 2
-}
\ No newline at end of file
diff --git a/game/factions/usa_1955.py b/game/factions/usa_1955.py
deleted file mode 100644
index efa0af47..00000000
--- a/game/factions/usa_1955.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from dcs.vehicles import *
-from dcs.ships import *
-from dcs.planes import *
-from dcs.helicopters import *
-
-USA_1955 = {
- "country": "USA",
- "side": "blue",
- "units": [
- F_86F_Sabre,
- P_51D,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- Armor.MT_M4A4_Sherman_Firefly,
- Armor.MT_M4_Sherman,
- Armor.MBT_M60A3_Patton,
- Armor.APC_M2A1,
- Armor.M30_Cargo_Carrier,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
-
- AirDefence.AAA_Bofors_40mm,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ]
-}
\ No newline at end of file
diff --git a/game/factions/usa_1960.py b/game/factions/usa_1960.py
deleted file mode 100644
index e2b64bd1..00000000
--- a/game/factions/usa_1960.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from dcs.vehicles import *
-from dcs.ships import *
-from dcs.planes import *
-from dcs.helicopters import *
-
-USA_1960 = {
- "country": "USA",
- "side": "blue",
- "units": [
- F_86F_Sabre,
- P_51D,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.AAA_Vulcan_M163,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad":[
- AirDefence.AAA_Vulcan_M163
- ]
-}
\ No newline at end of file
diff --git a/game/factions/usa_1965.py b/game/factions/usa_1965.py
deleted file mode 100644
index 59dc651a..00000000
--- a/game/factions/usa_1965.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-USA_1965 = {
- "country": "USA",
- "side": "blue",
- "units": [
-
- F_5E_3,
- F_4E,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- B_52H,
-
- UH_1H,
-
- Armor.MBT_M60A3_Patton,
- Armor.APC_M113,
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Chaparral_M48,
- AirDefence.SAM_Hawk_PCP,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ],
- "shorad":[
- AirDefence.AAA_Vulcan_M163,
- AirDefence.SAM_Chaparral_M48
- ], "boat":[
- ]
-}
\ No newline at end of file
diff --git a/game/factions/usa_1990.py b/game/factions/usa_1990.py
deleted file mode 100644
index df2b37df..00000000
--- a/game/factions/usa_1990.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-USA_1990 = {
- "country": "USA",
- "side": "blue",
- "units": [
- F_15C,
- F_15E,
- F_14B,
- FA_18C_hornet,
- F_16C_50,
-
- A_10A,
- AV8BNA,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_64A,
-
- Armor.MBT_M1A2_Abrams,
- Armor.IFV_LAV_25,
- Armor.APC_M1043_HMMWV_Armament,
- Armor.ATGM_M1045_HMMWV_TOW,
- Armor.ATGM_M1134_Stryker,
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad":[
- AirDefence.SAM_Avenger_M1097,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- Oliver_Hazzard_Perry_class,
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-72 Abraham Lincoln",
- "CVN-73 Georges Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat":[
- "ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
- ], "has_jtac": True
-}
\ No newline at end of file
diff --git a/game/factions/usa_2005.py b/game/factions/usa_2005.py
deleted file mode 100644
index d6b63a58..00000000
--- a/game/factions/usa_2005.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from dcs.helicopters import *
-from dcs.planes import *
-from dcs.ships import *
-from dcs.vehicles import *
-
-USA_2005 = {
- "country": "USA",
- "side": "blue",
- "units": [
- F_15C,
- F_15E,
- F_14B,
- FA_18C_hornet,
- F_16C_50,
- A_10C,
- A_10C_2,
- AV8BNA,
- MQ_9_Reaper,
-
- KC_135,
- KC130,
- C_130,
- E_3A,
-
- UH_1H,
- AH_64D,
-
- Armor.MBT_M1A2_Abrams,
- Armor.ATGM_M1134_Stryker,
- Armor.APC_M1126_Stryker_ICV,
- Armor.IFV_M2A2_Bradley,
- Armor.IFV_LAV_25,
- Armor.APC_M1043_HMMWV_Armament,
- Armor.ATGM_M1045_HMMWV_TOW,
-
- Artillery.MLRS_M270,
- Artillery.SPH_M109_Paladin,
-
- Unarmed.Transport_M818,
- Infantry.Infantry_M4,
- Infantry.Soldier_M249,
-
- AirDefence.SAM_Hawk_PCP,
- AirDefence.SAM_Patriot_EPP_III,
-
- CVN_74_John_C__Stennis,
- LHA_1_Tarawa,
- Armed_speedboat,
- ], "shorad": [
- AirDefence.SAM_Avenger_M1097,
- ], "aircraft_carrier": [
- CVN_74_John_C__Stennis,
- ], "helicopter_carrier": [
- LHA_1_Tarawa,
- ], "destroyer": [
- USS_Arleigh_Burke_IIa,
- ], "cruiser": [
- Ticonderoga_class,
- ], "carrier_names": [
- "CVN-71 Theodore Roosevelt",
- "CVN-72 Abraham Lincoln",
- "CVN-73 George Washington",
- "CVN-74 John C. Stennis",
- ], "lhanames": [
- "LHA-1 Tarawa",
- "LHA-2 Saipan",
- "LHA-3 Belleau Wood",
- "LHA-4 Nassau",
- "LHA-5 Peleliu"
- ], "boat":[
- "ArleighBurkeGroupGenerator"
- ], "has_jtac": True
-}
diff --git a/game/game.py b/game/game.py
index 0d8b9682..7308a128 100644
--- a/game/game.py
+++ b/game/game.py
@@ -1,11 +1,36 @@
-from datetime import datetime, timedelta
+import logging
+import math
+import random
+import sys
+from datetime import date, datetime, timedelta
+from typing import Dict, List
-from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
+from dcs.action import Coalition
+from dcs.mapping import Point
+from dcs.task import CAP, CAS, PinpointStrike, Task
+from dcs.unittype import UnitType
+from dcs.vehicles import AirDefence
+
+from game import db
+from game.db import PLAYER_BUDGET_BASE, REWARDS
+from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
-from gen.flights.ai_flight_planner import FlightPlanner
+from game.plugins import LuaPluginManager
+from gen.ato import AirTaskingOrder
+from gen.conflictgen import Conflict
+from gen.flights.ai_flight_planner import CoalitionMissionPlanner
+from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner import GroundPlanner
-from .event import *
+from theater import ConflictTheater, ControlPoint
+from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
+from . import persistency
+from .debriefing import Debriefing
+from .event.event import Event, UnitsDeliveryEvent
+from .event.frontlineattack import FrontlineAttackEvent
+from .factions.faction import Faction
+from .infos.information import Information
from .settings import Settings
+from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5
@@ -45,41 +70,45 @@ PLAYER_BUDGET_IMPORTANCE_LOG = 2
class Game:
- settings = None # type: Settings
- budget = PLAYER_BUDGET_INITIAL
- events = None # type: typing.List[Event]
- pending_transfers = None # type: typing.Dict[]
- ignored_cps = None # type: typing.Collection[ControlPoint]
- turn = 0
- game_stats: GameStats = None
-
- current_unit_id = 0
- current_group_id = 0
-
- def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, settings):
+ def __init__(self, player_name: str, enemy_name: str,
+ theater: ConflictTheater, start_date: datetime,
+ settings: Settings):
self.settings = settings
- self.events = []
+ self.events: List[Event] = []
self.theater = theater
self.player_name = player_name
- self.player_country = db.FACTIONS[player_name]["country"]
+ self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name
- self.enemy_country = db.FACTIONS[enemy_name]["country"]
+ self.enemy_country = db.FACTIONS[enemy_name].country
self.turn = 0
- self.date = datetime(start_date.year, start_date.month, start_date.day)
+ self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
- self.planners = {}
- self.ground_planners = {}
+ self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points = self.compute_conflicts_position()
- self.__frontlineData = []
- self.__destroyed_units = []
- self.jtacs = []
+ self.__destroyed_units: List[str] = []
self.savepath = ""
+ self.budget = PLAYER_BUDGET_INITIAL
+ self.current_unit_id = 0
+ self.current_group_id = 0
+
+ self.conditions = self.generate_conditions()
+
+ self.blue_ato = AirTaskingOrder()
+ self.red_ato = AirTaskingOrder()
+
+ self.aircraft_inventory = GlobalAircraftInventory(
+ self.theater.controlpoints
+ )
self.sanitize_sides()
+ self.on_load()
+ def generate_conditions(self) -> Conditions:
+ return Conditions.generate(self.theater, self.date,
+ self.current_turn_time_of_day, self.settings)
def sanitize_sides(self):
"""
@@ -95,11 +124,11 @@ class Game:
self.enemy_country = "Russia"
@property
- def player_faction(self):
+ def player_faction(self) -> Faction:
return db.FACTIONS[self.player_name]
@property
- def enemy_faction(self):
+ def enemy_faction(self) -> Faction:
return db.FACTIONS[self.enemy_name]
def _roll(self, prob, mult):
@@ -113,32 +142,10 @@ class Game:
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
def _generate_events(self):
- for player_cp, enemy_cp in self.theater.conflicts(True):
- self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp)
-
- def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
- importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
-
- if for_task == AirDefence and not self.settings.sams:
- return [x for x in db.find_unittype(AirDefence, self.enemy_name) if x not in db.SAM_BAN]
- else:
- return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy_name)
-
- def _commision_units(self, cp: ControlPoint):
- for for_task in [CAS, CAP, AirDefence]:
- limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance,
- COMMISION_LIMITS_SCALE) * self.settings.multiplier
- missing_units = limit - cp.base.total_units(for_task)
- if missing_units > 0:
- awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance,
- COMMISION_AMOUNTS_SCALE) * self.settings.multiplier
- points_to_spend = cp.base.append_commision_points(for_task, awarded_points)
- if points_to_spend > 0:
- unittypes = self.commision_unit_types(cp, for_task)
- if len(unittypes) > 0:
- d = {random.choice(unittypes): points_to_spend}
- logging.info("Commision {}: {}".format(cp, d))
- cp.base.commision_units(d)
+ for front_line in self.theater.conflicts(True):
+ self._generate_player_event(FrontlineAttackEvent,
+ front_line.control_point_a,
+ front_line.control_point_b)
@property
def budget_reward_amount(self):
@@ -194,11 +201,20 @@ class Game:
else:
return event and event.name and event.name == self.player_name
- def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
+ def on_load(self) -> None:
+ LuaPluginManager.load_settings(self.settings)
+ ObjectiveDistanceCache.set_theater(self.theater)
+ # Save game compatibility.
+
+ # TODO: Remove in 2.3.
+ if not hasattr(self, "conditions"):
+ self.conditions = self.generate_conditions()
+
+ def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
- self.turn = self.turn + 1
+ self.turn += 1
for event in self.events:
if self.settings.version == "dev":
@@ -219,34 +235,37 @@ class Game:
if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
- self.ignored_cps = []
- if ignored_cps:
- self.ignored_cps = ignored_cps
+ self.conditions = self.generate_conditions()
- self.events = [] # type: typing.List[Event]
+ self.initialize_turn()
+
+ # Autosave progress
+ persistency.autosave(self)
+
+ def initialize_turn(self) -> None:
+ self.events = []
self._generate_events()
# Update statistics
self.game_stats.update(self)
+ self.aircraft_inventory.reset()
+ for cp in self.theater.controlpoints:
+ self.aircraft_inventory.set_from_control_point(cp)
+
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
- self.planners = {}
self.ground_planners = {}
+ self.blue_ato.clear()
+ self.red_ato.clear()
+ CoalitionMissionPlanner(self, is_player=True).plan_missions()
+ CoalitionMissionPlanner(self, is_player=False).plan_missions()
for cp in self.theater.controlpoints:
- if cp.has_runway():
- planner = FlightPlanner(cp, self)
- planner.plan_flights()
- self.planners[cp.id] = planner
-
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
- # Autosave progress
- persistency.autosave(self)
-
def _enemy_reinforcement(self):
"""
Compute and commision reinforcement for enemy bases
@@ -274,7 +293,7 @@ class Game:
potential_cp_armor = self.theater.enemy_points()
i = 0
- potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if u in db.UNIT_BY_TASK[PinpointStrike]]
+ potential_units = db.FACTIONS[self.enemy_name].frontline_units
print("Enemy Recruiting")
print(potential_cp_armor)
@@ -300,8 +319,9 @@ class Game:
if budget_for_armored_units > 0:
budget_for_aircraft += budget_for_armored_units
- potential_units = [u for u in db.FACTIONS[self.enemy_name]["units"] if
- u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
+ potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
+ if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
+
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
while budget_for_aircraft > 0:
i = i + 1
@@ -319,11 +339,11 @@ class Game:
self.informations.append(info)
@property
- def current_turn_daytime(self):
- return ["dawn", "day", "dusk", "night"][self.turn % 4]
+ def current_turn_time_of_day(self) -> TimeOfDay:
+ return list(TimeOfDay)[self.turn % 4]
@property
- def current_day(self):
+ def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self):
@@ -348,10 +368,13 @@ class Game:
points = []
# By default, use the existing frontline conflict position
- for conflict in self.theater.conflicts():
- points.append(Conflict.frontline_position(self.theater, conflict[0], conflict[1])[0])
- points.append(conflict[0].position)
- points.append(conflict[1].position)
+ for front_line in self.theater.conflicts():
+ position = Conflict.frontline_position(self.theater,
+ front_line.control_point_a,
+ front_line.control_point_b)
+ points.append(position[0])
+ points.append(front_line.control_point_a.position)
+ points.append(front_line.control_point_b.position)
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
@@ -400,6 +423,13 @@ class Game:
return False
return True
+ def get_culling_points(self):
+ """
+ Check culling points
+ :return: List of culling points
+ """
+ return self.__culling_points
+
# 1 = red, 2 = blue
def get_player_coalition_id(self):
return 2
@@ -408,10 +438,10 @@ class Game:
return 1
def get_player_coalition(self):
- return dcs.action.Coalition.Blue
+ return Coalition.Blue
def get_enemy_coalition(self):
- return dcs.action.Coalition.Red
+ return Coalition.Red
def get_player_color(self):
return "blue"
diff --git a/game/inventory.py b/game/inventory.py
new file mode 100644
index 00000000..89f5afa1
--- /dev/null
+++ b/game/inventory.py
@@ -0,0 +1,123 @@
+"""Inventory management APIs."""
+from collections import defaultdict
+from typing import Dict, Iterable, Iterator, Set, Tuple
+
+from dcs.unittype import UnitType
+
+from gen.flights.flight import Flight
+from theater import ControlPoint
+
+
+class ControlPointAircraftInventory:
+ """Aircraft inventory for a single control point."""
+
+ def __init__(self, control_point: ControlPoint) -> None:
+ self.control_point = control_point
+ self.inventory: Dict[UnitType, int] = defaultdict(int)
+
+ def add_aircraft(self, aircraft: UnitType, count: int) -> None:
+ """Adds aircraft to the inventory.
+
+ Args:
+ aircraft: The type of aircraft to add.
+ count: The number of aircraft to add.
+ """
+ self.inventory[aircraft] += count
+
+ def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
+ """Removes aircraft from the inventory.
+
+ Args:
+ aircraft: The type of aircraft to remove.
+ count: The number of aircraft to remove.
+
+ Raises:
+ ValueError: The control point cannot fulfill the requested number of
+ aircraft.
+ """
+ available = self.inventory[aircraft]
+ if available < count:
+ raise ValueError(
+ f"Cannot remove {count} {aircraft.id} from "
+ f"{self.control_point.name}. Only have {available}."
+ )
+ self.inventory[aircraft] -= count
+
+ def available(self, aircraft: UnitType) -> int:
+ """Returns the number of available aircraft of the given type.
+
+ Args:
+ aircraft: The type of aircraft to query.
+ """
+ try:
+ return self.inventory[aircraft]
+ except KeyError:
+ return 0
+
+ @property
+ def types_available(self) -> Iterator[UnitType]:
+ """Iterates over all available aircraft types."""
+ for aircraft, count in self.inventory.items():
+ if count > 0:
+ yield aircraft
+
+ @property
+ def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
+ """Iterates over all available aircraft types, including amounts."""
+ for aircraft, count in self.inventory.items():
+ if count > 0:
+ yield aircraft, count
+
+ def clear(self) -> None:
+ """Clears all aircraft from the inventory."""
+ self.inventory.clear()
+
+
+class GlobalAircraftInventory:
+ """Game-wide aircraft inventory."""
+ def __init__(self, control_points: Iterable[ControlPoint]) -> None:
+ self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
+ cp: ControlPointAircraftInventory(cp) for cp in control_points
+ }
+
+ def reset(self) -> None:
+ """Clears all control points and their inventories."""
+ for inventory in self.inventories.values():
+ inventory.clear()
+
+ def set_from_control_point(self, control_point: ControlPoint) -> None:
+ """Set the control point's aircraft inventory.
+
+ If the inventory for the given control point has already been set for
+ the turn, it will be overwritten.
+ """
+ inventory = self.inventories[control_point]
+ for aircraft, count in control_point.base.aircraft.items():
+ inventory.add_aircraft(aircraft, count)
+
+ def for_control_point(
+ self,
+ control_point: ControlPoint) -> ControlPointAircraftInventory:
+ """Returns the inventory specific to the given control point."""
+ return self.inventories[control_point]
+
+ @property
+ def available_types_for_player(self) -> Iterator[UnitType]:
+ """Iterates over all aircraft types available to the player."""
+ seen: Set[UnitType] = set()
+ for control_point, inventory in self.inventories.items():
+ if control_point.captured:
+ for aircraft in inventory.types_available:
+ if aircraft not in seen:
+ seen.add(aircraft)
+ yield aircraft
+
+ def claim_for_flight(self, flight: Flight) -> None:
+ """Removes aircraft from the inventory for the given flight."""
+ inventory = self.for_control_point(flight.from_cp)
+ inventory.remove_aircraft(flight.unit_type, flight.count)
+
+ def return_from_flight(self, flight: Flight) -> None:
+ """Returns a flight's aircraft to the inventory."""
+ inventory = self.for_control_point(flight.from_cp)
+ inventory.add_aircraft(flight.unit_type, flight.count)
diff --git a/game/models/game_stats.py b/game/models/game_stats.py
index 2690d861..e6d628f4 100644
--- a/game/models/game_stats.py
+++ b/game/models/game_stats.py
@@ -1,3 +1,5 @@
+from typing import List
+
class FactionTurnMetadata:
"""
Store metadata about a faction
@@ -31,10 +33,8 @@ class GameStats:
Store statistics for the current game
"""
- data_per_turn: [GameTurnMetadata] = []
-
def __init__(self):
- self.data_per_turn = []
+ self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game):
"""
diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py
index 48c5965c..4dc18dae 100644
--- a/game/operation/frontlineattack.py
+++ b/game/operation/frontlineattack.py
@@ -1,7 +1,8 @@
-from game.db import assigned_units_split
-
-from .operation import *
+from dcs.terrain.terrain import Terrain
+from gen.conflictgen import Conflict
+from .operation import Operation
+from .. import db
MAX_DISTANCE_BETWEEN_GROUPS = 12000
@@ -34,6 +35,4 @@ class FrontlineAttackOperation(Operation):
conflict=conflict)
def generate(self):
- self.briefinggen.title = "Frontline CAS"
- self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
super(FrontlineAttackOperation, self).generate()
diff --git a/game/operation/operation.py b/game/operation/operation.py
index b23c219e..0ff06ebe 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -1,30 +1,51 @@
-from typing import Set
+import logging
+import os
+from pathlib import Path
+from typing import List, Optional, Set
-from gen import *
-from gen.airfields import AIRFIELD_DATA
-from gen.beacons import load_beacons_for_terrain
-from gen.radios import RadioRegistry
-from gen.tacan import TacanRegistry
+from dcs import Mission
+from dcs.action import DoScript, DoScriptFile
+from dcs.coalition import Coalition
from dcs.countries import country_dict
from dcs.lua.parse import loads
+from dcs.mapping import Point
from dcs.terrain.terrain import Terrain
-from userdata.debriefing import *
+from dcs.translation import String
+from dcs.triggers import TriggerStart
+from dcs.unittype import UnitType
+
+from game.plugins import LuaPluginManager
+from gen import Conflict, FlightType, VisualGenerator
+from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
+from gen.airfields import AIRFIELD_DATA
+from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
+from gen.armor import GroundConflictGenerator, JtacInfo
+from gen.beacons import load_beacons_for_terrain
+from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
+from gen.environmentgen import EnvironmentGenerator
+from gen.forcedoptionsgen import ForcedOptionsGenerator
+from gen.groundobjectsgen import GroundObjectsGenerator
+from gen.kneeboard import KneeboardGenerator
+from gen.radios import RadioFrequency, RadioRegistry
+from gen.tacan import TacanRegistry
+from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
+from theater import ControlPoint
+from .. import db
+from ..debriefing import Debriefing
class Operation:
attackers_starting_position = None # type: db.StartingPosition
defenders_starting_position = None # type: db.StartingPosition
- current_mission = None # type: dcs.Mission
- regular_mission = None # type: dcs.Mission
- quick_mission = None # type: dcs.Mission
+ current_mission = None # type: Mission
+ regular_mission = None # type: Mission
+ quick_mission = None # type: Mission
conflict = None # type: Conflict
- armorgen = None # type: ArmorConflictGenerator
airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
- envgen = None # type: EnvironmentGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
@@ -43,19 +64,20 @@ class Operation:
defender_name: str,
from_cp: ControlPoint,
departure_cp: ControlPoint,
- to_cp: ControlPoint = None):
+ to_cp: ControlPoint):
self.game = game
self.attacker_name = attacker_name
- self.attacker_country = db.FACTIONS[attacker_name]["country"]
+ self.attacker_country = db.FACTIONS[attacker_name].country
self.defender_name = defender_name
- self.defender_country = db.FACTIONS[defender_name]["country"]
+ self.defender_country = db.FACTIONS[defender_name].country
print(self.defender_country, self.attacker_country)
self.from_cp = from_cp
self.departure_cp = departure_cp
self.to_cp = to_cp
self.is_quick = False
+ self.plugin_scripts: List[str] = []
- def units_of(self, country_name: str) -> typing.Collection[UnitType]:
+ def units_of(self, country_name: str) -> List[UnitType]:
return []
def is_successfull(self, debriefing: Debriefing) -> bool:
@@ -68,14 +90,13 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
- self.briefinggen = BriefingGenerator(self.current_mission,
- self.conflict, self.game)
+ # self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat?
def prepare(self, terrain: Terrain, is_quick: bool):
with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"]
- self.current_mission = dcs.Mission(terrain)
+ self.current_mission = Mission(terrain)
print(self.game.player_country)
print(country_dict[db.country_id_from_name(self.game.player_country)])
@@ -106,7 +127,74 @@ class Operation:
self.defenders_starting_position = None
else:
self.attackers_starting_position = self.departure_cp.at
- self.defenders_starting_position = self.to_cp.at
+ # TODO: Is this possible?
+ if self.to_cp is not None:
+ self.defenders_starting_position = self.to_cp.at
+ else:
+ self.defenders_starting_position = None
+
+ def inject_lua_trigger(self, contents: str, comment: str) -> None:
+ trigger = TriggerStart(comment=comment)
+ trigger.add_action(DoScript(String(contents)))
+ self.current_mission.triggerrules.triggers.append(trigger)
+
+ def bypass_plugin_script(self, mnemonic: str) -> None:
+ self.plugin_scripts.append(mnemonic)
+
+ def inject_plugin_script(self, plugin_mnemonic: str, script: str,
+ script_mnemonic: str) -> None:
+ if script_mnemonic in self.plugin_scripts:
+ logging.debug(
+ f"Skipping already loaded {script} for {plugin_mnemonic}"
+ )
+ else:
+ self.plugin_scripts.append(script_mnemonic)
+
+ plugin_path = Path("./resources/plugins", plugin_mnemonic)
+
+ script_path = Path(plugin_path, script)
+ if not script_path.exists():
+ logging.error(
+ f"Cannot find {script_path} for plugin {plugin_mnemonic}"
+ )
+ return
+
+ trigger = TriggerStart(comment=f"Load {script_mnemonic}")
+ filename = script_path.resolve()
+ fileref = self.current_mission.map_resource.add_resource_file(filename)
+ trigger.add_action(DoScriptFile(fileref))
+ self.current_mission.triggerrules.triggers.append(trigger)
+
+ def notify_info_generators(
+ self,
+ groundobjectgen: GroundObjectsGenerator,
+ airsupportgen: AirSupportConflictGenerator,
+ jtacs: List[JtacInfo],
+ airgen: AircraftConflictGenerator,
+ ):
+ """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
+ """
+ gens: List[MissionInfoGenerator] = [
+ KneeboardGenerator(self.current_mission, self.game),
+ BriefingGenerator(self.current_mission, self.game)
+ ]
+ for gen in gens:
+ for dynamic_runway in groundobjectgen.runways.values():
+ gen.add_dynamic_runway(dynamic_runway)
+
+ for tanker in airsupportgen.air_support.tankers:
+ gen.add_tanker(tanker)
+
+ if self.is_awacs_enabled:
+ for awacs in airsupportgen.air_support.awacs:
+ gen.add_awacs(awacs)
+
+ for jtac in jtacs:
+ gen.add_jtac(jtac)
+
+ for flight in airgen.flights:
+ gen.add_flight(flight)
+ gen.generate()
def generate(self):
radio_registry = RadioRegistry()
@@ -137,13 +225,9 @@ class Operation:
for frequency in unique_map_frequencies:
radio_registry.reserve(frequency)
- # Generate meteo
- envgen = EnviromentGenerator(self.current_mission, self.conflict,
- self.game)
- if self.environment_settings is None:
- self.environment_settings = envgen.generate()
- else:
- envgen.load(self.environment_settings)
+ # Set mission time and weather conditions.
+ EnvironmentGenerator(self.current_mission,
+ self.game.conditions).generate()
# Generate ground object first
@@ -185,23 +269,23 @@ class Operation:
airgen = AircraftConflictGenerator(
self.current_mission, self.conflict, self.game.settings, self.game,
radio_registry)
- for cp in self.game.theater.controlpoints:
- side = cp.captured
- if side:
- country = self.current_mission.country(self.game.player_country)
- else:
- country = self.current_mission.country(self.game.enemy_country)
- if cp.id in self.game.planners.keys():
- airgen.generate_flights(
- cp,
- country,
- self.game.planners[cp.id],
- groundobjectgen.runways
- )
+
+ airgen.generate_flights(
+ self.current_mission.country(self.game.player_country),
+ self.game.blue_ato,
+ groundobjectgen.runways
+ )
+ airgen.generate_flights(
+ self.current_mission.country(self.game.enemy_country),
+ self.game.red_ato,
+ groundobjectgen.runways
+ )
# Generate ground units on frontline everywhere
jtacs: List[JtacInfo] = []
- for player_cp, enemy_cp in self.game.theater.conflicts(True):
+ for front_line in self.game.theater.conflicts(True):
+ player_cp = front_line.control_point_a
+ enemy_cp = front_line.control_point_b
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
self.current_mission.country(self.attacker_country),
self.current_mission.country(self.defender_country),
@@ -236,126 +320,167 @@ class Operation:
if self.game.settings.perf_smoke_gen:
visualgen.generate()
- # Inject Plugins Lua Scripts
- listOfPluginsScripts = []
- plugin_file_path = Path("./resources/scripts/plugins/__plugins.lst")
- if plugin_file_path.exists():
- for line in plugin_file_path.read_text().splitlines():
- name = line.strip()
- if not name.startswith( '#' ):
- trigger = TriggerStart(comment="Load " + name)
- listOfPluginsScripts.append(name)
- fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name)
- trigger.add_action(DoScriptFile(fileref))
- self.current_mission.triggerrules.triggers.append(trigger)
- else:
- logging.info(
- f"Not loading plugins, {plugin_file_path} does not exist")
+ luaData = {}
+ luaData["AircraftCarriers"] = {}
+ luaData["Tankers"] = {}
+ luaData["AWACs"] = {}
+ luaData["JTACs"] = {}
+ luaData["TargetPoints"] = {}
- # Inject Mist Script if not done already in the plugins
- if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice
- trigger = TriggerStart(comment="Load Mist Lua framework")
- fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua")
- trigger.add_action(DoScriptFile(fileref))
- self.current_mission.triggerrules.triggers.append(trigger)
+ self.assign_channels_to_flights(airgen.flights,
+ airsupportgen.air_support)
- # Inject JSON library if not done already in the plugins
- if not "json.lua" in listOfPluginsScripts : # don't load the script twice
- trigger = TriggerStart(comment="Load JSON Lua library")
- fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua")
- trigger.add_action(DoScriptFile(fileref))
- self.current_mission.triggerrules.triggers.append(trigger)
+ for tanker in airsupportgen.air_support.tankers:
+ luaData["Tankers"][tanker.callsign] = {
+ "dcsGroupName": tanker.dcsGroupName,
+ "callsign": tanker.callsign,
+ "variant": tanker.variant,
+ "radio": tanker.freq.mhz,
+ "tacan": str(tanker.tacan.number) + tanker.tacan.band.name
+ }
- # Inject Ciribob's JTACAutoLase if not done already in the plugins
- if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice
- trigger = TriggerStart(comment="Load JTACAutoLase.lua script")
- fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua")
- trigger.add_action(DoScriptFile(fileref))
- self.current_mission.triggerrules.triggers.append(trigger)
+ if self.is_awacs_enabled:
+ for awacs in airsupportgen.air_support.awacs:
+ luaData["AWACs"][awacs.callsign] = {
+ "dcsGroupName": awacs.dcsGroupName,
+ "callsign": awacs.callsign,
+ "radio": awacs.freq.mhz
+ }
+
+ for jtac in jtacs:
+ luaData["JTACs"][jtac.callsign] = {
+ "dcsGroupName": jtac.dcsGroupName,
+ "callsign": jtac.callsign,
+ "zone": jtac.region,
+ "dcsUnit": jtac.unit_name,
+ "laserCode": jtac.code
+ }
+
+ for flight in airgen.flights:
+ if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
+ flightType = flight.flight_type.name
+ flightTarget = flight.package.target
+ if flightTarget:
+ flightTargetName = None
+ flightTargetType = None
+ if hasattr(flightTarget, 'obj_name'):
+ flightTargetName = flightTarget.obj_name
+ flightTargetType = flightType + f" TGT ({flightTarget.category})"
+ elif hasattr(flightTarget, 'name'):
+ flightTargetName = flightTarget.name
+ flightTargetType = flightType + " TGT (Airbase)"
+ luaData["TargetPoints"][flightTargetName] = {
+ "name": flightTargetName,
+ "type": flightTargetType,
+ "position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
+ }
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
- state_location = "[[" + os.path.abspath("state.json") + "]]"
+ state_location = "[[" + os.path.abspath(".") + "]]"
lua = """
- -- setting configuration table
- env.info("DCSLiberation|: setting configuration table")
-
- -- all data in this table is overridable.
- dcsLiberation = {}
-
- -- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
- dcsLiberation.installPath=""" + state_location + """
-
- -- you can override dcsLiberation.JTACAutoLase to make it use your own function ; it will be called with these parameters : ({jtac.unit_name}, {jtac.code}, {smoke}, 'vehicle') for all JTACs
- if ctld then
- dcsLiberation.JTACAutoLase=ctld.JTACAutoLase
- elseif JTACAutoLase then
- dcsLiberation.JTACAutoLase=JTACAutoLase
- end
-
- -- later, we'll add more data to the table
- --dcsLiberation.POIs = {}
- --dcsLiberation.BASEs = {}
- --dcsLiberation.JTACs = {}
- """
+-- setting configuration table
+env.info("DCSLiberation|: setting configuration table")
+
+-- all data in this table is overridable.
+dcsLiberation = {}
+
+-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
+dcsLiberation.installPath=""" + state_location + """
+
+"""
+ # Process the tankers
+ lua += """
+
+-- list the tankers generated by Liberation
+dcsLiberation.Tankers = {
+"""
+ for key in luaData["Tankers"]:
+ data = luaData["Tankers"][key]
+ dcsGroupName= data["dcsGroupName"]
+ callsign = data["callsign"]
+ variant = data["variant"]
+ tacan = data["tacan"]
+ radio = data["radio"]
+ lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
+ #lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
+ lua += "}"
+
+ # Process the AWACSes
+ lua += """
+
+-- list the AWACs generated by Liberation
+dcsLiberation.AWACs = {
+"""
+ for key in luaData["AWACs"]:
+ data = luaData["AWACs"][key]
+ dcsGroupName= data["dcsGroupName"]
+ callsign = data["callsign"]
+ radio = data["radio"]
+ lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
+ #lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
+ lua += "}"
+
+ # Process the JTACs
+ lua += """
+
+-- list the JTACs generated by Liberation
+dcsLiberation.JTACs = {
+"""
+ for key in luaData["JTACs"]:
+ data = luaData["JTACs"][key]
+ dcsGroupName= data["dcsGroupName"]
+ callsign = data["callsign"]
+ zone = data["zone"]
+ laserCode = data["laserCode"]
+ dcsUnit = data["dcsUnit"]
+ lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
+ #lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
+ lua += "}"
+
+ # Process the Target Points
+ lua += """
+
+-- list the target points generated by Liberation
+dcsLiberation.TargetPoints = {
+"""
+ for key in luaData["TargetPoints"]:
+ data = luaData["TargetPoints"][key]
+ name = data["name"]
+ pointType = data["type"]
+ positionX = data["position"]["x"]
+ positionY = data["position"]["y"]
+ lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
+ #lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
+ lua += "}"
+
+ lua += """
+
+-- list the airbases generated by Liberation
+-- dcsLiberation.Airbases = {}
+
+-- list the aircraft carriers generated by Liberation
+-- dcsLiberation.Carriers = {}
+
+-- later, we'll add more data to the table
+
+"""
+
trigger = TriggerStart(comment="Set DCS Liberation data")
trigger.add_action(DoScript(String(lua)))
self.current_mission.triggerrules.triggers.append(trigger)
- # Inject DCS-Liberation script if not done already in the plugins
- if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice
- trigger = TriggerStart(comment="Load DCS Liberation script")
- fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua")
- trigger.add_action(DoScriptFile(fileref))
- self.current_mission.triggerrules.triggers.append(trigger)
-
- # add a configuration for JTACAutoLase and start lasing for all JTACs
- smoke = "true"
- if hasattr(self.game.settings, "jtac_smoke_on"):
- if not self.game.settings.jtac_smoke_on:
- smoke = "false"
-
- lua = """
- -- setting and starting JTACs
- env.info("DCSLiberation|: setting and starting JTACs")
- """
-
- for jtac in jtacs:
- lua += f"if dcsLiberation.JTACAutoLase then dcsLiberation.JTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle') end\n"
-
- trigger = TriggerStart(comment="Start JTACs")
- trigger.add_action(DoScript(String(lua)))
- self.current_mission.triggerrules.triggers.append(trigger)
+ # Inject Plugins Lua Scripts and data
+ for plugin in LuaPluginManager.plugins():
+ if plugin.enabled:
+ plugin.inject_scripts(self)
+ plugin.inject_configuration(self)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
-
- kneeboard_generator = KneeboardGenerator(self.current_mission)
-
- for dynamic_runway in groundobjectgen.runways.values():
- self.briefinggen.add_dynamic_runway(dynamic_runway)
-
- for tanker in airsupportgen.air_support.tankers:
- self.briefinggen.add_tanker(tanker)
- kneeboard_generator.add_tanker(tanker)
-
- if self.is_awacs_enabled:
- for awacs in airsupportgen.air_support.awacs:
- self.briefinggen.add_awacs(awacs)
- kneeboard_generator.add_awacs(awacs)
-
- for jtac in jtacs:
- self.briefinggen.add_jtac(jtac)
- kneeboard_generator.add_jtac(jtac)
-
- for flight in airgen.flights:
- self.briefinggen.add_flight(flight)
- kneeboard_generator.add_flight(flight)
-
- self.briefinggen.generate()
- kneeboard_generator.generate()
+ self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
def assign_channels_to_flights(self, flights: List[FlightData],
air_support: AirSupport) -> None:
@@ -376,6 +501,7 @@ class Operation:
logging.warning(f"No aircraft data for {airframe.id}")
return
- aircraft_data.channel_allocator.assign_channels_for_flight(
- flight, air_support
- )
+ if aircraft_data.channel_allocator is not None:
+ aircraft_data.channel_allocator.assign_channels_for_flight(
+ flight, air_support
+ )
diff --git a/userdata/persistency.py b/game/persistency.py
similarity index 82%
rename from userdata/persistency.py
rename to game/persistency.py
index 9e55dda0..617274ea 100644
--- a/userdata/persistency.py
+++ b/game/persistency.py
@@ -2,8 +2,9 @@ import logging
import os
import pickle
import shutil
+from typing import Optional
-_dcs_saved_game_folder = None # type: str
+_dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str):
@@ -40,30 +41,33 @@ def restore_game():
try:
save = pickle.load(f)
return save
- except:
- logging.error("Invalid Save game")
+ except Exception:
+ logging.exception("Invalid Save game")
return None
+
def load_game(path):
with open(path, "rb") as f:
try:
save = pickle.load(f)
save.savepath = path
return save
- except:
- logging.error("Invalid Save game")
+ except Exception:
+ logging.exception("Invalid Save game")
return None
+
def save_game(game) -> bool:
try:
with open(_temporary_save_file(), "wb") as f:
pickle.dump(game, f)
shutil.copy(_temporary_save_file(), game.savepath)
return True
- except Exception as e:
- logging.error(e)
+ except Exception:
+ logging.exception("Could not save game")
return False
+
def autosave(game) -> bool:
"""
Autosave to the autosave location
@@ -74,7 +78,7 @@ def autosave(game) -> bool:
with open(_autosave_path(), "wb") as f:
pickle.dump(game, f)
return True
- except Exception as e:
- logging.error(e)
+ except Exception:
+ logging.exception("Could not save game")
return False
diff --git a/game/plugins/__init__.py b/game/plugins/__init__.py
new file mode 100644
index 00000000..2203739d
--- /dev/null
+++ b/game/plugins/__init__.py
@@ -0,0 +1,2 @@
+from .luaplugin import LuaPlugin
+from .manager import LuaPluginManager
diff --git a/game/plugins/luaplugin.py b/game/plugins/luaplugin.py
new file mode 100644
index 00000000..f48bc185
--- /dev/null
+++ b/game/plugins/luaplugin.py
@@ -0,0 +1,180 @@
+from __future__ import annotations
+
+import json
+import logging
+import textwrap
+from dataclasses import dataclass
+from pathlib import Path
+from typing import List, Optional, TYPE_CHECKING
+
+from game.settings import Settings
+
+if TYPE_CHECKING:
+ from game.operation.operation import Operation
+
+
+class LuaPluginWorkOrder:
+
+ def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
+ disable: bool) -> None:
+ self.parent_mnemonic = parent_mnemonic
+ self.filename = filename
+ self.mnemonic = mnemonic
+ self.disable = disable
+
+ def work(self, operation: Operation) -> None:
+ if self.disable:
+ operation.bypass_plugin_script(self.mnemonic)
+ else:
+ operation.inject_plugin_script(self.parent_mnemonic, self.filename,
+ self.mnemonic)
+
+
+class PluginSettings:
+ def __init__(self, identifier: str, enabled_by_default: bool) -> None:
+ self.identifier = identifier
+ self.enabled_by_default = enabled_by_default
+ self.settings = Settings()
+ self.initialize_settings()
+
+ def set_settings(self, settings: Settings):
+ self.settings = settings
+ self.initialize_settings()
+
+ def initialize_settings(self) -> None:
+ # Plugin options are saved in the game's Settings, but it's possible for
+ # plugins to change across loads. If new plugins are added or new
+ # options added to those plugins, initialize the new settings.
+ self.settings.initialize_plugin_option(self.identifier,
+ self.enabled_by_default)
+
+ @property
+ def enabled(self) -> bool:
+ return self.settings.plugin_option(self.identifier)
+
+ def set_enabled(self, enabled: bool) -> None:
+ self.settings.set_plugin_option(self.identifier, enabled)
+
+
+class LuaPluginOption(PluginSettings):
+ def __init__(self, identifier: str, name: str,
+ enabled_by_default: bool) -> None:
+ super().__init__(identifier, enabled_by_default)
+ self.name = name
+
+
+@dataclass(frozen=True)
+class LuaPluginDefinition:
+ identifier: str
+ name: str
+ present_in_ui: bool
+ enabled_by_default: bool
+ options: List[LuaPluginOption]
+ work_orders: List[LuaPluginWorkOrder]
+ config_work_orders: List[LuaPluginWorkOrder]
+
+ @classmethod
+ def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
+ data = json.loads(path.read_text())
+
+ options = []
+ for option in data.get("specificOptions"):
+ option_id = option["mnemonic"]
+ options.append(LuaPluginOption(
+ identifier=f"{name}.{option_id}",
+ name=option.get("nameInUI", name),
+ enabled_by_default=option.get("defaultValue")
+ ))
+
+ work_orders = []
+ for work_order in data.get("scriptsWorkOrders"):
+ work_orders.append(LuaPluginWorkOrder(
+ name, work_order.get("file"), work_order["mnemonic"],
+ work_order.get("disable", False)
+ ))
+ config_work_orders = []
+ for work_order in data.get("configurationWorkOrders"):
+ config_work_orders.append(LuaPluginWorkOrder(
+ name, work_order.get("file"), work_order["mnemonic"],
+ work_order.get("disable", False)
+ ))
+
+ return cls(
+ identifier=name,
+ name=data["nameInUI"],
+ present_in_ui=not data.get("skipUI", False),
+ enabled_by_default=data.get("defaultValue", False),
+ options=options,
+ work_orders=work_orders,
+ config_work_orders=config_work_orders
+ )
+
+
+class LuaPlugin(PluginSettings):
+
+ def __init__(self, definition: LuaPluginDefinition) -> None:
+ self.definition = definition
+ super().__init__(self.definition.identifier,
+ self.definition.enabled_by_default)
+
+ @property
+ def name(self) -> str:
+ return self.definition.name
+
+ @property
+ def show_in_ui(self) -> bool:
+ return self.definition.present_in_ui
+
+ @property
+ def options(self) -> List[LuaPluginOption]:
+ return self.definition.options
+
+ @classmethod
+ def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
+ try:
+ definition = LuaPluginDefinition.from_json(name, path)
+ except KeyError:
+ logging.exception("Required plugin configuration value missing")
+ return None
+
+ return cls(definition)
+
+ def set_settings(self, settings: Settings):
+ super().set_settings(settings)
+ for option in self.definition.options:
+ option.set_settings(self.settings)
+
+ def inject_scripts(self, operation: Operation) -> None:
+ for work_order in self.definition.work_orders:
+ work_order.work(operation)
+
+ def inject_configuration(self, operation: Operation) -> None:
+ # inject the plugin options
+ if self.options:
+ option_decls = []
+ for option in self.options:
+ enabled = str(option.enabled).lower()
+ name = option.identifier
+ option_decls.append(
+ f" dcsLiberation.plugins.{name} = {enabled}")
+
+ joined_options = "\n".join(option_decls)
+
+ lua = textwrap.dedent(f"""\
+ -- {self.identifier} plugin configuration.
+
+ if dcsLiberation then
+ if not dcsLiberation.plugins then
+ dcsLiberation.plugins = {{}}
+ end
+ dcsLiberation.plugins.{self.identifier} = {{}}
+ {joined_options}
+ end
+
+ """)
+
+ operation.inject_lua_trigger(
+ lua, f"{self.identifier} plugin configuration")
+
+ for work_order in self.definition.config_work_orders:
+ work_order.work(operation)
diff --git a/game/plugins/manager.py b/game/plugins/manager.py
new file mode 100644
index 00000000..19b87118
--- /dev/null
+++ b/game/plugins/manager.py
@@ -0,0 +1,50 @@
+import json
+import logging
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from game.settings import Settings
+from game.plugins.luaplugin import LuaPlugin
+
+
+class LuaPluginManager:
+ _plugins_loaded = False
+ _plugins: Dict[str, LuaPlugin] = {}
+
+ @classmethod
+ def _load_plugins(cls) -> None:
+ plugins_path = Path("resources/plugins")
+
+ path = plugins_path / "plugins.json"
+ if not path.exists():
+ raise RuntimeError(f"{path} does not exist. Cannot continue.")
+
+ logging.info(f"Reading plugins list from {path}")
+
+ data = json.loads(path.read_text())
+ for name in data:
+ plugin_path = plugins_path / name / "plugin.json"
+ if not plugin_path.exists():
+ raise RuntimeError(
+ f"Invalid plugin configuration: required plugin {name} "
+ f"does not exist at {plugin_path}")
+ logging.info(f"Loading plugin {name} from {plugin_path}")
+ plugin = LuaPlugin.from_json(name, plugin_path)
+ if plugin is not None:
+ cls._plugins[name] = plugin
+ cls._plugins_loaded = True
+
+ @classmethod
+ def _get_plugins(cls) -> Dict[str, LuaPlugin]:
+ if not cls._plugins_loaded:
+ cls._load_plugins()
+ return cls._plugins
+
+ @classmethod
+ def plugins(cls) -> List[LuaPlugin]:
+ return list(cls._get_plugins().values())
+
+ @classmethod
+ def load_settings(cls, settings: Settings) -> None:
+ for plugin in cls.plugins():
+ plugin.set_settings(settings)
diff --git a/game/settings.py b/game/settings.py
index 4566ad0f..764e5ff5 100644
--- a/game/settings.py
+++ b/game/settings.py
@@ -1,3 +1,5 @@
+from typing import Dict
+
class Settings:
@@ -24,8 +26,6 @@ class Settings:
self.sams = True # Legacy parameter do not use
self.cold_start = False # Legacy parameter do not use
self.version = None
- self.include_jtac_if_available = True
- self.jtac_smoke_on = True
# Performance oriented
self.perf_red_alert_state = True
@@ -40,4 +40,37 @@ class Settings:
self.perf_culling = False
self.perf_culling_distance = 100
+ # LUA Plugins system
+ self.plugins: Dict[str, bool] = {}
+ # Cheating
+ self.show_red_ato = False
+
+ self.never_delay_player_flights = False
+
+ @staticmethod
+ def plugin_settings_key(identifier: str) -> str:
+ return f"plugins.{identifier}"
+
+ def initialize_plugin_option(self, identifier: str,
+ default_value: bool) -> None:
+ try:
+ self.plugin_option(identifier)
+ except KeyError:
+ self.set_plugin_option(identifier, default_value)
+
+ def plugin_option(self, identifier: str) -> bool:
+ return self.plugins[self.plugin_settings_key(identifier)]
+
+ def set_plugin_option(self, identifier: str, enabled: bool) -> None:
+ self.plugins[self.plugin_settings_key(identifier)] = enabled
+
+ def __setstate__(self, state) -> None:
+ # __setstate__ is called with the dict of the object being unpickled. We
+ # can provide save compatibility for new settings options (which
+ # normally would not be present in the unpickled object) by creating a
+ # new settings object, updating it with the unpickled state, and
+ # updating our dict with that.
+ new_state = Settings().__dict__
+ new_state.update(state)
+ self.__dict__.update(new_state)
diff --git a/game/utils.py b/game/utils.py
index 34b82aa4..44652472 100644
--- a/game/utils.py
+++ b/game/utils.py
@@ -1,14 +1,14 @@
-def meter_to_feet(value_in_meter):
+def meter_to_feet(value_in_meter: float) -> int:
return int(3.28084 * value_in_meter)
-def feet_to_meter(value_in_feet):
- return int(float(value_in_feet)/3.048)
+def feet_to_meter(value_in_feet: float) -> int:
+ return int(value_in_feet / 3.28084)
-def meter_to_nm(value_in_meter):
- return int(float(value_in_meter)*0.000539957)
+def meter_to_nm(value_in_meter: float) -> int:
+ return int(value_in_meter / 1852)
-def nm_to_meter(value_in_nm):
- return int(float(value_in_nm)*1852)
\ No newline at end of file
+def nm_to_meter(value_in_nm: float) -> int:
+ return int(value_in_nm * 1852)
diff --git a/game/version.py b/game/version.py
new file mode 100644
index 00000000..b9b97187
--- /dev/null
+++ b/game/version.py
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+
+#: Current version of Liberation.
+VERSION = "2.2.0"
+if Path("buildnumber").exists():
+ with open("buildnumber", "r") as file:
+ VERSION += f"-{file.readline()}"
diff --git a/game/weather.py b/game/weather.py
new file mode 100644
index 00000000..d6775614
--- /dev/null
+++ b/game/weather.py
@@ -0,0 +1,183 @@
+from __future__ import annotations
+
+import datetime
+import logging
+import random
+from dataclasses import dataclass
+from enum import Enum
+from typing import Optional
+
+from dcs.weather import Weather as PydcsWeather, Wind
+
+from game.settings import Settings
+from theater import ConflictTheater
+
+
+class TimeOfDay(Enum):
+ Dawn = "dawn"
+ Day = "day"
+ Dusk = "dusk"
+ Night = "night"
+
+
+@dataclass(frozen=True)
+class WindConditions:
+ at_0m: Wind
+ at_2000m: Wind
+ at_8000m: Wind
+
+
+@dataclass(frozen=True)
+class Clouds:
+ base: int
+ density: int
+ thickness: int
+ precipitation: PydcsWeather.Preceptions
+
+
+@dataclass(frozen=True)
+class Fog:
+ visibility: int
+ thickness: int
+
+
+class Weather:
+ def __init__(self) -> None:
+ self.clouds = self.generate_clouds()
+ self.fog = self.generate_fog()
+ self.wind = self.generate_wind()
+
+ def generate_clouds(self) -> Optional[Clouds]:
+ raise NotImplementedError
+
+ def generate_fog(self) -> Optional[Fog]:
+ if random.randrange(5) != 0:
+ return None
+ return Fog(
+ visibility=random.randint(2500, 5000),
+ thickness=random.randint(100, 500)
+ )
+
+ def generate_wind(self) -> WindConditions:
+ raise NotImplementedError
+
+ @staticmethod
+ def random_wind(minimum: int, maximum) -> WindConditions:
+ wind_direction = random.randint(0, 360)
+ at_0m_factor = 1
+ at_2000m_factor = 2
+ at_8000m_factor = 3
+ base_wind = random.randint(minimum, maximum)
+
+ return WindConditions(
+ # Always some wind to make the smoke move a bit.
+ at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
+ at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
+ at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
+ )
+
+ @staticmethod
+ def random_cloud_base() -> int:
+ return random.randint(2000, 3000)
+
+ @staticmethod
+ def random_cloud_thickness() -> int:
+ return random.randint(100, 400)
+
+
+class ClearSkies(Weather):
+ def generate_clouds(self) -> Optional[Clouds]:
+ return None
+
+ def generate_fog(self) -> Optional[Fog]:
+ return None
+
+ def generate_wind(self) -> WindConditions:
+ return self.random_wind(0, 0)
+
+
+class Cloudy(Weather):
+ def generate_clouds(self) -> Optional[Clouds]:
+ return Clouds(
+ base=self.random_cloud_base(),
+ density=random.randint(1, 8),
+ thickness=self.random_cloud_thickness(),
+ precipitation=PydcsWeather.Preceptions.None_
+ )
+
+ def generate_wind(self) -> WindConditions:
+ return self.random_wind(0, 4)
+
+
+class Raining(Weather):
+ def generate_clouds(self) -> Optional[Clouds]:
+ return Clouds(
+ base=self.random_cloud_base(),
+ density=random.randint(5, 8),
+ thickness=self.random_cloud_thickness(),
+ precipitation=PydcsWeather.Preceptions.Rain
+ )
+
+ def generate_wind(self) -> WindConditions:
+ return self.random_wind(0, 6)
+
+
+class Thunderstorm(Weather):
+ def generate_clouds(self) -> Optional[Clouds]:
+ return Clouds(
+ base=self.random_cloud_base(),
+ density=random.randint(9, 10),
+ thickness=self.random_cloud_thickness(),
+ precipitation=PydcsWeather.Preceptions.Thunderstorm
+ )
+
+ def generate_wind(self) -> WindConditions:
+ return self.random_wind(0, 8)
+
+
+@dataclass
+class Conditions:
+ time_of_day: TimeOfDay
+ start_time: datetime.datetime
+ weather: Weather
+
+ @classmethod
+ def generate(cls, theater: ConflictTheater, day: datetime.date,
+ time_of_day: TimeOfDay, settings: Settings) -> Conditions:
+ return cls(
+ time_of_day=time_of_day,
+ start_time=cls.generate_start_time(
+ theater, day, time_of_day, settings.night_disabled
+ ),
+ weather=cls.generate_weather()
+ )
+
+ @classmethod
+ def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
+ time_of_day: TimeOfDay,
+ night_disabled: bool) -> datetime.datetime:
+ if night_disabled:
+ logging.info("Skip Night mission due to user settings")
+ time_range = {
+ TimeOfDay.Dawn: (8, 9),
+ TimeOfDay.Day: (10, 12),
+ TimeOfDay.Dusk: (12, 14),
+ TimeOfDay.Night: (14, 17),
+ }[time_of_day]
+ else:
+ time_range = theater.daytime_map[time_of_day.value]
+
+ time = datetime.time(hour=random.randint(*time_range))
+ return datetime.datetime.combine(day, time)
+
+ @classmethod
+ def generate_weather(cls) -> Weather:
+ chances = {
+ Thunderstorm: 1,
+ Raining: 20,
+ Cloudy: 60,
+ ClearSkies: 20,
+ }
+ weather_type = random.choices(list(chances.keys()),
+ weights=list(chances.values()))[0]
+ return weather_type()
diff --git a/gen/__init__.py b/gen/__init__.py
index ad11614f..6fd6547c 100644
--- a/gen/__init__.py
+++ b/gen/__init__.py
@@ -1,4 +1,3 @@
-from .aaa import *
from .aircraft import *
from .armor import *
from .airsupportgen import *
@@ -12,4 +11,3 @@ from .forcedoptionsgen import *
from .kneeboard import *
from . import naming
-
diff --git a/gen/aaa.py b/gen/aaa.py
deleted file mode 100644
index d9822202..00000000
--- a/gen/aaa.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from .conflictgen import *
-from .naming import *
-
-from dcs.mission import *
-from dcs.mission import *
-
-from .conflictgen import *
-from .naming import *
-
-DISTANCE_FACTOR = 0.5, 1
-EXTRA_AA_MIN_DISTANCE = 50000
-EXTRA_AA_MAX_DISTANCE = 150000
-EXTRA_AA_POSITION_FROM_CP = 550
-
-class ExtraAAConflictGenerator:
- def __init__(self, mission: Mission, conflict: Conflict, game, player_country: Country, enemy_country: Country):
- self.mission = mission
- self.game = game
- self.conflict = conflict
- self.player_country = player_country
- self.enemy_country = enemy_country
-
- def generate(self):
-
- for cp in self.game.theater.controlpoints:
- if cp.is_global:
- continue
-
- if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE:
- continue
-
- if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE:
- continue
-
- if cp.position.distance_to_point(self.conflict.to_cp.position) < EXTRA_AA_MIN_DISTANCE:
- continue
-
- if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MAX_DISTANCE:
- continue
-
- country_name = cp.captured and self.player_country or self.enemy_country
- position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP)
-
- self.mission.vehicle_group(
- country=self.mission.country(country_name),
- name=namegen.next_basedefense_name(),
- _type=db.EXTRA_AA[country_name],
- position=position,
- group_size=1
- )
-
diff --git a/gen/aircraft.py b/gen/aircraft.py
index fb485e0a..7c4eac80 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -1,38 +1,106 @@
+from __future__ import annotations
+
+import logging
+import random
from dataclasses import dataclass
-from typing import Type
+from datetime import timedelta
+from functools import cached_property
+from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING
from dcs import helicopters
-from dcs.action import ActivateGroup, AITaskPush, MessageToAll
-from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
+from dcs.action import AITaskPush, ActivateGroup
+from dcs.condition import CoalitionHasAirdrome, TimeAfter
+from dcs.country import Country
from dcs.flyingunit import FlyingUnit
-from dcs.helicopters import helicopter_map, UH_1H
-from dcs.terrain.terrain import Airport, NoParkingSlotError
-from dcs.triggers import TriggerOnce, Event
+from dcs.helicopters import UH_1H, helicopter_map
+from dcs.mission import Mission, StartType
+from dcs.planes import (
+ AJS37,
+ B_17G,
+ Bf_109K_4,
+ FW_190A8,
+ FW_190D9,
+ F_14B,
+ I_16,
+ JF_17,
+ Ju_88A4,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW,
+ Su_33, A_20G, Tu_22M3, B_52H,
+)
+from dcs.point import MovingPoint, PointAction
+from dcs.task import (
+ AntishipStrike,
+ AttackGroup,
+ Bombing,
+ CAP,
+ CAS,
+ ControlledTask,
+ EPLRS,
+ EngageTargets,
+ EngageTargetsInZone,
+ GroundAttack,
+ OptROE,
+ OptRTBOnBingoFuel,
+ OptRTBOnOutOfAmmo,
+ OptReactOnThreat,
+ OptRestrictAfterburner,
+ OptRestrictJettison,
+ OrbitAction,
+ PinpointStrike,
+ SEAD,
+ StartCommand,
+ Targets,
+ Task, WeaponType,
+)
+from dcs.terrain.terrain import Airport
+from dcs.translation import String
+from dcs.triggers import Event, TriggerOnce, TriggerRule
+from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
+from dcs.unittype import FlyingType, UnitType
+from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
-from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
+from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
-from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
-from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
-from .conflictgen import *
-from .naming import *
+from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
+from gen.runways import RunwayData
+from gen.conflictgen import FRONTLINE_LENGTH
+from dcs.mapping import Point
+from theater import TheaterGroundObject
+from theater.controlpoint import ControlPoint, ControlPointType
+from .conflictgen import Conflict
+from .flights.flightplan import (
+ CasFlightPlan,
+ FormationFlightPlan,
+ PatrollingFlightPlan,
+)
+from .flights.traveltime import TotEstimator
+from .naming import namegen
+from .runways import RunwayAssigner
+
+if TYPE_CHECKING:
+ from game import Game
WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500
WARM_START_ALTITUDE = 3000
WARM_START_AIRSPEED = 550
-CAP_DURATION = 30 # minutes
-
RTB_ALTITUDE = 800
RTB_DISTANCE = 5000
HELI_ALT = 500
@@ -47,6 +115,11 @@ GERMAN_WW2_CHANNEL = MHz(40)
HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
+TARGET_WAYPOINTS = (
+ FlightWaypointType.TARGET_GROUP_LOC,
+ FlightWaypointType.TARGET_POINT,
+ FlightWaypointType.TARGET_SHIP,
+ )
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
@@ -66,6 +139,8 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
allied_ww2_aircraft = [
I_16,
P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
@@ -91,6 +166,26 @@ class ChannelNamer:
return f"COMM{radio_id} Ch {channel_id}"
+class SingleRadioChannelNamer(ChannelNamer):
+ """Channel namer for the aircraft with only a single radio.
+
+ Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
+ it's not necessary for us to name the radio when naming the channel.
+ """
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ return f"Ch {channel_id}"
+
+
+class HueyChannelNamer(ChannelNamer):
+ """Channel namer for the UH-1H."""
+
+ @staticmethod
+ def channel_name(radio_id: int, channel_id: int) -> str:
+ return f"COM3 Ch {channel_id}"
+
+
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@@ -151,6 +246,9 @@ class ChannelAssignment:
class FlightData:
"""Details of a planned flight."""
+ #: The package that the flight belongs to.
+ package: Package
+
flight_type: FlightType
#: All units in the flight.
@@ -162,8 +260,8 @@ class FlightData:
#: True if this flight belongs to the player's coalition.
friendly: bool
- #: Number of minutes after mission start the flight is set to depart.
- departure_delay: int
+ #: Number of seconds after mission start the flight is set to depart.
+ departure_delay: timedelta
#: Arrival airport.
arrival: RunwayData
@@ -183,11 +281,13 @@ class FlightData:
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
- def __init__(self, flight_type: FlightType, units: List[FlyingUnit],
- size: int, friendly: bool, departure_delay: int,
- departure: RunwayData, arrival: RunwayData,
- divert: Optional[RunwayData], waypoints: List[FlightWaypoint],
+ def __init__(self, package: Package, flight_type: FlightType,
+ units: List[FlyingUnit], size: int, friendly: bool,
+ departure_delay: timedelta, departure: RunwayData,
+ arrival: RunwayData, divert: Optional[RunwayData],
+ waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None:
+ self.package = package
self.flight_type = flight_type
self.units = units
self.size = size
@@ -264,8 +364,12 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
- flight.assign_channel(
- self.intra_flight_radio_index, 1, flight.intra_flight_channel)
+ if self.intra_flight_radio_index is not None:
+ flight.assign_channel(
+ self.intra_flight_radio_index, 1, flight.intra_flight_channel)
+
+ if self.inter_flight_radio_index is None:
+ return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
@@ -307,16 +411,28 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
@dataclass(frozen=True)
-class WarthogRadioChannelAllocator(RadioChannelAllocator):
- """Preset channel allocator for the A-10C."""
+class NoOpChannelAllocator(RadioChannelAllocator):
+ """Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
- # The A-10's radio works differently than most aircraft. Doesn't seem to
- # be a way to set these from the mission editor, let alone pydcs.
pass
+@dataclass(frozen=True)
+class FarmerRadioChannelAllocator(RadioChannelAllocator):
+ """Preset channel allocator for the MiG-19P."""
+
+ def assign_channels_for_flight(self, flight: FlightData,
+ air_support: AirSupport) -> None:
+ # The Farmer only has 6 preset channels. It also only has a VHF radio,
+ # and currently our ATC data and AWACS are only in the UHF band.
+ radio_id = 1
+ flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
+ # TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
+ # TODO: Assign 2 and 3 to AWACS if it is VHF.
+
+
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
@@ -335,8 +451,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator):
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
- flight.assign_channel(radio_id, 4, flight.departure.atc)
- flight.assign_channel(radio_id, 5, flight.arrival.atc)
+ if flight.departure.atc is not None:
+ flight.assign_channel(radio_id, 4, flight.departure.atc)
+ if flight.arrival.atc is not None:
+ flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@@ -348,11 +466,14 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
air_support: AirSupport) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
- flight.assign_channel(radio_id, 2, flight.departure.atc)
- flight.assign_channel(radio_id, 3, flight.arrival.atc)
+ if flight.departure.atc is not None:
+ flight.assign_channel(radio_id, 2, flight.departure.atc)
+ if flight.arrival.atc is not None:
+ flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
+
@dataclass(frozen=True)
class AircraftData:
"""Additional aircraft data not exposed by pydcs."""
@@ -376,8 +497,10 @@ class AircraftData:
AIRCRAFT_DATA: Dict[str, AircraftData] = {
"A-10C": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
- intra_flight_radio=get_radio("AN/ARC-164"), # VHF for intraflight is not accepted anymore by DCS (see https://forums.eagle.ru/showthread.php?p=4499738)
- channel_allocator=WarthogRadioChannelAllocator()
+ # VHF for intraflight is not accepted anymore by DCS
+ # (see https://forums.eagle.ru/showthread.php?p=4499738).
+ intra_flight_radio=get_radio("AN/ARC-164"),
+ channel_allocator=NoOpChannelAllocator()
),
"AJS37": AircraftData(
@@ -446,6 +569,15 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_namer=ViperChannelNamer
),
+ "Ka-50": AircraftData(
+ inter_flight_radio=get_radio("R-800L1"),
+ intra_flight_radio=get_radio("R-800L1"),
+ # The R-800L1 doesn't have preset channels, and the other radio is for
+ # communications with FAC and ground units, which don't currently have
+ # radios assigned, so no channels to configure.
+ channel_allocator=NoOpChannelAllocator(),
+ ),
+
"M-2000C": AircraftData(
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
@@ -456,6 +588,29 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_namer=MirageChannelNamer
),
+ "MiG-15bis": AircraftData(
+ inter_flight_radio=get_radio("RSI-6K HF"),
+ intra_flight_radio=get_radio("RSI-6K HF"),
+ channel_allocator=NoOpChannelAllocator(),
+ ),
+
+ "MiG-19P": AircraftData(
+ inter_flight_radio=get_radio("RSIU-4V"),
+ intra_flight_radio=get_radio("RSIU-4V"),
+ channel_allocator=FarmerRadioChannelAllocator(),
+ channel_namer=SingleRadioChannelNamer
+ ),
+
+ "MiG-21Bis": AircraftData(
+ inter_flight_radio=get_radio("RSIU-5V"),
+ intra_flight_radio=get_radio("RSIU-5V"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1,
+ intra_flight_radio_index=1
+ ),
+ channel_namer=SingleRadioChannelNamer,
+ ),
+
"P-51D": AircraftData(
inter_flight_radio=get_radio("SCR522"),
intra_flight_radio=get_radio("SCR522"),
@@ -465,24 +620,50 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
),
channel_namer=SCR522ChannelNamer
),
+
+ "UH-1H": AircraftData(
+ inter_flight_radio=get_radio("AN/ARC-51BX"),
+ # Ideally this would use the AN/ARC-131 because that radio is supposed
+ # to be used for flight comms, but DCS won't allow it as the flight's
+ # frequency, nor will it allow the AN/ARC-134.
+ intra_flight_radio=get_radio("AN/ARC-51BX"),
+ channel_allocator=CommonRadioChannelAllocator(
+ inter_flight_radio_index=1,
+ intra_flight_radio_index=1
+ ),
+ channel_namer=HueyChannelNamer
+ )
}
+AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
- escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
-
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
- game, radio_registry: RadioRegistry):
+ game: Game, radio_registry: RadioRegistry):
self.m = mission
self.game = game
self.settings = settings
self.conflict = conflict
self.radio_registry = radio_registry
- self.escort_targets = []
self.flights: List[FlightData] = []
+ @cached_property
+ def use_client(self) -> bool:
+ """True if Client should be used instead of Player."""
+ blue_clients = self.client_slots_in_ato(self.game.blue_ato)
+ red_clients = self.client_slots_in_ato(self.game.red_ato)
+ return blue_clients + red_clients > 1
+
+ @staticmethod
+ def client_slots_in_ato(ato: AirTaskingOrder) -> int:
+ total = 0
+ for package in ato.packages:
+ for flight in package.flights:
+ total += flight.client_count
+ return total
+
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
"""Allocates an intra-flight channel to a group.
@@ -499,36 +680,31 @@ class AircraftConflictGenerator:
except KeyError:
return get_fallback_channel(airframe)
- def _start_type(self) -> StartType:
- return self.settings.cold_start and StartType.Cold or StartType.Warm
+ @staticmethod
+ def _start_type(start_type: str) -> StartType:
+ if start_type == "Runway":
+ return StartType.Runway
+ elif start_type == "Cold":
+ return StartType.Cold
+ return StartType.Warm
- def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task],
- flight: Flight, dynamic_runways: Dict[str, RunwayData]):
+ def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
+ package: Package, flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
did_load_loadout = False
unit_type = group.units[0].unit_type
if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
- override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type]
- if type(override_loadout) == dict:
+ # Clear pylons
+ for p in group.units:
+ p.pylons.clear()
- # Clear pylons
- for p in group.units:
- p.pylons.clear()
-
- # Now load loadout
- if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
- payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
- group.load_loadout(payload_name)
- did_load_loadout = True
- logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
- elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
- payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"]
- group.load_loadout(payload_name)
- did_load_loadout = True
- logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
- elif issubclass(override_loadout, MainTask):
- group.load_task_default_loadout(override_loadout)
+ # Now load loadout
+ if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
+ payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
+ group.load_loadout(payload_name)
did_load_loadout = True
+ logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
if not did_load_loadout:
group.load_task_default_loadout(for_task)
@@ -537,13 +713,12 @@ class AircraftConflictGenerator:
for unit_instance in group.units:
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
- single_client = flight.client_count == 1
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
- if single_client:
- unit.set_player()
- else:
+ if self.use_client:
unit.set_client()
+ else:
+ unit.set_player()
# Do not generate player group with late activation.
if group.late_activation:
@@ -561,64 +736,48 @@ class AircraftConflictGenerator:
# TODO: Support for different departure/arrival airfields.
cp = flight.from_cp
- fallback_runway = RunwayData(cp.full_name, runway_name="")
+ fallback_runway = RunwayData(cp.full_name, runway_heading=0,
+ runway_name="")
if cp.cptype == ControlPointType.AIRBASE:
- departure_runway = self.get_preferred_runway(flight.from_cp.airport)
+ assigner = RunwayAssigner(self.game.conditions)
+ departure_runway = assigner.get_preferred_runway(
+ flight.from_cp.airport)
elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else:
logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway
- # The first waypoint is set automatically by pydcs, so it's not in our
- # list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
- # in our FlightData.
- first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
self.flights.append(FlightData(
+ package=package,
flight_type=flight.flight_type,
units=group.units,
size=len(group.units),
friendly=flight.from_cp.captured,
- departure_delay=flight.scheduled_in,
+ # Set later.
+ departure_delay=timedelta(),
departure=departure_runway,
arrival=departure_runway,
# TODO: Support for divert airfields.
divert=None,
- waypoints=[first_point] + flight.points,
+ # Waypoints are added later, after they've had their TOTs set.
+ waypoints=[],
intra_flight_channel=channel
))
# Special case so Su 33 carrier take off
if unit_type is Su_33:
- if task is not CAP:
+ if flight.flight_type is not CAP:
for unit in group.units:
unit.fuel = Su_33.fuel_max / 2.2
else:
for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8
- def get_preferred_runway(self, airport: Airport) -> RunwayData:
- """Returns the preferred runway for the given airport.
-
- Right now we're only selecting runways based on whether or not they have
- ILS, but we could also choose based on wind conditions, or which
- direction flight plans should follow.
- """
- runways = list(RunwayData.for_pydcs_airport(airport))
- for runway in runways:
- # Prefer any runway with ILS.
- if runway.ils is not None:
- return runway
- # Otherwise we lack the mission information to pick more usefully,
- # so just use the first runway.
- return runways[0]
-
- def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup:
+ def _generate_at_airport(self, name: str, side: Country,
+ unit_type: FlyingType, count: int, start_type: str,
+ airport: Optional[Airport] = None) -> FlyingGroup:
assert count > 0
- assert unit is not None
-
- if start_type is None:
- start_type = self._start_type()
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
return self.m.flight_group_from_airport(
@@ -627,13 +786,12 @@ class AircraftConflictGenerator:
aircraft_type=unit_type,
airport=airport,
maintask=None,
- start_type=start_type,
+ start_type=self._start_type(start_type),
group_size=count,
parking_slots=None)
- def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
+ def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
assert count > 0
- assert unit is not None
if unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
@@ -654,18 +812,15 @@ class AircraftConflictGenerator:
altitude=alt,
speed=speed,
maintask=None,
- start_type=self._start_type(),
group_size=count)
group.points[0].alt_type = "RADIO"
return group
- def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup], start_type=None) -> FlyingGroup:
+ def _generate_at_group(self, name: str, side: Country,
+ unit_type: FlyingType, count: int, start_type: str,
+ at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
assert count > 0
- assert unit is not None
-
- if start_type is None:
- start_type = self._start_type()
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
return self.m.flight_group_from_unit(
@@ -674,41 +829,17 @@ class AircraftConflictGenerator:
aircraft_type=unit_type,
pad_group=at,
maintask=None,
- start_type=start_type,
+ start_type=self._start_type(start_type),
group_size=count)
- def _generate_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: db.StartingPosition):
- if isinstance(at, Point):
- return self._generate_inflight(name, side, unit_type, count, client_count, at)
- elif isinstance(at, Group):
- takeoff_ban = unit_type in db.CARRIER_TAKEOFF_BAN
- ai_ban = client_count == 0 and self.settings.only_player_takeoff
-
- if not takeoff_ban and not ai_ban:
- return self._generate_at_group(name, side, unit_type, count, client_count, at)
- else:
- return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
- elif issubclass(at, Airport):
- takeoff_ban = unit_type in db.TAKEOFF_BAN
- ai_ban = client_count == 0 and self.settings.only_player_takeoff
-
- if not takeoff_ban and not ai_ban:
- try:
- return self._generate_at_airport(name, side, unit_type, count, client_count, at)
- except NoParkingSlotError:
- logging.info("No parking slot found at " + at.name + ", switching to air start.")
- pass
- return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
- else:
- assert False
-
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
point = group.add_waypoint(position, altitude, airspeed)
point.alt_type = "RADIO"
return point
- def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None):
- if not at:
+ def _rtb_for(self, group: FlyingGroup, cp: ControlPoint,
+ at: Optional[db.StartingPosition] = None):
+ if at is None:
at = cp.at
position = at if isinstance(at, Point) else at.position
@@ -751,259 +882,654 @@ class AircraftConflictGenerator:
else:
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
-
- def generate_flights(self, cp, country, flight_planner: FlightPlanner,
- dynamic_runways: Dict[str, RunwayData]):
- # Clear pydcs parking slots
- if cp.airport is not None:
- logging.info("CLEARING SLOTS @ " + cp.airport.name)
- logging.info("===============")
+ def clear_parking_slots(self) -> None:
+ for cp in self.game.theater.controlpoints:
if cp.airport is not None:
- for ps in cp.airport.parking_slots:
- logging.info("SLOT : " + str(ps.unit_id))
- ps.unit_id = None
- logging.info("----------------")
- logging.info("===============")
+ for parking_slot in cp.airport.parking_slots:
+ parking_slot.unit_id = None
- for flight in flight_planner.flights:
+ def generate_flights(self, country, ato: AirTaskingOrder,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ self.clear_parking_slots()
- if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position):
- logging.info("Flight not generated : culled")
+ for package in ato.packages:
+ if not package.flights:
continue
- logging.info("Generating flight : " + str(flight.unit_type))
- group = self.generate_planned_flight(cp, country, flight)
- self.setup_flight_group(group, flight, flight.flight_type,
- dynamic_runways)
- self.setup_group_activation_trigger(flight, group)
+ for flight in package.flights:
+ culled = self.game.position_culled(flight.from_cp.position)
+ if flight.client_count == 0 and culled:
+ logging.info("Flight not generated: culled")
+ continue
+ logging.info(f"Generating flight: {flight.unit_type}")
+ group = self.generate_planned_flight(flight.from_cp, country,
+ flight)
+ self.setup_flight_group(group, package, flight, dynamic_runways)
+ self.create_waypoints(group, package, flight)
+ def set_activation_time(self, flight: Flight, group: FlyingGroup,
+ delay: timedelta) -> None:
+ # Note: Late activation causes the waypoint TOTs to look *weird* in the
+ # mission editor. Waypoint times will be relative to the group
+ # activation time rather than in absolute local time. A flight delayed
+ # until 09:10 when the overall mission start time is 09:00, with a join
+ # time of 09:30 will show the join time as 00:30, not 09:30.
+ group.late_activation = True
- def setup_group_activation_trigger(self, flight, group):
- if flight.scheduled_in > 0 and flight.client_count == 0:
+ activation_trigger = TriggerOnce(
+ Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
+ activation_trigger.add_condition(
+ TimeAfter(seconds=int(delay.total_seconds())))
- if flight.start_type != "In Flight" and flight.from_cp.cptype not in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
- group.late_activation = False
- group.uncontrolled = True
+ self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
+ activation_trigger.add_action(ActivateGroup(group.id))
+ self.m.triggerrules.triggers.append(activation_trigger)
- activation_trigger = TriggerOnce(Event.NoEvent, "FlightStartTrigger" + str(group.id))
- activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in * 60))
- if (flight.from_cp.cptype == ControlPointType.AIRBASE):
- if flight.from_cp.captured:
- activation_trigger.add_condition(
- CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
- else:
- activation_trigger.add_condition(
- CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
+ def set_startup_time(self, flight: Flight, group: FlyingGroup,
+ delay: timedelta) -> None:
+ # Uncontrolled causes the AI unit to spawn, but not begin startup.
+ group.uncontrolled = True
- if flight.flight_type == FlightType.INTERCEPTION:
- self.setup_interceptor_triggers(group, flight, activation_trigger)
+ activation_trigger = TriggerOnce(Event.NoEvent,
+ f"FlightStartTrigger{group.id}")
+ activation_trigger.add_condition(
+ TimeAfter(seconds=int(delay.total_seconds())))
- group.add_trigger_action(StartCommand())
- activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
+ self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
+ group.add_trigger_action(StartCommand())
+ activation_trigger.add_action(AITaskPush(group.id, len(group.tasks)))
+ self.m.triggerrules.triggers.append(activation_trigger)
- self.m.triggerrules.triggers.append(activation_trigger)
- else:
- group.late_activation = True
- activation_trigger = TriggerOnce(Event.NoEvent, "FlightLateActivationTrigger" + str(group.id))
- activation_trigger.add_condition(TimeAfter(seconds=flight.scheduled_in*60))
+ def prevent_spawn_at_hostile_airbase(self, flight: Flight,
+ trigger: TriggerRule) -> None:
+ # Prevent delayed flights from spawning at airbases if they were
+ # captured before they've spawned.
+ if flight.from_cp.cptype != ControlPointType.AIRBASE:
+ return
- if(flight.from_cp.cptype == ControlPointType.AIRBASE):
- if flight.from_cp.captured:
- activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_player_coalition_id(), flight.from_cp.id))
- else:
- activation_trigger.add_condition(CoalitionHasAirdrome(self.game.get_enemy_coalition_id(), flight.from_cp.id))
-
- if flight.flight_type == FlightType.INTERCEPTION:
- self.setup_interceptor_triggers(group, flight, activation_trigger)
-
- activation_trigger.add_action(ActivateGroup(group.id))
- self.m.triggerrules.triggers.append(activation_trigger)
-
- def setup_interceptor_triggers(self, group, flight, activation_trigger):
-
- detection_zone = self.m.triggers.add_triggerzone(flight.from_cp.position, radius=25000, hidden=False, name="ITZ")
if flight.from_cp.captured:
- activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_enemy_color(), detection_zone.id)) # TODO : support unit type in part of coalition
- activation_trigger.add_action(MessageToAll(String("WARNING : Enemy aircraft have been detected in the vicinity of " + flight.from_cp.name + ". Interceptors are taking off."), 20))
+ coalition = self.game.get_player_coalition_id()
else:
- activation_trigger.add_condition(PartOfCoalitionInZone(self.game.get_player_color(), detection_zone.id))
- activation_trigger.add_action(MessageToAll(String("WARNING : We have detected that enemy aircraft are scrambling for an interception on " + flight.from_cp.name + " airbase."), 20))
+ coalition = self.game.get_enemy_coalition_id()
+
+ trigger.add_condition(
+ CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight:Flight):
try:
- if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
- flight.start_type = "Cold"
-
if flight.start_type == "In Flight":
- group = self._generate_group(
+ group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
- client_count=0,
at=cp.position)
+ elif cp.is_fleet:
+ group_name = cp.get_carrier_group_name()
+ group = self._generate_at_group(
+ name=namegen.next_unit_name(country, cp.id, flight.unit_type),
+ side=country,
+ unit_type=flight.unit_type,
+ count=flight.count,
+ start_type=flight.start_type,
+ at=self.m.find_group(group_name))
else:
- st = StartType.Runway
- if flight.start_type == "Cold":
- st = StartType.Cold
- elif flight.start_type == "Warm":
- st = StartType.Warm
-
- if cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]:
- group_name = cp.get_carrier_group_name()
- group = self._generate_at_group(
- name=namegen.next_unit_name(country, cp.id, flight.unit_type),
- side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- client_count=0,
- at=self.m.find_group(group_name),
- start_type=st)
- else:
- group = self._generate_at_airport(
- name=namegen.next_unit_name(country, cp.id, flight.unit_type),
- side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- client_count=0,
- airport=cp.airport,
- start_type=st)
+ group = self._generate_at_airport(
+ name=namegen.next_unit_name(country, cp.id, flight.unit_type),
+ side=country,
+ unit_type=flight.unit_type,
+ count=flight.count,
+ start_type=flight.start_type,
+ airport=cp.airport)
except Exception as e:
# Generated when there is no place on Runway or on Parking Slots
logging.error(e)
logging.warning("No room on runway or parking slots. Starting from the air.")
flight.start_type = "In Flight"
- group = self._generate_group(
+ group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
- client_count=0,
at=cp.position)
group.points[0].alt = 1500
- flight.group = group
return group
-
- def setup_flight_group(self, group, flight, flight_type,
- dynamic_runways: Dict[str, RunwayData]):
-
- if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
- group.task = CAP.name
- self._setup_group(group, CAP, flight, dynamic_runways)
- # group.points[0].tasks.clear()
- group.points[0].tasks.clear()
- group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air]))
- # group.tasks.append(EngageTargets(max_distance=nm_to_meter(120), targets=[Targets.All.Air]))
- if flight.unit_type not in GUNFIGHTERS:
- group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.AAM))
- else:
- group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Cannon))
-
- elif flight_type in [FlightType.CAS, FlightType.BAI]:
- group.task = CAS.name
- self._setup_group(group, CAS, flight, dynamic_runways)
- group.points[0].tasks.clear()
- group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles]))
- group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
- group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree))
- group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.Unguided))
- group.points[0].tasks.append(OptRestrictJettison(True))
- elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
- group.task = SEAD.name
- self._setup_group(group, SEAD, flight, dynamic_runways)
- group.points[0].tasks.clear()
- group.points[0].tasks.append(NoTask())
- group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
- group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
- group.points[0].tasks.append(OptRestrictJettison(True))
- group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM))
- elif flight_type in [FlightType.STRIKE]:
- group.task = PinpointStrike.name
- self._setup_group(group, GroundAttack, flight, dynamic_runways)
- group.points[0].tasks.clear()
- group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
- group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
- group.points[0].tasks.append(OptRestrictJettison(True))
- elif flight_type in [FlightType.ANTISHIP]:
- group.task = AntishipStrike.name
- self._setup_group(group, AntishipStrike, flight, dynamic_runways)
- group.points[0].tasks.clear()
- group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
- group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
- group.points[0].tasks.append(OptRestrictJettison(True))
+ @staticmethod
+ def configure_behavior(
+ group: FlyingGroup,
+ react_on_threat: Optional[OptReactOnThreat.Values] = None,
+ roe: Optional[OptROE.Values] = None,
+ rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
+ restrict_jettison: Optional[bool] = None) -> None:
+ group.points[0].tasks.clear()
+ if react_on_threat is not None:
+ group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
+ if roe is not None:
+ group.points[0].tasks.append(OptROE(roe))
+ if restrict_jettison is not None:
+ group.points[0].tasks.append(OptRestrictJettison(restrict_jettison))
+ if rtb_winchester is not None:
+ group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
- group.points[0].tasks.append(OptRestrictAfterburner(True))
+ # Do not restrict afterburner.
+ # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
+ @staticmethod
+ def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
if hasattr(flight.unit_type, 'eplrs'):
if flight.unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
- for i, point in enumerate(flight.points):
- if not point.only_for_player or (point.only_for_player and flight.client_count > 0):
- pt = group.add_waypoint(Point(point.x, point.y), point.alt)
- if point.waypoint_type == FlightWaypointType.PATROL_TRACK:
- action = ControlledTask(OrbitAction(altitude=pt.alt, pattern=OrbitAction.OrbitPattern.RaceTrack))
- action.stop_after_duration(CAP_DURATION * 60)
- #for tgt in point.targets:
- # if hasattr(tgt, "position"):
- # engagetgt = EngageTargetsInZone(tgt.position, radius=CAP_DEFAULT_ENGAGE_DISTANCE, targets=[Targets.All.Air])
- # pt.tasks.append(engagetgt)
- elif point.waypoint_type == FlightWaypointType.LANDING_POINT:
- pt.type = "Land"
- pt.action = PointAction.Landing
- elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE:
+ def configure_cap(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = CAP.name
+ self._setup_group(group, CAP, package, flight, dynamic_runways)
- if group.units[0].unit_type == B_17G:
- if len(point.targets) > 0:
- bcenter = Point(0,0)
- for j, t in enumerate(point.targets):
- bcenter.x += t.position.x
- bcenter.y += t.position.y
- bcenter.x = bcenter.x / len(point.targets)
- bcenter.y = bcenter.y / len(point.targets)
- bombing = Bombing(bcenter)
- bombing.params["expend"] = "All"
- bombing.params["attackQtyLimit"] = False
- bombing.params["directionEnabled"] = False
- bombing.params["altitudeEnabled"] = False
- bombing.params["weaponType"] = 2032
- bombing.params["groupAttack"] = True
- pt.tasks.append(bombing)
- else:
- for j, t in enumerate(point.targets):
- print(t.position)
- pt.tasks.append(Bombing(t.position))
- if group.units[0].unit_type == JF_17 and j < 4:
- group.add_nav_target_point(t.position, "PP" + str(j + 1))
- if group.units[0].unit_type == F_14B and j == 0:
- group.add_nav_target_point(t.position, "ST")
- if group.units[0].unit_type == AJS37 and j < 9:
- group.add_nav_target_point(t.position, "M" + str(j + 1))
- elif point.waypoint_type == FlightWaypointType.INGRESS_SEAD:
+ if flight.unit_type not in GUNFIGHTERS:
+ ammo_type = OptRTBOnOutOfAmmo.Values.AAM
+ else:
+ ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
- tgroup = self.m.find_group(point.targetGroup.group_identifier)
- if tgroup is not None:
- task = AttackGroup(tgroup.id)
- task.params["expend"] = "All"
- task.params["attackQtyLimit"] = False
- task.params["directionEnabled"] = False
- task.params["altitudeEnabled"] = False
- task.params["weaponType"] = 268402702 # Guided Weapons
- task.params["groupAttack"] = True
- pt.tasks.append(task)
+ self.configure_behavior(group, rtb_winchester=ammo_type)
- for j, t in enumerate(point.targets):
- if group.units[0].unit_type == JF_17 and j < 4:
- group.add_nav_target_point(t.position, "PP" + str(j + 1))
- if group.units[0].unit_type == F_14B and j == 0:
- group.add_nav_target_point(t.position, "ST")
- if group.units[0].unit_type == AJS37 and j < 9:
- group.add_nav_target_point(t.position, "M" + str(j + 1))
+ group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
+ targets=[Targets.All.Air]))
- if pt is not None:
- pt.alt_type = point.alt_type
- pt.name = String(point.name)
+ def configure_cas(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = CAS.name
+ self._setup_group(group, CAS, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
+ restrict_jettison=True)
+ def configure_dead(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = SEAD.name
+ self._setup_group(group, SEAD, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
+ restrict_jettison=True)
+
+ def configure_sead(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = SEAD.name
+ self._setup_group(group, SEAD, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
+ restrict_jettison=True)
+
+ def configure_strike(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = GroundAttack.name
+ self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ restrict_jettison=True)
+
+ def configure_anti_ship(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = AntishipStrike.name
+ self._setup_group(group, AntishipStrike, package, flight,
+ dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ restrict_jettison=True)
+
+ def configure_escort(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ # Escort groups are actually given the CAP task so they can perform the
+ # Search Then Engage task, which we have to use instead of the Escort
+ # task for the reasons explained in JoinPointBuilder.
+ group.task = CAP.name
+ self._setup_group(group, CAP, package, flight, dynamic_runways)
+ self.configure_behavior(group, roe=OptROE.Values.OpenFire,
+ restrict_jettison=True)
+
+ def configure_unknown_task(self, group: FlyingGroup,
+ flight: Flight) -> None:
+ logging.error(f"Unhandled flight type: {flight.flight_type.name}")
+ self.configure_behavior(group)
+
+ def setup_flight_group(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ flight_type = flight.flight_type
+ if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
+ FlightType.INTERCEPTION]:
+ self.configure_cap(group, package, flight, dynamic_runways)
+ elif flight_type in [FlightType.CAS, FlightType.BAI]:
+ self.configure_cas(group, package, flight, dynamic_runways)
+ elif flight_type in [FlightType.DEAD, ]:
+ self.configure_dead(group, package, flight, dynamic_runways)
+ elif flight_type in [FlightType.SEAD, ]:
+ self.configure_sead(group, package, flight, dynamic_runways)
+ elif flight_type in [FlightType.STRIKE]:
+ self.configure_strike(group, package, flight, dynamic_runways)
+ elif flight_type in [FlightType.ANTISHIP]:
+ self.configure_anti_ship(group, package, flight, dynamic_runways)
+ elif flight_type == FlightType.ESCORT:
+ self.configure_escort(group, package, flight, dynamic_runways)
+ else:
+ self.configure_unknown_task(group, flight)
+
+ self.configure_eplrs(group, flight)
+
+ def create_waypoints(
+ self, group: FlyingGroup, package: Package, flight: Flight) -> None:
+
+ for waypoint in flight.points:
+ waypoint.tot = None
+
+ takeoff_point = FlightWaypoint.from_pydcs(group.points[0],
+ flight.from_cp)
+ self.set_takeoff_time(takeoff_point, package, flight, group)
+
+ filtered_points = [] # type: List[FlightWaypoint]
+
+ for point in flight.points:
+ if point.only_for_player and not flight.client_count:
+ continue
+ filtered_points.append(point)
+ # Only add 1 target waypoint for Viggens. This only affects player flights,
+ # the Viggen can't have more than 9 waypoints which leaves us with two target point
+ # under the current flight plans.
+ # TODO: Make this smarter, it currently selects a random unit in the group for target,
+ # this could be updated to make it pick the "best" two targets in the group.
+ if flight.unit_type is AJS37 and flight.client_count:
+ viggen_target_points = [
+ (idx, point) for idx, point in enumerate(filtered_points) if point.waypoint_type in TARGET_WAYPOINTS
+ ]
+ keep_target = viggen_target_points[random.randint(0, len(viggen_target_points) - 1)]
+ filtered_points = [
+ point for idx, point in enumerate(filtered_points) if (
+ point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
+ )
+ ]
+
+ for idx, point in enumerate(filtered_points):
+ PydcsWaypointBuilder.for_waypoint(
+ point, group, package, flight, self.m
+ ).build()
+
+ # Set here rather than when the FlightData is created so they waypoints
+ # have their TOTs set.
+ self.flights[-1].waypoints = [takeoff_point] + flight.points
self._setup_custom_payload(flight, group)
+
+ def should_delay_flight(self, flight: Flight,
+ start_time: timedelta) -> bool:
+ if start_time.total_seconds() <= 0:
+ return False
+
+ if not flight.client_count:
+ return True
+
+ return not self.settings.never_delay_player_flights
+
+ def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
+ flight: Flight, group: FlyingGroup) -> None:
+ estimator = TotEstimator(package)
+ start_time = estimator.mission_start_time(flight)
+
+ if self.should_delay_flight(flight, start_time):
+ if self.should_activate_late(flight):
+ # Late activation causes the aircraft to not be spawned
+ # until triggered.
+ self.set_activation_time(flight, group, start_time)
+ elif flight.start_type == "Cold":
+ # Setting the start time causes the AI to wait until the
+ # specified time to begin their startup sequence.
+ self.set_startup_time(flight, group, start_time)
+
+ # And setting *our* waypoint TOT causes the takeoff time to show up in
+ # the player's kneeboard.
+ waypoint.tot = estimator.takeoff_time_for_flight(flight)
+ # And finally assign it to the FlightData info so it shows correctly in
+ # the briefing.
+ self.flights[-1].departure_delay = start_time
+
+ @staticmethod
+ def should_activate_late(flight: Flight) -> bool:
+ if flight.client_count:
+ # Never delay players. Note that cold start player flights with
+ # AI members will still be marked as uncontrolled until the start
+ # trigger fires to postpone engine start.
+ #
+ # Player flights that start on the runway or in the air will start
+ # immediately, and AI flight members will not be delayed.
+ return False
+
+ if flight.start_type != "Cold":
+ # Avoid spawning aircraft in the air or on the runway until it's
+ # time for their mission. Also avoid burning through gas spawning
+ # hot aircraft hours before their takeoff time.
+ return True
+
+ if flight.from_cp.is_fleet:
+ # Carrier spawns will crowd the carrier deck, especially without
+ # super carrier.
+ # TODO: Is there enough parking on the supercarrier?
+ return True
+
+ return False
+
+
+class PydcsWaypointBuilder:
+ def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
+ package: Package, flight: Flight,
+ mission: Mission) -> None:
+ self.waypoint = waypoint
+ self.group = group
+ self.package = package
+ self.flight = flight
+ self.mission = mission
+
+ def build(self) -> MovingPoint:
+ waypoint = self.group.add_waypoint(
+ Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt)
+
+ waypoint.alt_type = self.waypoint.alt_type
+ waypoint.name = String(self.waypoint.name)
+ tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
+ if tot is not None:
+ self.set_waypoint_tot(waypoint, tot)
+ return waypoint
+
+ def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
+ self.waypoint.tot = tot
+ if not self._viggen_client_tot():
+ waypoint.ETA = int(tot.total_seconds())
+ waypoint.ETA_locked = True
+ waypoint.speed_locked = False
+
+ @classmethod
+ def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
+ package: Package, flight: Flight,
+ mission: Mission) -> PydcsWaypointBuilder:
+ builders = {
+ FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
+ FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
+ FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
+ FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
+ FlightWaypointType.JOIN: JoinPointBuilder,
+ FlightWaypointType.LANDING_POINT: LandingPointBuilder,
+ FlightWaypointType.LOITER: HoldPointBuilder,
+ FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
+ }
+ builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
+ return builder(waypoint, group, package, flight, mission)
+
+ def _viggen_client_tot(self) -> bool:
+ """Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
+ If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
+ """
+ if (
+ (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
+ (self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
+ ):
+ return True
+ else:
+ return False
+
+ def register_special_waypoints(self, targets) -> None:
+ """Create special target waypoints for various aircraft"""
+ for i, t in enumerate(targets):
+ if self.group.units[0].unit_type == JF_17 and i < 4:
+ self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
+ if self.group.units[0].unit_type == F_14B and i == 0:
+ self.group.add_nav_target_point(t.position, "ST")
+
+
+class DefaultWaypointBuilder(PydcsWaypointBuilder):
+ pass
+
+
+class HoldPointBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+ loiter = ControlledTask(OrbitAction(
+ altitude=waypoint.alt,
+ pattern=OrbitAction.OrbitPattern.Circle
+ ))
+ if not isinstance(self.flight.flight_plan, FormationFlightPlan):
+ flight_plan_type = self.flight.flight_plan.__class__.__name__
+ logging.error(
+ f"Cannot configure hold for for {self.flight} because "
+ f"{flight_plan_type} does not define a push time. AI will push "
+ "immediately and may flight unsuitable speeds."
+ )
+ return waypoint
+ push_time = self.flight.flight_plan.push_time
+ self.waypoint.departure_time = push_time
+ loiter.stop_after_time(int(push_time.total_seconds()))
+ waypoint.add_task(loiter)
+ return waypoint
+
+
+class CasIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+ if isinstance(self.flight.flight_plan, CasFlightPlan):
+ waypoint.add_task(EngageTargetsInZone(
+ position=self.flight.flight_plan.target,
+ radius=FRONTLINE_LENGTH / 2,
+ targets=[
+ Targets.All.GroundUnits.GroundVehicles,
+ Targets.All.GroundUnits.AirDefence.AAA,
+ Targets.All.GroundUnits.Infantry,
+ ])
+ )
+ else:
+ logging.error(
+ "No CAS waypoint found. Falling back to search and engage")
+ waypoint.add_task(EngageTargets(
+ max_distance=nm_to_meter(10),
+ targets=[
+ Targets.All.GroundUnits.GroundVehicles,
+ Targets.All.GroundUnits.AirDefence.AAA,
+ Targets.All.GroundUnits.Infantry,
+ ])
+ )
+ return waypoint
+
+
+class DeadIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ target_group = self.package.target
+ if isinstance(target_group, TheaterGroundObject):
+ tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
+ if tgroup is not None: # the Mission group name because of SkyNet prefixes.
+ task = AttackGroup(tgroup.id)
+ task.params["expend"] = "All"
+ task.params["attackQtyLimit"] = False
+ task.params["directionEnabled"] = False
+ task.params["altitudeEnabled"] = False
+ task.params["weaponType"] = 268402702 # Guided Weapons
+ task.params["groupAttack"] = True
+ waypoint.tasks.append(task)
+ else:
+ logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
+ self.register_special_waypoints(self.waypoint.targets)
+ return waypoint
+
+
+class SeadIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ target_group = self.package.target
+ if isinstance(target_group, TheaterGroundObject):
+ tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
+ if tgroup is not None: # the Mission group name because of SkyNet prefixes.
+ waypoint.add_task(EngageTargetsInZone(
+ position=tgroup.position,
+ radius=nm_to_meter(30),
+ targets=[
+ Targets.All.GroundUnits.AirDefence,
+ ])
+ )
+ else:
+ logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
+ self.register_special_waypoints(self.waypoint.targets)
+ return waypoint
+
+
+class StrikeIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
+ return self.build_bombing()
+ else:
+ return self.build_strike()
+
+ def build_bombing(self) -> MovingPoint:
+ waypoint = super().build()
+
+ targets = self.waypoint.targets
+ if not targets:
+ return waypoint
+
+ center = Point(0, 0)
+ for target in targets:
+ center.x += target.position.x
+ center.y += target.position.y
+ center.x /= len(targets)
+ center.y /= len(targets)
+ bombing = Bombing(center)
+ bombing.params["expend"] = "All"
+ bombing.params["attackQtyLimit"] = False
+ bombing.params["directionEnabled"] = False
+ bombing.params["altitudeEnabled"] = False
+ bombing.params["weaponType"] = WeaponType.Bombs.value
+ bombing.params["groupAttack"] = True
+ waypoint.tasks.append(bombing)
+ return waypoint
+
+ def build_strike(self) -> MovingPoint:
+ waypoint = super().build()
+ for target in self.waypoint.targets:
+
+ targets = [target]
+ # If the target type is a group of units,
+ # then target each unit in the group with a Bombing task on their position
+ # (It is not perfect, we should have an engage Group task instead,
+ # but we don't have the group ref in the model there)
+ # TODO : for building group, engage all the buildings as well
+ if isinstance(target, TheaterGroundObject):
+ if len(target.units) > 0:
+ targets = target.units
+
+ for t in targets:
+ bombing = Bombing(t.position)
+ # If there is only one target, drop all ordnance in one pass
+ if len(self.waypoint.targets) == 1 and len(targets) == 1:
+ bombing.params["expend"] = "All"
+ bombing.params["weaponType"] = WeaponType.Auto.value
+ bombing.params["groupAttack"] = True
+ waypoint.tasks.append(bombing)
+ print(bombing)
+
+ # Register special waypoints
+ self.register_special_waypoints(targets)
+ return waypoint
+
+
+class JoinPointBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+ if self.flight.flight_type == FlightType.ESCORT:
+ self.configure_escort_tasks(waypoint)
+ return waypoint
+
+ @staticmethod
+ def configure_escort_tasks(waypoint: MovingPoint) -> None:
+ # Ideally we would use the escort mission type and escort task to have
+ # the AI automatically but the AI only escorts AI flights while they are
+ # traveling between waypoints. When an AI flight performs an attack
+ # (such as attacking the mission target), AI escorts wander aimlessly
+ # until the escorted group resumes its flight plan.
+ #
+ # As such, we instead use the Search Then Engage task, which is an
+ # enroute task that causes the AI to follow their flight plan and engage
+ # enemies of the set type within a certain distance. The downside to
+ # this approach is that AI escorts are no longer related to the group
+ # they are escorting, aside from the fact that they fly a similar flight
+ # plan at the same time. With Escort, the escorts will follow the
+ # escorted group out of the area. The strike element may or may not fly
+ # directly over the target, and they may or may not require multiple
+ # attack runs. For the escort flight we must just assume a flight plan
+ # for the escort to fly. If the strike flight doesn't need to overfly
+ # the target, the escorts are needlessly going in harms way. If the
+ # strike flight needs multiple passes, the escorts may leave before the
+ # escorted aircraft do.
+ #
+ # Another possible option would be to use Search Then Engage for join ->
+ # ingress and egress -> split, but use a Search Then Engage in Zone task
+ # for the target area that is set to end on a flag flip that occurs when
+ # the strike aircraft finish their attack task.
+ #
+ # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
+ waypoint.add_task(ControlledTask(EngageTargets(
+ # TODO: From doctrine.
+ max_distance=nm_to_meter(30),
+ targets=[Targets.All.Air.Planes.Fighters]
+ )))
+
+ # We could set this task to end at the split point. pydcs doesn't
+ # currently support that task end condition though, and we don't really
+ # need it.
+
+
+class LandingPointBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+ waypoint.type = "Land"
+ waypoint.action = PointAction.Landing
+ return waypoint
+
+
+class RaceTrackBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
+ flight_plan_type = self.flight.flight_plan.__class__.__name__
+ logging.error(
+ f"Cannot create race track for {self.flight} because "
+ f"{flight_plan_type} does not define a patrol.")
+ return waypoint
+
+ racetrack = ControlledTask(OrbitAction(
+ altitude=waypoint.alt,
+ pattern=OrbitAction.OrbitPattern.RaceTrack
+ ))
+ self.set_waypoint_tot(
+ waypoint, self.flight.flight_plan.patrol_start_time)
+ racetrack.stop_after_time(
+ int(self.flight.flight_plan.patrol_end_time.total_seconds()))
+ waypoint.add_task(racetrack)
+ return waypoint
diff --git a/gen/airfields.py b/gen/airfields.py
index b7e08712..5ea5c57c 100644
--- a/gen/airfields.py
+++ b/gen/airfields.py
@@ -3,11 +3,11 @@
Remove once https://github.com/pydcs/dcs/issues/69 tracks getting the missing
data added to pydcs. Until then, missing data can be manually filled in here.
"""
-from dataclasses import dataclass, field
-import logging
-from typing import Dict, Iterator, Optional, Tuple
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Dict, Optional, Tuple
-from dcs.terrain.terrain import Airport
from .radios import MHz, RadioFrequency
from .tacan import TacanBand, TacanChannel
@@ -195,10 +195,12 @@ AIRFIELD_DATA = {
runway_length=8623,
atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)),
outer_ndb={
- "22": ("AP", MHz(443, 0)), "4": "443.00 (AN)"
+ "22": ("AP", MHz(443, 0)),
+ "04": ("AN", MHz(443)),
},
inner_ndb={
- "22": ("P", MHz(215, 0)), "4": "215.00 (N)"
+ "22": ("P", MHz(215, 0)),
+ "04": ("N", MHz(215)),
},
),
@@ -1501,61 +1503,3 @@ AIRFIELD_DATA = {
atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)),
),
}
-
-
-@dataclass(frozen=True)
-class RunwayData:
- airfield_name: str
- runway_name: str
- atc: Optional[RadioFrequency] = None
- tacan: Optional[TacanChannel] = None
- tacan_callsign: Optional[str] = None
- ils: Optional[RadioFrequency] = None
- icls: Optional[int] = None
-
- @classmethod
- def for_airfield(cls, airport: Airport, runway: str) -> "RunwayData":
- """Creates RunwayData for the given runway of an airfield.
-
- Args:
- airport: The airfield the runway belongs to.
- runway: Identifier of the runway to use. e.g. "03" or "20L".
- """
- atc: Optional[RadioFrequency] = None
- tacan: Optional[TacanChannel] = None
- tacan_callsign: Optional[str] = None
- ils: Optional[RadioFrequency] = None
- try:
- airfield = AIRFIELD_DATA[airport.name]
- if airfield.atc is not None:
- atc = airfield.atc.uhf
- else:
- atc = None
- tacan = airfield.tacan
- tacan_callsign = airfield.tacan_callsign
- ils = airfield.ils_freq(runway)
- except KeyError:
- logging.warning(f"No airfield data for {airport.name}")
- return cls(
- airfield_name=airport.name,
- runway_name=runway,
- atc=atc,
- tacan=tacan,
- tacan_callsign=tacan_callsign,
- ils=ils
- )
-
- @classmethod
- def for_pydcs_airport(cls, airport: Airport) -> Iterator["RunwayData"]:
- for runway in airport.runways:
- runway_number = runway.heading // 10
- runway_side = ["", "L", "R"][runway.leftright]
- runway_name = f"{runway_number:02}{runway_side}"
- yield cls.for_airfield(airport, runway_name)
-
- # pydcs only exposes one runway per physical runway, so to expose
- # both sides of the runway we need to generate the other.
- runway_number = ((runway.heading + 180) % 360) // 10
- runway_side = ["", "R", "L"][runway.leftright]
- runway_name = f"{runway_number:02}{runway_side}"
- yield cls.for_airfield(airport, runway_name)
diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py
index 791c80b6..97aeea1f 100644
--- a/gen/airsupportgen.py
+++ b/gen/airsupportgen.py
@@ -1,8 +1,21 @@
from dataclasses import dataclass, field
+from typing import List, Type
+from dcs.mission import Mission, StartType
+from dcs.planes import IL_78M
+from dcs.task import (
+ AWACS,
+ ActivateBeaconCommand,
+ MainTask,
+ Refueling,
+ SetImmortalCommand,
+ SetInvisibleCommand,
+)
+
+from game import db
+from .naming import namegen
from .callsigns import callsign_for_support_unit
-from .conflictgen import *
-from .naming import *
+from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
@@ -17,6 +30,7 @@ AWACS_ALT = 13000
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
+ dcsGroupName: str
callsign: str
freq: RadioFrequency
@@ -24,6 +38,7 @@ class AwacsInfo:
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
+ dcsGroupName: str
callsign: str
variant: str
freq: RadioFrequency
@@ -49,7 +64,7 @@ class AirSupportConflictGenerator:
self.tacan_registry = tacan_registry
@classmethod
- def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
+ def support_tasks(cls) -> List[Type[MainTask]]:
return [Refueling, AWACS]
def generate(self, is_awacs_enabled):
@@ -76,6 +91,7 @@ class AirSupportConflictGenerator:
speed=574,
tacanchannel=str(tacan),
)
+ tanker_group.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
@@ -102,7 +118,7 @@ class AirSupportConflictGenerator:
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
- self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan))
+ self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
if is_awacs_enabled:
try:
@@ -118,10 +134,12 @@ class AirSupportConflictGenerator:
frequency=freq.mhz,
start_type=StartType.Warm,
)
+ awacs_flight.set_frequency(freq.mhz)
+
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(AwacsInfo(
- callsign_for_support_unit(awacs_flight), freq))
+ str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
except:
print("No AWACS for faction")
\ No newline at end of file
diff --git a/gen/armor.py b/gen/armor.py
index 463d7571..5685a120 100644
--- a/gen/armor.py
+++ b/gen/armor.py
@@ -1,13 +1,40 @@
+import logging
+import random
from dataclasses import dataclass
+from typing import List
+from dcs import Mission
from dcs.action import AITaskPush
-from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess
-from dcs.triggers import TriggerOnce, Event
+from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
+from dcs.country import Country
+from dcs.mapping import Point
+from dcs.planes import MQ_9_Reaper
+from dcs.point import PointAction
+from dcs.task import (
+ AttackGroup,
+ ControlledTask,
+ EPLRS,
+ FireAtPoint,
+ GoToWaypoint,
+ Hold,
+ OrbitAction,
+ SetImmortalCommand,
+ SetInvisibleCommand,
+)
+from dcs.triggers import Event, TriggerOnce
+from dcs.unit import Vehicle
+from dcs.unittype import VehicleType
-from gen import namegen
-from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
+from game import db
+from .naming import namegen
+from gen.ground_forces.ai_ground_planner import (
+ CombatGroupRole,
+ DISTANCE_FROM_FRONTLINE,
+)
from .callsigns import callsign_for_support_unit
-from .conflictgen import *
+from .conflictgen import Conflict
+from .ground_forces.combat_stance import CombatStance
+from game.plugins import LuaPluginManager
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@@ -28,6 +55,7 @@ RANDOM_OFFSET_ATTACK = 250
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
+ dcsGroupName: str
unit_name: str
callsign: str
region: str
@@ -48,7 +76,7 @@ class GroundConflictGenerator:
self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point:
- distance = randint(
+ distance = random.randint(
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
)
@@ -112,13 +140,13 @@ class GroundConflictGenerator:
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
# Add JTAC
- if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available:
+ if self.game.player_faction.has_jtac:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper
- if "jtac_unit" in self.game.player_faction:
- utype = self.game.player_faction["jtac_unit"]
+ if self.game.player_faction.jtac_unit is not None:
+ utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
name=n,
@@ -132,7 +160,7 @@ class GroundConflictGenerator:
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
- self.jtacs.append(JtacInfo(n, callsign, frontline, str(code)))
+ self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
@@ -165,7 +193,7 @@ class GroundConflictGenerator:
heading=forward_heading,
move_formation=PointAction.OffRoad)
- for i in range(randint(3, 10)):
+ for i in range(random.randint(3, 10)):
u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
diff --git a/gen/ato.py b/gen/ato.py
new file mode 100644
index 00000000..d814e5ee
--- /dev/null
+++ b/gen/ato.py
@@ -0,0 +1,205 @@
+"""Air Tasking Orders.
+
+The classes of the Air Tasking Order (ATO) define all of the missions that have
+been planned, and which aircraft have been assigned to them. Each planned
+mission, or "package" is composed of individual flights. The package may contain
+dissimilar aircraft performing different roles, but all for the same goal. For
+example, the package to strike an enemy airfield may contain an escort flight,
+a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
+the single CAP flight.
+"""
+import logging
+from collections import defaultdict
+from dataclasses import dataclass, field
+from datetime import timedelta
+from typing import Dict, List, Optional
+
+from dcs.mapping import Point
+
+from theater.missiontarget import MissionTarget
+from .flights.flight import Flight, FlightType
+from .flights.flightplan import FormationFlightPlan
+
+
+@dataclass(frozen=True)
+class Task:
+ """The main task of a flight or package."""
+
+ #: The type of task.
+ task_type: FlightType
+
+ #: The location of the objective.
+ location: str
+
+
+@dataclass(frozen=True)
+class PackageWaypoints:
+ join: Point
+ ingress: Point
+ egress: Point
+ split: Point
+
+
+@dataclass
+class Package:
+ """A mission package."""
+
+ #: The mission target. Currently can be either a ControlPoint or a
+ #: TheaterGroundObject (non-ControlPoint map objectives).
+ target: MissionTarget
+
+ #: The set of flights in the package.
+ flights: List[Flight] = field(default_factory=list)
+
+ delay: int = field(default=0)
+
+ #: Desired TOT as an offset from mission start.
+ time_over_target: timedelta = field(default=timedelta())
+
+ waypoints: Optional[PackageWaypoints] = field(default=None)
+
+ @property
+ def formation_speed(self) -> Optional[int]:
+ """The speed of the package when in formation.
+
+ If none of the flights in the package will join a formation, this
+ returns None. This is nto uncommon, since only strike-like (strike,
+ DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
+ Others (CAP and CAS, currently) will coordinate in target timing but
+ fly their own path to the target.
+ """
+ speeds = []
+ for flight in self.flights:
+ if isinstance(flight.flight_plan, FormationFlightPlan):
+ speeds.append(flight.flight_plan.best_flight_formation_speed)
+ if not speeds:
+ return None
+ return min(speeds)
+
+ # TODO: Should depend on the type of escort.
+ # SEAD might be able to leave before CAP.
+ @property
+ def escort_start_time(self) -> Optional[timedelta]:
+ times = []
+ for flight in self.flights:
+ waypoint = flight.flight_plan.request_escort_at()
+ if waypoint is None:
+ continue
+ tot = flight.flight_plan.tot_for_waypoint(waypoint)
+ if tot is None:
+ logging.error(
+ f"{flight} requested escort at {waypoint} but that "
+ "waypoint has no TOT. It may not be escorted.")
+ continue
+ times.append(tot)
+ if times:
+ return min(times)
+ return None
+
+ @property
+ def escort_end_time(self) -> Optional[timedelta]:
+ times = []
+ for flight in self.flights:
+ waypoint = flight.flight_plan.dismiss_escort_at()
+ if waypoint is None:
+ continue
+ tot = flight.flight_plan.tot_for_waypoint(waypoint)
+ if tot is None:
+ tot = flight.flight_plan.depart_time_for_waypoint(waypoint)
+ if tot is None:
+ logging.error(
+ f"{flight} dismissed escort at {waypoint} but that "
+ "waypoint has no TOT or departure time. It may not be "
+ "escorted.")
+ continue
+ times.append(tot)
+ if times:
+ return max(times)
+ return None
+
+ def add_flight(self, flight: Flight) -> None:
+ """Adds a flight to the package."""
+ self.flights.append(flight)
+
+ def remove_flight(self, flight: Flight) -> None:
+ """Removes a flight from the package."""
+ self.flights.remove(flight)
+ if not self.flights:
+ self.waypoints = None
+
+ @property
+ def primary_task(self) -> Optional[FlightType]:
+ if not self.flights:
+ return None
+
+ flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
+ for flight in self.flights:
+ flight_counts[flight.flight_type] += 1
+
+ # The package will contain a mix of mission types, but in general we can
+ # determine the goal of the mission because some mission types are more
+ # likely to be the main task than others. For example, a package with
+ # only CAP flights is a CAP package, a flight with CAP and strike is a
+ # strike package, a flight with CAP and DEAD is a DEAD package, and a
+ # flight with strike and SEAD is an OCA/Strike package. The type of
+ # package is determined by the highest priority flight in the package.
+ task_priorities = [
+ FlightType.CAS,
+ FlightType.STRIKE,
+ FlightType.ANTISHIP,
+ FlightType.BAI,
+ FlightType.EVAC,
+ FlightType.TROOP_TRANSPORT,
+ FlightType.RECON,
+ FlightType.ELINT,
+ FlightType.DEAD,
+ FlightType.SEAD,
+ FlightType.LOGISTICS,
+ FlightType.INTERCEPTION,
+ FlightType.TARCAP,
+ FlightType.CAP,
+ FlightType.BARCAP,
+ FlightType.EWAR,
+ FlightType.ESCORT,
+ ]
+ for task in task_priorities:
+ if flight_counts[task]:
+ return task
+
+ # If we get here, our task_priorities list above is incomplete. Log the
+ # issue and return the type of *any* flight in the package.
+ some_mission = next(iter(self.flights)).flight_type
+ logging.warning(f"Unhandled mission type: {some_mission}")
+ return some_mission
+
+ @property
+ def package_description(self) -> str:
+ """Generates a package description based on flight composition."""
+ task = self.primary_task
+ if task is None:
+ return "No mission"
+ return task.name
+
+ def __hash__(self) -> int:
+ # TODO: Far from perfect. Number packages?
+ return hash(self.target.name)
+
+
+@dataclass
+class AirTaskingOrder:
+ """The entire ATO for one coalition."""
+
+ #: The set of all planned packages in the ATO.
+ packages: List[Package] = field(default_factory=list)
+
+ def add_package(self, package: Package) -> None:
+ """Adds a package to the ATO."""
+ self.packages.append(package)
+
+ def remove_package(self, package: Package) -> None:
+ """Removes a package from the ATO."""
+ self.packages.remove(package)
+
+ def clear(self) -> None:
+ """Removes all packages from the ATO."""
+ self.packages.clear()
diff --git a/gen/briefinggen.py b/gen/briefinggen.py
index 10e07001..062ee8b1 100644
--- a/gen/briefinggen.py
+++ b/gen/briefinggen.py
@@ -1,19 +1,26 @@
+"""
+Briefing generation logic
+"""
+from __future__ import annotations
import os
-from collections import defaultdict
-from dataclasses import dataclass
import random
-from typing import List
+import logging
+from dataclasses import dataclass
+from theater.frontline import FrontLine
+from typing import List, Dict, TYPE_CHECKING
+from jinja2 import Environment, FileSystemLoader, select_autoescape
-from game import db
from dcs.mission import Mission
from .aircraft import FlightData
-from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
-from .conflictgen import Conflict
+from theater import ControlPoint
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
+from .runways import RunwayData
+if TYPE_CHECKING:
+ from game import Game
@dataclass
class CommInfo:
@@ -22,19 +29,33 @@ class CommInfo:
freq: RadioFrequency
+class FrontLineInfo:
+ def __init__(self, front_line: FrontLine):
+ self.front_line: FrontLine = front_line
+ self.player_base: ControlPoint = front_line.control_point_a
+ self.enemy_base: ControlPoint = front_line.control_point_b
+ self.player_zero: bool = self.player_base.base.total_armor == 0
+ self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
+ self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
+ self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
+ self.combat_stances = CombatStance
+
class MissionInfoGenerator:
"""Base type for generators of mission information for the player.
Examples of subtypes include briefing generators, kneeboard generators, etc.
"""
- def __init__(self, mission: Mission) -> None:
+ def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission
+ self.game = game
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
+ self.frontlines: List[FrontLineInfo] = []
+ self.dynamic_runways: List[RunwayData] = []
def add_awacs(self, awacs: AwacsInfo) -> None:
"""Adds an AWACS/GCI to the mission.
@@ -77,20 +98,13 @@ class MissionInfoGenerator:
"""
self.tankers.append(tanker)
- def generate(self) -> None:
- """Generates the mission information."""
- raise NotImplementedError
+ def add_frontline(self, frontline: FrontLineInfo) -> None:
+ """Adds a frontline to the briefing
-
-class BriefingGenerator(MissionInfoGenerator):
-
- def __init__(self, mission: Mission, conflict: Conflict, game):
- super().__init__(mission)
- self.conflict = conflict
- self.game = game
- self.title = ""
- self.description = ""
- self.dynamic_runways: List[RunwayData] = []
+ Arguments:
+ frontline: Frontline conflict information
+ """
+ self.frontlines.append(frontline)
def add_dynamic_runway(self, runway: RunwayData) -> None:
"""Adds a dynamically generated runway to the briefing.
@@ -100,148 +114,51 @@ class BriefingGenerator(MissionInfoGenerator):
"""
self.dynamic_runways.append(runway)
- def add_flight_description(self, flight: FlightData):
- assert flight.client_units
-
- aircraft = flight.aircraft_type
- flight_unit_name = db.unit_type_name(aircraft)
- self.description += "-" * 50 + "\n"
- self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
-
- for i, wpt in enumerate(flight.waypoints):
- self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
- self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
-
- def add_ally_flight_description(self, flight: FlightData):
- assert not flight.client_units
- aircraft = flight.aircraft_type
- flight_unit_name = db.unit_type_name(aircraft)
- self.description += (
- f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
- f"departing in {flight.departure_delay} minutes\n"
- )
-
- def generate(self):
- self.description = ""
-
- self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
- self.description += "=" * 15 + "\n\n"
-
- self.generate_ongoing_war_text()
-
- self.description += "\n"*2
- self.description += "Your flights:" + "\n"
- self.description += "=" * 15 + "\n\n"
-
- for flight in self.flights:
- if flight.client_units:
- self.add_flight_description(flight)
-
- self.description += "\n"*2
- self.description += "Planned ally flights:" + "\n"
- self.description += "=" * 15 + "\n"
- allied_flights_by_departure = defaultdict(list)
- for flight in self.flights:
- if not flight.client_units and flight.friendly:
- name = flight.departure.airfield_name
- allied_flights_by_departure[name].append(flight)
- for departure, flights in allied_flights_by_departure.items():
- self.description += f"\nFrom {departure}\n"
- self.description += "-" * 50 + "\n\n"
- for flight in flights:
- self.add_ally_flight_description(flight)
-
- if self.comms:
- self.description += "\n\nComms Frequencies:\n"
- self.description += "=" * 15 + "\n"
- for comm_info in self.comms:
- self.description += f"{comm_info.name}: {comm_info.freq}\n"
- self.description += ("-" * 50) + "\n"
-
- for runway in self.dynamic_runways:
- self.description += f"{runway.airfield_name}\n"
- self.description += f"RADIO : {runway.atc}\n"
- if runway.tacan is not None:
- self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
- if runway.icls is not None:
- self.description += f"ICLS Channel : {runway.icls}\n"
- self.description += "-" * 50 + "\n"
+ def generate(self) -> None:
+ """Generates the mission information."""
+ raise NotImplementedError
- self.description += "JTACS [F-10 Menu] : \n"
- self.description += "===================\n\n"
- for jtac in self.jtacs:
- self.description += f"{jtac.region} -- Code : {jtac.code}\n"
+class BriefingGenerator(MissionInfoGenerator):
- self.mission.set_description_text(self.description)
+ def __init__(self, mission: Mission, game: Game):
+ super().__init__(mission, game)
+ self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
+ env = Environment(
+ loader=FileSystemLoader("resources/briefing/templates"),
+ autoescape=select_autoescape(
+ disabled_extensions=("",),
+ default_for_string=True,
+ default=True,
+ ),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+ self.template = env.get_template("briefingtemplate_EN.j2")
+ def generate(self) -> None:
+ """Generate the mission briefing
+ """
+ self._generate_frontline_info()
+ self.generate_allied_flights_by_departure()
+ self.mission.set_description_text(self.template.render(vars(self)))
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
+ def _generate_frontline_info(self) -> None:
+ """Build FrontLineInfo objects from FrontLine type and append to briefing.
+ """
+ for front_line in self.game.theater.conflicts(from_player=True):
+ self.add_frontline(FrontLineInfo(front_line))
- def generate_ongoing_war_text(self):
-
- self.description += "Current situation:\n"
- self.description += "=" * 15 + "\n\n"
-
- conflict_number = 0
-
- for c in self.game.theater.conflicts():
- conflict_number = conflict_number + 1
- if c[0].captured:
- player_base = c[0]
- enemy_base = c[1]
- else:
- player_base = c[1]
- enemy_base = c[0]
-
- has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor
- self.description += self.__random_frontline_sentence(player_base.name, enemy_base.name)
-
- if enemy_base.id in player_base.stances.keys():
- stance = player_base.stances[enemy_base.id]
-
- if player_base.base.total_armor == 0:
- self.description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n"
- elif enemy_base.base.total_armor == 0:
- self.description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n"
- if stance == CombatStance.AGGRESSIVE:
- if has_numerical_superiority:
- self.description += "On this location, our ground forces will try to make progress against the enemy"
- self.description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n"
- elif has_numerical_superiority:
- self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
- elif stance == CombatStance.ELIMINATION:
- if has_numerical_superiority:
- self.description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". "
- self.description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n"
- elif has_numerical_superiority:
- self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
- elif stance == CombatStance.BREAKTHROUGH:
- if has_numerical_superiority:
- self.description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n"
- elif has_numerical_superiority:
- self.description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n"
- elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
- if has_numerical_superiority:
- self.description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n"
- elif has_numerical_superiority:
- self.description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n"
-
- if conflict_number == 0:
- self.description += "There are currently no fights on the ground.\n"
-
- self.description += "\n\n"
-
-
- def __random_frontline_sentence(self, player_base_name, enemy_base_name):
- templates = [
- "There are combats between {} and {}. ",
- "The war on the ground is still going on between {} and {}. ",
- "Our ground forces in {} are opposed to enemy forces based in {}. ",
- "Our forces from {} are fighting enemies based in {}. ",
- "There is an active frontline between {} and {}. ",
- ]
- return random.choice(templates).format(player_base_name, enemy_base_name)
-
-
+ # TODO: This should determine if runway is friendly through a method more robust than the existing string match
+ def generate_allied_flights_by_departure(self) -> None:
+ """Create iterable to display allied flights grouped by departure airfield.
+ """
+ for flight in self.flights:
+ if not flight.client_units and flight.friendly:
+ name = flight.departure.airfield_name
+ if name in self.allied_flights_by_departure: # where else can we get this?
+ self.allied_flights_by_departure[name].append(flight)
+ else:
+ self.allied_flights_by_departure[name] = [flight]
diff --git a/gen/conflictgen.py b/gen/conflictgen.py
index 9b83b51e..3c9eecfe 100644
--- a/gen/conflictgen.py
+++ b/gen/conflictgen.py
@@ -1,21 +1,11 @@
import logging
-import typing
-import pdb
-import dcs
+import random
+from typing import Tuple
-from random import randint
-from dcs import Mission
+from dcs.country import Country
+from dcs.mapping import Point
-from dcs.mission import *
-from dcs.vehicles import *
-from dcs.unitgroup import *
-from dcs.unittype import *
-from dcs.mapping import *
-from dcs.point import *
-from dcs.task import *
-from dcs.country import *
-
-from theater import *
+from theater import ConflictTheater, ControlPoint
AIR_DISTANCE = 40000
@@ -65,24 +55,6 @@ def _heading_sum(h, a) -> int:
class Conflict:
- attackers_side = None # type: str
- defenders_side = None # type: str
- attackers_country = None # type: Country
- defenders_country = None # type: Country
- from_cp = None # type: ControlPoint
- to_cp = None # type: ControlPoint
- position = None # type: Point
- size = None # type: int
- radials = None # type: typing.List[int]
-
- heading = None # type: int
- distance = None # type: int
-
- ground_attackers_location = None # type: Point
- ground_defenders_location = None # type: Point
- air_attackers_location = None # type: Point
- air_defenders_location = None # type: Point
-
def __init__(self,
theater: ConflictTheater,
from_cp: ControlPoint,
@@ -155,7 +127,7 @@ class Conflict:
else:
return self.position
- def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> typing.Optional[Point]:
+ def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
@classmethod
@@ -163,7 +135,7 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
- def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[typing.Tuple[Point, int]]:
+ def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position)
attack_distance = from_cp.position.distance_to_point(to_cp.position)
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
@@ -174,9 +146,7 @@ class Conflict:
@classmethod
- def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]:
- initial, heading = cls.frontline_position(theater, from_cp, to_cp)
-
+ def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
"""
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
@@ -193,9 +163,6 @@ class Conflict:
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
"""
frontline = cls.frontline_position(theater, from_cp, to_cp)
- if not frontline:
- return None
-
center_position, heading = frontline
left_position, right_position = None, None
@@ -243,7 +210,7 @@ class Conflict:
"""
@classmethod
- def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]:
+ def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for _ in range(0, int(max_distance), 500):
if theater.is_on_land(pos):
@@ -302,10 +269,14 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
attackers_location = position.point_from_heading(attack_heading, distance)
- attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater)
+ attackers_location = Conflict._find_ground_position(
+ attackers_location, int(distance * 2),
+ _heading_sum(attack_heading, 180), theater)
defenders_location = position.point_from_heading(defense_heading, distance)
- defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
+ defenders_location = Conflict._find_ground_position(
+ defenders_location, int(distance * 2),
+ _heading_sum(defense_heading, 180), theater)
return cls(
position=position,
@@ -429,7 +400,7 @@ class Conflict:
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
- attack_position = position.point_from_heading(heading, randint(0, int(distance)))
+ attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
@@ -456,7 +427,9 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
defenders_location = position.point_from_heading(defense_heading, distance)
- defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
+ defenders_location = Conflict._find_ground_position(
+ defenders_location, int(distance * 2),
+ _heading_sum(defense_heading, 180), theater)
return cls(
position=position,
diff --git a/gen/defenses/armor_group_generator.py b/gen/defenses/armor_group_generator.py
index 7b772e31..c8bc472c 100644
--- a/gen/defenses/armor_group_generator.py
+++ b/gen/defenses/armor_group_generator.py
@@ -11,8 +11,7 @@ def generate_armor_group(faction:str, game, ground_object):
This generate a group of ground units
:return: Generated group
"""
-
- possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()]
+ possible_unit = [u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()]
if len(possible_unit) > 0:
unit_type = random.choice(possible_unit)
return generate_armor_group_of_type(game, ground_object, unit_type)
diff --git a/gen/environmentgen.py b/gen/environmentgen.py
index 1d861842..7712cea5 100644
--- a/gen/environmentgen.py
+++ b/gen/environmentgen.py
@@ -1,156 +1,36 @@
-import logging
-import typing
-import random
-from datetime import datetime, timedelta, time
+from typing import Optional
from dcs.mission import Mission
-from dcs.triggers import *
-from dcs.condition import *
-from dcs.action import *
-from dcs.unit import Skill
-from dcs.point import MovingPoint, PointProperties
-from dcs.action import *
-from dcs.weather import *
-from game import db
-from theater import *
-from gen import *
-
-WEATHER_CLOUD_BASE = 2000, 3000
-WEATHER_CLOUD_DENSITY = 1, 8
-WEATHER_CLOUD_THICKNESS = 100, 400
-WEATHER_CLOUD_BASE_MIN = 1600
-
-WEATHER_FOG_CHANCE = 20
-WEATHER_FOG_VISIBILITY = 2500, 5000
-WEATHER_FOG_THICKNESS = 100, 500
-
-RANDOM_TIME = {
- "night": 7,
- "dusk": 40,
- "dawn": 40,
- "day": 100,
-}
-
-RANDOM_WEATHER = {
- 1: 0, # thunderstorm
- 2: 20, # rain
- 3: 80, # clouds
- 4: 100, # clear
-}
+from game.weather import Clouds, Fog, Conditions, WindConditions
-class EnvironmentSettings:
- weather_dict = None
- start_time = None
-
-
-class EnviromentGenerator:
- def __init__(self, mission: Mission, conflict: Conflict, game):
+class EnvironmentGenerator:
+ def __init__(self, mission: Mission, conditions: Conditions) -> None:
self.mission = mission
- self.conflict = conflict
- self.game = game
+ self.conditions = conditions
- def _gen_time(self):
+ def set_clouds(self, clouds: Optional[Clouds]) -> None:
+ if clouds is None:
+ return
+ self.mission.weather.clouds_base = clouds.base
+ self.mission.weather.clouds_thickness = clouds.thickness
+ self.mission.weather.clouds_density = clouds.density
+ self.mission.weather.clouds_iprecptns = clouds.precipitation
- start_time = self.game.current_day
+ def set_fog(self, fog: Optional[Fog]) -> None:
+ if fog is None:
+ return
+ self.mission.weather.fog_visibility = fog.visibility
+ self.mission.weather.fog_thickness = fog.thickness
- daytime = self.game.current_turn_daytime
- logging.info("Mission time will be {}".format(daytime))
- if self.game.settings.night_disabled:
- logging.info("Skip Night mission due to user settings")
- if daytime == "dawn":
- time_range = (8, 9)
- elif daytime == "day":
- time_range = (10, 12)
- elif daytime == "dusk":
- time_range = (12, 14)
- elif daytime == "night":
- time_range = (14, 17)
- else:
- time_range = (10, 12)
- else:
- time_range = self.game.theater.daytime_map[daytime]
-
- start_time += timedelta(hours=random.randint(*time_range))
-
- logging.info("time - {}, slot - {}, night skipped - {}".format(
- str(start_time),
- str(time_range),
- self.game.settings.night_disabled))
-
- self.mission.start_time = start_time
-
- def _generate_wind(self, wind_speed, wind_direction=None):
- # wind
- if not wind_direction:
- wind_direction = random.randint(0, 360)
-
- self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
- self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
- self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
-
- def _generate_base_weather(self):
- # clouds
- self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
- self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
- self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
-
- # wind
- self._generate_wind(random.randint(0, 4))
-
- # fog
- if random.randint(0, 100) < WEATHER_FOG_CHANCE:
- self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY)
- self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS)
-
- def _gen_random_weather(self):
- weather_type = None
- for k, v in RANDOM_WEATHER.items():
- if random.randint(0, 100) <= v:
- weather_type = k
- break
-
- logging.info("generated weather {}".format(weather_type))
- if weather_type == 1:
- # thunderstorm
- self._generate_base_weather()
- self._generate_wind(random.randint(0, 8))
-
- self.mission.weather.clouds_density = random.randint(9, 10)
- self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm
- elif weather_type == 2:
- # rain
- self._generate_base_weather()
- self.mission.weather.clouds_density = random.randint(5, 8)
- self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain
-
- self._generate_wind(random.randint(0, 6))
- elif weather_type == 3:
- # clouds
- self._generate_base_weather()
- elif weather_type == 4:
- # clear
- pass
-
- if self.mission.weather.clouds_density > 0:
- # sometimes clouds are randomized way too low and need to be fixed
- self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
-
- if self.mission.weather.wind_at_ground.speed == 0:
- # frontline smokes look silly w/o any wind
- self._generate_wind(1)
-
- def generate(self) -> EnvironmentSettings:
- self._gen_time()
- self._gen_random_weather()
-
- settings = EnvironmentSettings()
- settings.start_time = self.mission.start_time
- settings.weather_dict = self.mission.weather.dict()
- return settings
-
- def load(self, settings: EnvironmentSettings):
- self.mission.start_time = settings.start_time
- self.mission.weather.load_from_dict(settings.weather_dict)
+ def set_wind(self, wind: WindConditions) -> None:
+ self.mission.weather.wind_at_ground = wind.at_0m
+ self.mission.weather.wind_at_2000 = wind.at_2000m
+ self.mission.weather.wind_at_8000 = wind.at_8000m
+ def generate(self):
+ self.mission.start_time = self.conditions.start_time
+ self.set_clouds(self.conditions.weather.clouds)
+ self.set_fog(self.conditions.weather.fog)
+ self.set_wind(self.conditions.weather.wind)
diff --git a/gen/fleet/carrier_group.py b/gen/fleet/carrier_group.py
index 119bc2d4..a06ebaee 100644
--- a/gen/fleet/carrier_group.py
+++ b/gen/fleet/carrier_group.py
@@ -1,34 +1,26 @@
import random
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.group_generator import ShipGroupGenerator
-class CarrierGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(CarrierGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add carrier
- if "aircraft_carrier" in self.faction.keys():
-
- if "supercarrier" in self.faction.keys() and self.game.settings.supercarrier:
- carrier_type = random.choice(self.faction["supercarrier"])
- else:
- carrier_type = random.choice(self.faction["aircraft_carrier"])
+ if len(self.faction.aircraft_carrier) > 0:
+ carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
else:
return
# Add destroyers escort
- if "destroyer" in self.faction.keys():
- dd_type = random.choice(self.faction["destroyer"])
+ if len(self.faction.destroyers) > 0:
+ dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading)
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
- self.get_generated_group().points[0].speed = 20
\ No newline at end of file
+ self.get_generated_group().points[0].speed = 20
diff --git a/gen/fleet/cn_dd_group.py b/gen/fleet/cn_dd_group.py
index efef82d2..020f68c2 100644
--- a/gen/fleet/cn_dd_group.py
+++ b/gen/fleet/cn_dd_group.py
@@ -1,15 +1,26 @@
+from __future__ import annotations
+
import random
+from typing import TYPE_CHECKING
+
+from dcs.ships import (
+ Type_052C_Destroyer,
+ Type_052B_Destroyer,
+ Type_054A_Frigate,
+ CGN_1144_2_Pyotr_Velikiy,
+)
+
+from game.factions.faction import Faction
from gen.fleet.dd_group import DDGroupGenerator
-from gen.sam.group_generator import GroupGenerator
-from dcs.ships import *
+from gen.sam.group_generator import ShipGroupGenerator
+from theater.theatergroundobject import TheaterGroundObject
+
+if TYPE_CHECKING:
+ from game.game import Game
-class ChineseNavyGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(ChineseNavyGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
@@ -38,5 +49,5 @@ class ChineseNavyGroupGenerator(GroupGenerator):
class Type54GroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)
diff --git a/gen/fleet/dd_group.py b/gen/fleet/dd_group.py
index 7c02a64b..b11de653 100644
--- a/gen/fleet/dd_group.py
+++ b/gen/fleet/dd_group.py
@@ -1,14 +1,21 @@
-import random
+from __future__ import annotations
+from typing import TYPE_CHECKING
-from gen.sam.group_generator import GroupGenerator
-from dcs.ships import *
+from game.factions.faction import Faction
+from theater.theatergroundobject import TheaterGroundObject
+
+from gen.sam.group_generator import ShipGroupGenerator
+from dcs.unittype import ShipType
+from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
+
+if TYPE_CHECKING:
+ from game.game import Game
-class DDGroupGenerator(GroupGenerator):
+class DDGroupGenerator(ShipGroupGenerator):
- def __init__(self, game, ground_object, faction, ddtype):
- super(DDGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
+ super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype
def generate(self):
@@ -18,10 +25,10 @@ class DDGroupGenerator(GroupGenerator):
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)
diff --git a/gen/fleet/lha_group.py b/gen/fleet/lha_group.py
index 8945c8f2..cfbafcbb 100644
--- a/gen/fleet/lha_group.py
+++ b/gen/fleet/lha_group.py
@@ -1,25 +1,21 @@
import random
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.group_generator import ShipGroupGenerator
-class LHAGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(LHAGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class LHAGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add carrier
- if "helicopter_carrier" in self.faction.keys():
- carrier_type = random.choice(self.faction["helicopter_carrier"])
+ if len(self.faction.helicopter_carrier) > 0:
+ carrier_type = random.choice(self.faction.helicopter_carrier)
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading)
# Add destroyers escort
- if "destroyer" in self.faction.keys():
- dd_type = random.choice(self.faction["destroyer"])
+ if len(self.faction.destroyers) > 0:
+ dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
- self.get_generated_group().points[0].speed = 20
\ No newline at end of file
+ self.get_generated_group().points[0].speed = 20
diff --git a/gen/fleet/ru_dd_group.py b/gen/fleet/ru_dd_group.py
index dc048029..0948991a 100644
--- a/gen/fleet/ru_dd_group.py
+++ b/gen/fleet/ru_dd_group.py
@@ -1,15 +1,29 @@
+from __future__ import annotations
import random
+from typing import TYPE_CHECKING
+
+from dcs.ships import (
+ FFL_1124_4_Grisha,
+ FSG_1241_1MP_Molniya,
+ FFG_11540_Neustrashimy,
+ FF_1135M_Rezky,
+ CG_1164_Moskva,
+ CGN_1144_2_Pyotr_Velikiy,
+ SSK_877,
+ SSK_641B
+)
from gen.fleet.dd_group import DDGroupGenerator
-from gen.sam.group_generator import GroupGenerator
-from dcs.ships import *
+from gen.sam.group_generator import ShipGroupGenerator
+from game.factions.faction import Faction
+from theater.theatergroundobject import TheaterGroundObject
-class RussianNavyGroupGenerator(GroupGenerator):
+if TYPE_CHECKING:
+ from game.game import Game
- def __init__(self, game, ground_object, faction):
- super(RussianNavyGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+
+class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
@@ -39,21 +53,20 @@ class RussianNavyGroupGenerator(GroupGenerator):
class GrishaGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
class MolniyaGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
class KiloSubGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
class TangoSubGroupGenerator(DDGroupGenerator):
- def __init__(self, game, ground_object, faction):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
-
diff --git a/gen/fleet/schnellboot.py b/gen/fleet/schnellboot.py
index 20459ac7..3de8783e 100644
--- a/gen/fleet/schnellboot.py
+++ b/gen/fleet/schnellboot.py
@@ -2,18 +2,14 @@ import random
from dcs.ships import Schnellboot_type_S130
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.group_generator import ShipGroupGenerator
-class SchnellbootGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(SchnellbootGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self):
for i in range(random.randint(2, 4)):
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
- self.get_generated_group().points[0].speed = 20
\ No newline at end of file
+ self.get_generated_group().points[0].speed = 20
diff --git a/gen/fleet/ship_group_generator.py b/gen/fleet/ship_group_generator.py
index 455d9f27..db0a78cd 100644
--- a/gen/fleet/ship_group_generator.py
+++ b/gen/fleet/ship_group_generator.py
@@ -12,6 +12,7 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
+
SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
"WW2LSTGroupGenerator": WW2LSTGroupGenerator,
@@ -28,26 +29,24 @@ SHIP_MAP = {
}
-def generate_ship_group(game, ground_object, faction:str):
+def generate_ship_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
- faction = db.FACTIONS[faction]
- if "boat" in faction.keys():
- generators = faction["boat"]
- if len(generators) > 0:
- gen = random.choice(generators)
- if gen in SHIP_MAP.keys():
- generator = SHIP_MAP[gen](game, ground_object, faction)
- generator.generate()
- return generator.get_generated_group()
- else:
- logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
+ faction = db.FACTIONS[faction_name]
+ if len(faction.navy_generators) > 0:
+ gen = random.choice(faction.navy_generators)
+ if gen in SHIP_MAP.keys():
+ generator = SHIP_MAP[gen](game, ground_object, faction)
+ generator.generate()
+ return generator.get_generated_group()
+ else:
+ logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
return None
-def generate_carrier_group(faction:str, game, ground_object):
+def generate_carrier_group(faction: str, game, ground_object):
"""
This generate a carrier group
:param parentCp: The parent control point
@@ -60,7 +59,7 @@ def generate_carrier_group(faction:str, game, ground_object):
return generator.get_generated_group()
-def generate_lha_group(faction:str, game, ground_object):
+def generate_lha_group(faction: str, game, ground_object):
"""
This generate a lha carrier group
:param parentCp: The parent control point
@@ -70,4 +69,4 @@ def generate_lha_group(faction:str, game, ground_object):
"""
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
- return generator.get_generated_group()
\ No newline at end of file
+ return generator.get_generated_group()
diff --git a/gen/fleet/uboat.py b/gen/fleet/uboat.py
index c93c7814..8d2e9cc0 100644
--- a/gen/fleet/uboat.py
+++ b/gen/fleet/uboat.py
@@ -2,14 +2,10 @@ import random
from dcs.ships import Uboat_VIIC_U_flak
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.group_generator import ShipGroupGenerator
-class UBoatGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(UBoatGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self):
diff --git a/gen/fleet/ww2lst.py b/gen/fleet/ww2lst.py
index 07f320d0..e0512009 100644
--- a/gen/fleet/ww2lst.py
+++ b/gen/fleet/ww2lst.py
@@ -2,14 +2,10 @@ import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.group_generator import ShipGroupGenerator
-class WW2LSTGroupGenerator(GroupGenerator):
-
- def __init__(self, game, ground_object, faction):
- super(WW2LSTGroupGenerator, self).__init__(game, ground_object)
- self.faction = faction
+class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self):
diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py
index 99cf8427..ce68be2d 100644
--- a/gen/flights/ai_flight_planner.py
+++ b/gen/flights/ai_flight_planner.py
@@ -1,789 +1,535 @@
-import math
+from __future__ import annotations
+
+import logging
import operator
import random
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
+
+from dcs.unittype import FlyingType, UnitType
from game import db
-from game.data.doctrine import MODERN_DOCTRINE
from game.data.radar_db import UNITS_WITH_RADAR
-from game.utils import meter_to_feet, nm_to_meter
+from game.infos.information import Information
+from game.utils import nm_to_meter
from gen import Conflict
-from gen.flights.ai_flight_planner_db import INTERCEPT_CAPABLE, CAP_CAPABLE, CAS_CAPABLE, SEAD_CAPABLE, STRIKE_CAPABLE, \
- DRONES
-from gen.flights.flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
+from gen.ato import Package
+from gen.flights.ai_flight_planner_db import (
+ CAP_CAPABLE,
+ CAP_PREFERRED,
+ CAS_CAPABLE,
+ CAS_PREFERRED,
+ SEAD_CAPABLE,
+ SEAD_PREFERRED,
+ STRIKE_CAPABLE,
+ STRIKE_PREFERRED,
+)
+from gen.flights.closestairfields import (
+ ClosestAirfields,
+ ObjectiveDistanceCache,
+)
+from gen.flights.flight import (
+ Flight,
+ FlightType,
+)
+from gen.flights.flightplan import FlightPlanBuilder
+from gen.flights.traveltime import TotEstimator
+from theater import (
+ ControlPoint,
+ FrontLine,
+ MissionTarget,
+ TheaterGroundObject,
+ SamGroundObject,
+)
+
+# Avoid importing some types that cause circular imports unless type checking.
+if TYPE_CHECKING:
+ from game import Game
+ from game.inventory import GlobalAircraftInventory
-MISSION_DURATION = 80
+@dataclass(frozen=True)
+class ProposedFlight:
+ """A flight outline proposed by the mission planner.
+
+ Proposed flights haven't been assigned specific aircraft yet. They have only
+ a task, a required number of aircraft, and a maximum distance allowed
+ between the objective and the departure airfield.
+ """
+
+ #: The flight's role.
+ task: FlightType
+
+ #: The number of aircraft required.
+ num_aircraft: int
+
+ #: The maximum distance between the objective and the departure airfield.
+ max_distance: int
+
+ def __str__(self) -> str:
+ return f"{self.task.name} {self.num_aircraft} ship"
-class FlightPlanner:
+@dataclass(frozen=True)
+class ProposedMission:
+ """A mission outline proposed by the mission planner.
- def __init__(self, from_cp, game):
- # TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine
- # TODO : the flight planner should plan package and operations
- self.from_cp = from_cp
+ Proposed missions haven't been assigned aircraft yet. They have only an
+ objective location and a list of proposed flights that are required for the
+ mission.
+ """
+
+ #: The mission objective.
+ location: MissionTarget
+
+ #: The proposed flights that are required for the mission.
+ flights: List[ProposedFlight]
+
+ def __str__(self) -> str:
+ flights = ', '.join([str(f) for f in self.flights])
+ return f"{self.location.name}: {flights}"
+
+
+class AircraftAllocator:
+ """Finds suitable aircraft for proposed missions."""
+
+ def __init__(self, closest_airfields: ClosestAirfields,
+ global_inventory: GlobalAircraftInventory,
+ is_player: bool) -> None:
+ self.closest_airfields = closest_airfields
+ self.global_inventory = global_inventory
+ self.is_player = is_player
+
+ def find_aircraft_for_flight(
+ self, flight: ProposedFlight
+ ) -> Optional[Tuple[ControlPoint, UnitType]]:
+ """Finds aircraft suitable for the given mission.
+
+ Searches for aircraft capable of performing the given mission within the
+ maximum allowed range. If insufficient aircraft are available for the
+ mission, None is returned.
+
+ Airfields are searched ordered nearest to farthest from the target and
+ searched twice. The first search looks for aircraft which prefer the
+ mission type, and the second search looks for any aircraft which are
+ capable of the mission type. For example, an F-14 from a nearby carrier
+ will be preferred for the CAP of an airfield that has only F-16s, but if
+ the carrier has only F/A-18s the F-16s will be used for CAP instead.
+
+ Note that aircraft *will* be removed from the global inventory on
+ success. This is to ensure that the same aircraft are not matched twice
+ on subsequent calls. If the found aircraft are not used, the caller is
+ responsible for returning them to the inventory.
+ """
+ result = self.find_aircraft_of_type(
+ flight, self.preferred_aircraft_for_task(flight.task)
+ )
+ if result is not None:
+ return result
+ return self.find_aircraft_of_type(
+ flight, self.capable_aircraft_for_task(flight.task)
+ )
+
+ @staticmethod
+ def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
+ cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
+ if task in cap_missions:
+ return CAP_PREFERRED
+ elif task == FlightType.CAS:
+ return CAS_PREFERRED
+ elif task in (FlightType.DEAD, FlightType.SEAD):
+ return SEAD_PREFERRED
+ elif task == FlightType.STRIKE:
+ return STRIKE_PREFERRED
+ elif task == FlightType.ESCORT:
+ return CAP_PREFERRED
+ else:
+ return []
+
+ @staticmethod
+ def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
+ cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
+ if task in cap_missions:
+ return CAP_CAPABLE
+ elif task == FlightType.CAS:
+ return CAS_CAPABLE
+ elif task in (FlightType.DEAD, FlightType.SEAD):
+ return SEAD_CAPABLE
+ elif task == FlightType.STRIKE:
+ return STRIKE_CAPABLE
+ elif task == FlightType.ESCORT:
+ return CAP_CAPABLE
+ else:
+ logging.error(f"Unplannable flight type: {task}")
+ return []
+
+ def find_aircraft_of_type(
+ self, flight: ProposedFlight, types: List[Type[FlyingType]],
+ ) -> Optional[Tuple[ControlPoint, UnitType]]:
+ airfields_in_range = self.closest_airfields.airfields_within(
+ flight.max_distance
+ )
+ for airfield in airfields_in_range:
+ if not airfield.is_friendly(self.is_player):
+ continue
+ inventory = self.global_inventory.for_control_point(airfield)
+ for aircraft, available in inventory.all_aircraft:
+ if aircraft in types and available >= flight.num_aircraft:
+ inventory.remove_aircraft(aircraft, flight.num_aircraft)
+ return airfield, aircraft
+
+ return None
+
+
+class PackageBuilder:
+ """Builds a Package for the flights it receives."""
+
+ def __init__(self, location: MissionTarget,
+ closest_airfields: ClosestAirfields,
+ global_inventory: GlobalAircraftInventory,
+ is_player: bool,
+ start_type: str) -> None:
+ self.package = Package(location)
+ self.allocator = AircraftAllocator(closest_airfields, global_inventory,
+ is_player)
+ self.global_inventory = global_inventory
+ self.start_type = start_type
+
+ def plan_flight(self, plan: ProposedFlight) -> bool:
+ """Allocates aircraft for the given flight and adds them to the package.
+
+ If no suitable aircraft are available, False is returned. If the failed
+ flight was critical and the rest of the mission will be scrubbed, the
+ caller should return any previously planned flights to the inventory
+ using release_planned_aircraft.
+ """
+ assignment = self.allocator.find_aircraft_for_flight(plan)
+ if assignment is None:
+ return False
+ airfield, aircraft = assignment
+ flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
+ plan.task, self.start_type)
+ self.package.add_flight(flight)
+ return True
+
+ def build(self) -> Package:
+ """Returns the built package."""
+ return self.package
+
+ def release_planned_aircraft(self) -> None:
+ """Returns any planned flights to the inventory."""
+ flights = list(self.package.flights)
+ for flight in flights:
+ self.global_inventory.return_from_flight(flight)
+ self.package.remove_flight(flight)
+
+
+class ObjectiveFinder:
+ """Identifies potential objectives for the mission planner."""
+
+ # TODO: Merge into doctrine.
+ AIRFIELD_THREAT_RANGE = nm_to_meter(150)
+ SAM_THREAT_RANGE = nm_to_meter(100)
+
+ def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
- self.aircraft_inventory = {} # local copy of the airbase inventory
+ self.is_player = is_player
- if from_cp.captured:
- self.faction = self.game.player_faction
- else:
- self.faction = self.game.enemy_faction
+ def enemy_sams(self) -> Iterator[TheaterGroundObject]:
+ """Iterates over all enemy SAM sites."""
+ # Control points might have the same ground object several times, for
+ # some reason.
+ found_targets: Set[str] = set()
+ for cp in self.enemy_control_points():
+ for ground_object in cp.ground_objects:
+ if not isinstance(ground_object, SamGroundObject):
+ continue
- if "doctrine" in self.faction.keys():
- self.doctrine = self.faction["doctrine"]
- else:
- self.doctrine = MODERN_DOCTRINE
+ if ground_object.is_dead:
+ continue
+ if ground_object.name in found_targets:
+ continue
- def reset(self):
+ if not self.object_has_radar(ground_object):
+ continue
+
+ # TODO: Yield in order of most threatening.
+ # Need to sort in order of how close their defensive range comes
+ # to friendly assets. To do that we need to add effective range
+ # information to the database.
+ yield ground_object
+ found_targets.add(ground_object.name)
+
+ def threatening_sams(self) -> Iterator[TheaterGroundObject]:
+ """Iterates over enemy SAMs in threat range of friendly control points.
+
+ SAM sites are sorted by their closest proximity to any friendly control
+ point (airfield or fleet).
"""
- Reset the planned flights and available units
+ sams: List[Tuple[TheaterGroundObject, int]] = []
+ for sam in self.enemy_sams():
+ ranges: List[int] = []
+ for cp in self.friendly_control_points():
+ ranges.append(sam.distance_to(cp))
+ sams.append((sam, min(ranges)))
+
+ sams = sorted(sams, key=operator.itemgetter(1))
+ for sam, _range in sams:
+ yield sam
+
+ def strike_targets(self) -> Iterator[TheaterGroundObject]:
+ """Iterates over enemy strike targets.
+
+ Targets are sorted by their closest proximity to any friendly control
+ point (airfield or fleet).
"""
- self.aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()})
- self.interceptor_flights = []
- self.cap_flights = []
- self.cas_flights = []
- self.strike_flights = []
- self.sead_flights = []
- self.custom_flights = []
- self.flights = []
- self.potential_sead_targets = []
- self.potential_strike_targets = []
+ targets: List[Tuple[TheaterGroundObject, int]] = []
+ # Control points might have the same ground object several times, for
+ # some reason.
+ found_targets: Set[str] = set()
+ for enemy_cp in self.enemy_control_points():
+ for ground_object in enemy_cp.ground_objects:
+ if ground_object.is_dead:
+ continue
+ if ground_object.name in found_targets:
+ continue
+ ranges: List[int] = []
+ for friendly_cp in self.friendly_control_points():
+ ranges.append(ground_object.distance_to(friendly_cp))
+ targets.append((ground_object, min(ranges)))
+ found_targets.add(ground_object.name)
+ targets = sorted(targets, key=operator.itemgetter(1))
+ for target, _range in targets:
+ yield target
- def plan_flights(self):
+ @staticmethod
+ def object_has_radar(ground_object: TheaterGroundObject) -> bool:
+ """Returns True if the ground object contains a unit with radar."""
+ for group in ground_object.groups:
+ for unit in group.units:
+ if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
+ return True
+ return False
- self.reset()
- self.compute_sead_targets()
- self.compute_strike_targets()
+ def front_lines(self) -> Iterator[FrontLine]:
+ """Iterates over all active front lines in the theater."""
+ for cp in self.friendly_control_points():
+ for connected in cp.connected_points:
+ if connected.is_friendly(self.is_player):
+ continue
- # The priority is to assign air-superiority fighter or interceptor to interception roles, so they can scramble if there is an attacker
- # self.commision_interceptors()
+ if Conflict.has_frontline_between(cp, connected):
+ yield FrontLine(cp, connected)
- # Then some CAP patrol for the next 2 hours
- self.commision_cap()
+ def vulnerable_control_points(self) -> Iterator[ControlPoint]:
+ """Iterates over friendly CPs that are vulnerable to enemy CPs.
- # Then setup cas
- self.commision_cas()
+ Vulnerability is defined as any enemy CP within threat range of of the
+ CP.
+ """
+ for cp in self.friendly_control_points():
+ airfields_in_proximity = self.closest_airfields_to(cp)
+ airfields_in_threat_range = airfields_in_proximity.airfields_within(
+ self.AIRFIELD_THREAT_RANGE
+ )
+ for airfield in airfields_in_threat_range:
+ if not airfield.is_friendly(self.is_player):
+ yield cp
+ break
- # Then prepare some sead flights if required
- self.commision_sead()
+ def friendly_control_points(self) -> Iterator[ControlPoint]:
+ """Iterates over all friendly control points."""
+ return (c for c in self.game.theater.controlpoints if
+ c.is_friendly(self.is_player))
- self.commision_strike()
+ 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))
- # TODO : commision ANTISHIP
+ def all_possible_targets(self) -> Iterator[MissionTarget]:
+ """Iterates over all possible mission targets in the theater.
- def remove_flight(self, index):
- try:
- flight = self.flights[index]
- if flight in self.interceptor_flights: self.interceptor_flights.remove(flight)
- if flight in self.cap_flights: self.cap_flights.remove(flight)
- if flight in self.cas_flights: self.cas_flights.remove(flight)
- if flight in self.strike_flights: self.strike_flights.remove(flight)
- if flight in self.sead_flights: self.sead_flights.remove(flight)
- if flight in self.custom_flights: self.custom_flights.remove(flight)
- self.flights.remove(flight)
- except IndexError:
+ Valid mission targets are control points (airfields and carriers), front
+ lines, and ground objects (SAM sites, factories, resource extraction
+ sites, etc).
+ """
+ for cp in self.game.theater.controlpoints:
+ yield cp
+ yield from cp.ground_objects
+ yield from self.front_lines()
+
+ @staticmethod
+ def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:
+ """Returns the closest airfields to the given location."""
+ return ObjectiveDistanceCache.get_closest_airfields(location)
+
+
+class CoalitionMissionPlanner:
+ """Coalition flight planning AI.
+
+ This class is responsible for automatically planning missions for the
+ coalition at the start of the turn.
+
+ The primary goal of the mission planner is to protect existing friendly
+ assets. Missions will be planned with the following priorities:
+
+ 1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy
+ losses of friendly aircraft.
+ 2. CAP for front line areas to protect ground and CAS units.
+ 3. DEAD to reduce necessity of SEAD for future missions.
+ 4. CAS to protect friendly ground units.
+ 5. Strike missions to reduce the enemy's resources.
+
+ TODO: Anti-ship and airfield strikes to reduce enemy sortie rates.
+ TODO: BAI to prevent enemy forces from reaching the front line.
+ TODO: Should fleets always have a CAP?
+
+ TODO: Stance and doctrine-specific planning behavior.
+ """
+
+ # TODO: Merge into doctrine, also limit by aircraft.
+ MAX_CAP_RANGE = nm_to_meter(100)
+ MAX_CAS_RANGE = nm_to_meter(50)
+ MAX_SEAD_RANGE = nm_to_meter(150)
+ MAX_STRIKE_RANGE = nm_to_meter(150)
+
+ def __init__(self, game: Game, is_player: bool) -> None:
+ self.game = game
+ self.is_player = is_player
+ self.objective_finder = ObjectiveFinder(self.game, self.is_player)
+ self.ato = self.game.blue_ato if is_player else self.game.red_ato
+
+ def propose_missions(self) -> Iterator[ProposedMission]:
+ """Identifies and iterates over potential mission in priority order."""
+ # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
+ for cp in self.objective_finder.vulnerable_control_points():
+ yield ProposedMission(cp, [
+ ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
+ ])
+
+ # Find front lines, plan CAP.
+ for front_line in self.objective_finder.front_lines():
+ yield ProposedMission(front_line, [
+ ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
+ ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
+ ])
+
+ # Find enemy SAM sites with ranges that cover friendly CPs, front lines,
+ # or objects, plan DEAD.
+ # Find enemy SAM sites with ranges that extend to within 50 nmi of
+ # friendly CPs, front, lines, or objects, plan DEAD.
+ for sam in self.objective_finder.threatening_sams():
+ yield ProposedMission(sam, [
+ ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
+ # TODO: Max escort range.
+ ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
+ ])
+
+ # Plan strike missions.
+ for target in self.objective_finder.strike_targets():
+ yield ProposedMission(target, [
+ ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
+ # TODO: Max escort range.
+ ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE),
+ ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE),
+ ])
+
+ def plan_missions(self) -> None:
+ """Identifies and plans mission for the turn."""
+ for proposed_mission in self.propose_missions():
+ self.plan_mission(proposed_mission)
+
+ self.stagger_missions()
+
+ for cp in self.objective_finder.friendly_control_points():
+ inventory = self.game.aircraft_inventory.for_control_point(cp)
+ for aircraft, available in inventory.all_aircraft:
+ self.message("Unused aircraft",
+ f"{available} {aircraft.id} from {cp}")
+
+ def plan_mission(self, mission: ProposedMission) -> None:
+ """Allocates aircraft for a proposed mission and adds it to the ATO."""
+
+ if self.game.settings.perf_ai_parking_start:
+ start_type = "Cold"
+ else:
+ start_type = "Warm"
+
+ builder = PackageBuilder(
+ mission.location,
+ self.objective_finder.closest_airfields_to(mission.location),
+ self.game.aircraft_inventory,
+ self.is_player,
+ start_type
+ )
+
+ missing_types: Set[FlightType] = set()
+ for proposed_flight in mission.flights:
+ if not builder.plan_flight(proposed_flight):
+ missing_types.add(proposed_flight.task)
+
+ if missing_types:
+ missing_types_str = ", ".join(
+ sorted([t.name for t in missing_types]))
+ builder.release_planned_aircraft()
+ self.message(
+ "Insufficient aircraft",
+ f"Not enough aircraft in range for {mission.location.name} "
+ f"capable of: {missing_types_str}")
return
+ package = builder.build()
+ flight_plan_builder = FlightPlanBuilder(self.game, package,
+ self.is_player)
+ for flight in package.flights:
+ flight_plan_builder.populate_flight_plan(flight)
+ self.ato.add_package(package)
- def commision_interceptors(self):
- """
- Pick some aircraft to assign them to interception roles
- """
+ def stagger_missions(self) -> None:
+ def start_time_generator(count: int, earliest: int, latest: int,
+ margin: int) -> Iterator[timedelta]:
+ interval = (latest - earliest) // count
+ for time in range(earliest, latest, interval):
+ error = random.randint(-margin, margin)
+ yield timedelta(minutes=max(0, time + error))
- # At least try to generate one interceptor group
- number_of_interceptor_groups = min(max(sum([v for k, v in self.aircraft_inventory.items()]) / 4, self.doctrine["MAX_NUMBER_OF_INTERCEPTION_GROUP"]), 1)
- possible_interceptors = [k for k in self.aircraft_inventory.keys() if k in INTERCEPT_CAPABLE]
+ dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
- if len(possible_interceptors) <= 0:
- possible_interceptors = [k for k,v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2]
+ non_dca_packages = [p for p in self.ato.packages if
+ p.primary_task not in dca_types]
- if number_of_interceptor_groups > 0:
- inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_interceptors})
- for i in range(number_of_interceptor_groups):
- try:
- unit = random.choice([k for k,v in inventory.items() if v >= 2])
- except IndexError:
- break
- inventory[unit] = inventory[unit] - 2
- flight = Flight(unit, 2, self.from_cp, FlightType.INTERCEPTION)
- flight.scheduled_in = 1
- flight.points = []
-
- self.interceptor_flights.append(flight)
- self.flights.append(flight)
-
- # Update inventory
- for k, v in inventory.items():
- self.aircraft_inventory[k] = v
-
- def commision_cap(self):
- """
- Pick some aircraft to assign them to defensive CAP roles (BARCAP)
- """
-
- possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2]
- inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
-
- offset = random.randint(0,5)
- for i in range(int(MISSION_DURATION/self.doctrine["CAP_EVERY_X_MINUTES"])):
-
- try:
- unit = random.choice([k for k, v in inventory.items() if v >= 2])
- except IndexError:
- break
-
- inventory[unit] = inventory[unit] - 2
- flight = Flight(unit, 2, self.from_cp, FlightType.CAP)
-
- flight.points = []
- flight.scheduled_in = offset + i*random.randint(self.doctrine["CAP_EVERY_X_MINUTES"] - 5, self.doctrine["CAP_EVERY_X_MINUTES"] + 5)
-
- if len(self._get_cas_locations()) > 0:
- enemy_cp = random.choice(self._get_cas_locations())
- self.generate_frontline_cap(flight, flight.from_cp, enemy_cp)
+ start_time = start_time_generator(
+ count=len(non_dca_packages),
+ earliest=5,
+ latest=90,
+ margin=5
+ )
+ for package in self.ato.packages:
+ tot = TotEstimator(package).earliest_tot()
+ if package.primary_task in dca_types:
+ # All CAP missions should be on station ASAP.
+ package.time_over_target = tot
else:
- self.generate_barcap(flight, flight.from_cp)
+ # But other packages should be spread out a bit. Note that take
+ # times are delayed, but all aircraft will become active at
+ # mission start. This makes it more worthwhile to attack enemy
+ # airfields to hit grounded aircraft, since they're more likely
+ # 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
- self.cap_flights.append(flight)
- self.flights.append(flight)
+ def message(self, title, text) -> None:
+ """Emits a planning message to the player.
- # Update inventory
- for k, v in inventory.items():
- self.aircraft_inventory[k] = v
-
- def commision_cas(self):
+ If the mission planner belongs to the players coalition, this emits a
+ message to the info panel.
"""
- Pick some aircraft to assign them to CAS
- """
-
- possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAS_CAPABLE and v >= 2]
- inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
- cas_location = self._get_cas_locations()
-
- if len(cas_location) > 0:
-
- offset = random.randint(0,5)
- for i in range(int(MISSION_DURATION/self.doctrine["CAS_EVERY_X_MINUTES"])):
-
- try:
- unit = random.choice([k for k, v in inventory.items() if v >= 2])
- except IndexError:
- break
-
- inventory[unit] = inventory[unit] - 2
- flight = Flight(unit, 2, self.from_cp, FlightType.CAS)
- flight.points = []
- flight.scheduled_in = offset + i * random.randint(self.doctrine["CAS_EVERY_X_MINUTES"] - 5, self.doctrine["CAS_EVERY_X_MINUTES"] + 5)
- location = random.choice(cas_location)
-
- self.generate_cas(flight, flight.from_cp, location)
-
- self.cas_flights.append(flight)
- self.flights.append(flight)
-
- # Update inventory
- for k, v in inventory.items():
- self.aircraft_inventory[k] = v
-
- def commision_sead(self):
- """
- Pick some aircraft to assign them to SEAD tasks
- """
-
- possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in SEAD_CAPABLE and v >= 2]
- inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
-
- if len(self.potential_sead_targets) > 0:
-
- offset = random.randint(0,5)
- for i in range(int(MISSION_DURATION/self.doctrine["SEAD_EVERY_X_MINUTES"])):
-
- if len(self.potential_sead_targets) <= 0:
- break
-
- try:
- unit = random.choice([k for k, v in inventory.items() if v >= 2])
- except IndexError:
- break
-
- inventory[unit] = inventory[unit] - 2
- flight = Flight(unit, 2, self.from_cp, random.choice([FlightType.SEAD, FlightType.DEAD]))
-
- flight.points = []
- flight.scheduled_in = offset + i*random.randint(self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, self.doctrine["SEAD_EVERY_X_MINUTES"] + 5)
-
- location = self.potential_sead_targets[0][0]
- self.potential_sead_targets.pop(0)
-
- self.generate_sead(flight, location, [])
-
- self.sead_flights.append(flight)
- self.flights.append(flight)
-
- # Update inventory
- for k, v in inventory.items():
- self.aircraft_inventory[k] = v
-
-
- def commision_strike(self):
- """
- Pick some aircraft to assign them to STRIKE tasks
- """
- possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in STRIKE_CAPABLE and v >= 2]
- inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft})
-
- if len(self.potential_strike_targets) > 0:
-
- offset = random.randint(0,5)
- for i in range(int(MISSION_DURATION/self.doctrine["STRIKE_EVERY_X_MINUTES"])):
-
- if len(self.potential_strike_targets) <= 0:
- break
-
- try:
- unit = random.choice([k for k, v in inventory.items() if v >= 2])
- except IndexError:
- break
-
- if unit in DRONES:
- count = 1
- else:
- count = 2
-
- inventory[unit] = inventory[unit] - count
- flight = Flight(unit, count, self.from_cp, FlightType.STRIKE)
-
- flight.points = []
- flight.scheduled_in = offset + i*random.randint(self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5)
-
- location = self.potential_strike_targets[0][0]
- self.potential_strike_targets.pop(0)
-
- self.generate_strike(flight, location)
-
- self.strike_flights.append(flight)
- self.flights.append(flight)
-
- # Update inventory
- for k, v in inventory.items():
- self.aircraft_inventory[k] = v
-
- def _get_cas_locations(self):
- return self._get_cas_locations_for_cp(self.from_cp)
-
- def _get_cas_locations_for_cp(self, for_cp):
- cas_locations = []
- for cp in for_cp.connected_points:
- if cp.captured != for_cp.captured:
- cas_locations.append(cp)
- return cas_locations
-
- def compute_strike_targets(self):
- """
- @return a list of potential strike targets in range
- """
-
- # target, distance
- self.potential_strike_targets = []
-
- for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]:
-
- # Compute distance to current cp
- distance = math.hypot(cp.position.x - self.from_cp.position.x,
- cp.position.y - self.from_cp.position.y)
-
- if distance > 2*self.doctrine["STRIKE_MAX_RANGE"]:
- # Then it's unlikely any child ground object is in range
- return
-
- added_group = []
- for g in cp.ground_objects:
- if g.group_id in added_group or g.is_dead: continue
-
- # Compute distance to current cp
- distance = math.hypot(cp.position.x - self.from_cp.position.x,
- cp.position.y - self.from_cp.position.y)
-
- if distance < self.doctrine["SEAD_MAX_RANGE"]:
- self.potential_strike_targets.append((g, distance))
- added_group.append(g)
-
- self.potential_strike_targets.sort(key=operator.itemgetter(1))
-
- def compute_sead_targets(self):
- """
- @return a list of potential sead targets in range
- """
-
- # target, distance
- self.potential_sead_targets = []
-
- for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]:
-
- # Compute distance to current cp
- distance = math.hypot(cp.position.x - self.from_cp.position.x,
- cp.position.y - self.from_cp.position.y)
-
- # Then it's unlikely any ground object is range
- if distance > 2*self.doctrine["SEAD_MAX_RANGE"]:
- return
-
- for g in cp.ground_objects:
-
- if g.dcs_identifier == "AA":
-
- # Check that there is at least one unit with a radar in the ground objects unit groups
- number_of_units = sum([len([r for r in group.units if db.unit_type_from_name(r.type) in UNITS_WITH_RADAR]) for group in g.groups])
- if number_of_units <= 0:
- continue
-
- # Compute distance to current cp
- distance = math.hypot(cp.position.x - self.from_cp.position.x,
- cp.position.y - self.from_cp.position.y)
-
- if distance < self.doctrine["SEAD_MAX_RANGE"]:
- self.potential_sead_targets.append((g, distance))
-
- self.potential_sead_targets.sort(key=operator.itemgetter(1))
-
- def __repr__(self):
- return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\
- + "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40
-
- def get_available_aircraft(self):
- base_aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()})
- for f in self.flights:
- if f.unit_type in base_aircraft_inventory.keys():
- base_aircraft_inventory[f.unit_type] = base_aircraft_inventory[f.unit_type] - f.count
- if base_aircraft_inventory[f.unit_type] <= 0:
- del base_aircraft_inventory[f.unit_type]
- return base_aircraft_inventory
-
-
- def generate_strike(self, flight, location):
-
- flight.flight_type = FlightType.STRIKE
- ascend = self.generate_ascend_point(flight.from_cp)
- flight.points.append(ascend)
-
- heading = flight.from_cp.position.heading_between_point(location.position)
- ingress_heading = heading - 180 + 25
- egress_heading = heading - 180 - 25
-
- ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
- ingress_point = FlightWaypoint(
- FlightWaypointType.INGRESS_STRIKE,
- ingress_pos.x,
- ingress_pos.y,
- self.doctrine["INGRESS_ALT"]
- )
- ingress_point.pretty_name = "INGRESS on " + location.obj_name
- ingress_point.description = "INGRESS on " + location.obj_name
- ingress_point.name = "INGRESS"
- flight.points.append(ingress_point)
-
- if len(location.groups) > 0 and location.dcs_identifier == "AA":
- for g in location.groups:
- for j, u in enumerate(g.units):
- point = FlightWaypoint(
- FlightWaypointType.TARGET_POINT,
- u.position.x,
- u.position.y,
- 0
- )
- point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
- point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
- point.name = location.obj_name + "#" + str(j)
- point.only_for_player = True
- ingress_point.targets.append(location)
- flight.points.append(point)
- else:
- if hasattr(location, "obj_name"):
- buildings = self.game.theater.find_ground_objects_by_obj_name(location.obj_name)
- print(buildings)
- for building in buildings:
- print("BUILDING " + str(building.is_dead) + " " + str(building.dcs_identifier))
- if building.is_dead:
- continue
-
- point = FlightWaypoint(
- FlightWaypointType.TARGET_POINT,
- building.position.x,
- building.position.y,
- 0
- )
- point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
- point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
- point.name = building.obj_name
- point.only_for_player = True
- ingress_point.targets.append(building)
- flight.points.append(point)
- else:
- point = FlightWaypoint(
- FlightWaypointType.TARGET_GROUP_LOC,
- location.position.x,
- location.position.y,
- 0
- )
- point.description = "STRIKE on " + location.obj_name
- point.pretty_name = "STRIKE on " + location.obj_name
- point.name = location.obj_name
- point.only_for_player = True
- ingress_point.targets.append(location)
- flight.points.append(point)
-
- egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
- egress_point = FlightWaypoint(
- FlightWaypointType.EGRESS,
- egress_pos.x,
- egress_pos.y,
- self.doctrine["EGRESS_ALT"]
- )
- egress_point.name = "EGRESS"
- egress_point.pretty_name = "EGRESS from " + location.obj_name
- egress_point.description = "EGRESS from " + location.obj_name
- flight.points.append(egress_point)
-
- descend = self.generate_descend_point(flight.from_cp)
- flight.points.append(descend)
-
- rtb = self.generate_rtb_waypoint(flight.from_cp)
- flight.points.append(rtb)
-
- def generate_barcap(self, flight, for_cp):
- """
- Generate a barcap flight at a given location
- :param flight: Flight to setup
- :param for_cp: CP to protect
- """
- flight.flight_type = FlightType.BARCAP if for_cp.is_carrier else FlightType.CAP
- patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1])
-
- if len(for_cp.ground_objects) > 0:
- loc = random.choice(for_cp.ground_objects)
- hdg = for_cp.position.heading_between_point(loc.position)
- radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1])
- orbit0p = loc.position.point_from_heading(hdg - 90, radius)
- orbit1p = loc.position.point_from_heading(hdg + 90, radius)
- else:
- loc = for_cp.position.point_from_heading(random.randint(0, 360), random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0], self.doctrine["CAP_DISTANCE_FROM_CP"][1]))
- hdg = for_cp.position.heading_between_point(loc)
- radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1])
- orbit0p = loc.point_from_heading(hdg - 90, radius)
- orbit1p = loc.point_from_heading(hdg + 90, radius)
-
- # Create points
- ascend = self.generate_ascend_point(flight.from_cp)
- flight.points.append(ascend)
-
- orbit0 = FlightWaypoint(
- FlightWaypointType.PATROL_TRACK,
- orbit0p.x,
- orbit0p.y,
- patrol_alt
- )
- orbit0.name = "ORBIT 0"
- orbit0.description = "Standby between this point and the next one"
- orbit0.pretty_name = "Race-track start"
- flight.points.append(orbit0)
-
- orbit1 = FlightWaypoint(
- FlightWaypointType.PATROL,
- orbit1p.x,
- orbit1p.y,
- patrol_alt
- )
- orbit1.name = "ORBIT 1"
- orbit1.description = "Standby between this point and the previous one"
- orbit1.pretty_name = "Race-track end"
- flight.points.append(orbit1)
-
- orbit0.targets.append(for_cp)
- obj_added = []
- for ground_object in for_cp.ground_objects:
- if ground_object.obj_name not in obj_added and not ground_object.airbase_group:
- orbit0.targets.append(ground_object)
- obj_added.append(ground_object.obj_name)
-
- descend = self.generate_descend_point(flight.from_cp)
- flight.points.append(descend)
-
- rtb = self.generate_rtb_waypoint(flight.from_cp)
- flight.points.append(rtb)
-
-
- def generate_frontline_cap(self, flight, ally_cp, enemy_cp):
- """
- Generate a cap flight for the frontline between ally_cp and enemy cp in order to ensure air superiority and
- protect friendly CAP airbase
- :param flight: Flight to setup
- :param ally_cp: CP to protect
- :param enemy_cp: Enemy connected cp
- """
- flight.flight_type = FlightType.CAP
- patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1])
-
- # Find targets waypoints
- ingress, heading, distance = Conflict.frontline_vector(ally_cp, enemy_cp, self.game.theater)
- center = ingress.point_from_heading(heading, distance / 2)
- orbit_center = center.point_from_heading(heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15)))
-
- combat_width = distance / 2
- if combat_width > 500000:
- combat_width = 500000
- if combat_width < 35000:
- combat_width = 35000
-
- radius = combat_width*1.25
- orbit0p = orbit_center.point_from_heading(heading, radius)
- orbit1p = orbit_center.point_from_heading(heading + 180, radius)
-
- # Create points
- ascend = self.generate_ascend_point(flight.from_cp)
- flight.points.append(ascend)
-
- orbit0 = FlightWaypoint(
- FlightWaypointType.PATROL_TRACK,
- orbit0p.x,
- orbit0p.y,
- patrol_alt
- )
- orbit0.name = "ORBIT 0"
- orbit0.description = "Standby between this point and the next one"
- orbit0.pretty_name = "Race-track start"
- flight.points.append(orbit0)
-
- orbit1 = FlightWaypoint(
- FlightWaypointType.PATROL,
- orbit1p.x,
- orbit1p.y,
- patrol_alt
- )
- orbit1.name = "ORBIT 1"
- orbit1.description = "Standby between this point and the previous one"
- orbit1.pretty_name = "Race-track end"
- flight.points.append(orbit1)
-
- # Note : Targets of a PATROL TRACK waypoints are the points to be defended
- orbit0.targets.append(flight.from_cp)
- orbit0.targets.append(center)
-
- descend = self.generate_descend_point(flight.from_cp)
- flight.points.append(descend)
-
- rtb = self.generate_rtb_waypoint(flight.from_cp)
- flight.points.append(rtb)
-
-
- def generate_sead(self, flight, location, custom_targets = []):
- """
- Generate a sead flight at a given location
- :param flight: Flight to setup
- :param location: Location of the SEAD target
- :param custom_targets: Custom targets if any
- """
- flight.points = []
- flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD])
-
- ascend = self.generate_ascend_point(flight.from_cp)
- flight.points.append(ascend)
-
- heading = flight.from_cp.position.heading_between_point(location.position)
- ingress_heading = heading - 180 + 25
- egress_heading = heading - 180 - 25
-
- ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
- ingress_point = FlightWaypoint(
- FlightWaypointType.INGRESS_SEAD,
- ingress_pos.x,
- ingress_pos.y,
- self.doctrine["INGRESS_ALT"]
- )
- ingress_point.name = "INGRESS"
- ingress_point.pretty_name = "INGRESS on " + location.obj_name
- ingress_point.description = "INGRESS on " + location.obj_name
- flight.points.append(ingress_point)
-
- if len(custom_targets) > 0:
- for target in custom_targets:
- point = FlightWaypoint(
- FlightWaypointType.TARGET_POINT,
- target.position.x,
- target.position.y,
- 0
- )
- point.alt_type = "RADIO"
- if flight.flight_type == FlightType.DEAD:
- point.description = "DEAD on " + target.type
- point.pretty_name = "DEAD on " + location.obj_name
- point.only_for_player = True
- else:
- point.description = "SEAD on " + location.obj_name
- point.pretty_name = "SEAD on " + location.obj_name
- point.only_for_player = True
- flight.points.append(point)
- ingress_point.targets.append(location)
- ingress_point.targetGroup = location
- else:
- point = FlightWaypoint(
- FlightWaypointType.TARGET_GROUP_LOC,
- location.position.x,
- location.position.y,
- 0
+ if self.is_player:
+ self.game.informations.append(
+ Information(title, text, self.game.turn)
)
- point.alt_type = "RADIO"
- if flight.flight_type == FlightType.DEAD:
- point.description = "DEAD on " + location.obj_name
- point.pretty_name = "DEAD on " + location.obj_name
- point.only_for_player = True
- else:
- point.description = "SEAD on " + location.obj_name
- point.pretty_name = "SEAD on " + location.obj_name
- point.only_for_player = True
- ingress_point.targets.append(location)
- ingress_point.targetGroup = location
- flight.points.append(point)
-
- egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
- egress_point = FlightWaypoint(
- FlightWaypointType.EGRESS,
- egress_pos.x,
- egress_pos.y,
- self.doctrine["EGRESS_ALT"]
- )
- egress_point.name = "EGRESS"
- egress_point.pretty_name = "EGRESS from " + location.obj_name
- egress_point.description = "EGRESS from " + location.obj_name
- flight.points.append(egress_point)
-
- descend = self.generate_descend_point(flight.from_cp)
- flight.points.append(descend)
-
- rtb = self.generate_rtb_waypoint(flight.from_cp)
- flight.points.append(rtb)
-
-
- def generate_cas(self, flight, from_cp, location):
- """
- Generate a CAS flight at a given location
- :param flight: Flight to setup
- :param location: Location of the CAS targets
- """
- is_helo = hasattr(flight.unit_type, "helicopter") and flight.unit_type.helicopter
- cap_alt = 1000
- flight.points = []
- flight.flight_type = FlightType.CAS
-
- ingress, heading, distance = Conflict.frontline_vector(from_cp, location, self.game.theater)
- center = ingress.point_from_heading(heading, distance / 2)
- egress = ingress.point_from_heading(heading, distance)
-
- ascend = self.generate_ascend_point(flight.from_cp)
- if is_helo:
- cap_alt = 500
- ascend.alt = 500
- flight.points.append(ascend)
-
- ingress_point = FlightWaypoint(
- FlightWaypointType.INGRESS_CAS,
- ingress.x,
- ingress.y,
- cap_alt
- )
- ingress_point.alt_type = "RADIO"
- ingress_point.name = "INGRESS"
- ingress_point.pretty_name = "INGRESS"
- ingress_point.description = "Ingress into CAS area"
- flight.points.append(ingress_point)
-
- center_point = FlightWaypoint(
- FlightWaypointType.CAS,
- center.x,
- center.y,
- cap_alt
- )
- center_point.alt_type = "RADIO"
- center_point.description = "Provide CAS"
- center_point.name = "CAS"
- center_point.pretty_name = "CAS"
- flight.points.append(center_point)
-
- egress_point = FlightWaypoint(
- FlightWaypointType.EGRESS,
- egress.x,
- egress.y,
- cap_alt
- )
- egress_point.alt_type = "RADIO"
- egress_point.description = "Egress from CAS area"
- egress_point.name = "EGRESS"
- egress_point.pretty_name = "EGRESS"
- flight.points.append(egress_point)
-
- descend = self.generate_descend_point(flight.from_cp)
- if is_helo:
- descend.alt = 300
- flight.points.append(descend)
-
- rtb = self.generate_rtb_waypoint(flight.from_cp)
- flight.points.append(rtb)
-
- def generate_ascend_point(self, from_cp):
- """
- Generate ascend point
- :param from_cp: Airport you're taking off from
- :return:
- """
- ascend_heading = from_cp.heading
- pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000)
- ascend = FlightWaypoint(
- FlightWaypointType.ASCEND_POINT,
- pos_ascend.x,
- pos_ascend.y,
- self.doctrine["PATTERN_ALTITUDE"]
- )
- ascend.name = "ASCEND"
- ascend.alt_type = "RADIO"
- ascend.description = "Ascend"
- ascend.pretty_name = "Ascend"
- return ascend
-
- def generate_descend_point(self, from_cp):
- """
- Generate approach/descend point
- :param from_cp: Airport you're landing at
- :return:
- """
- ascend_heading = from_cp.heading
- descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000)
- descend = FlightWaypoint(
- FlightWaypointType.DESCENT_POINT,
- descend.x,
- descend.y,
- self.doctrine["PATTERN_ALTITUDE"]
- )
- descend.name = "DESCEND"
- descend.alt_type = "RADIO"
- descend.description = "Descend to pattern alt"
- descend.pretty_name = "Descend to pattern alt"
- return descend
-
- def generate_rtb_waypoint(self, from_cp):
- """
- Generate RTB landing point
- :param from_cp: Airport you're landing at
- :return:
- """
- rtb = from_cp.position
- rtb = FlightWaypoint(
- FlightWaypointType.LANDING_POINT,
- rtb.x,
- rtb.y,
- 0
- )
- rtb.name = "LANDING"
- rtb.alt_type = "RADIO"
- rtb.description = "RTB"
- rtb.pretty_name = "RTB"
- return rtb
+ else:
+ logging.info(f"{title}: {text}")
diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py
index e1393a1a..fbc2b257 100644
--- a/gen/flights/ai_flight_planner_db.py
+++ b/gen/flights/ai_flight_planner_db.py
@@ -1,5 +1,84 @@
-from dcs.planes import *
-from dcs.helicopters import *
+from dcs.helicopters import (
+ AH_1W,
+ AH_64A,
+ AH_64D,
+ Ka_50,
+ Mi_24V,
+ Mi_28N,
+ Mi_8MT,
+ OH_58D,
+ SA342L,
+ SA342M,
+ UH_1H,
+)
+from dcs.planes import (
+ AJS37,
+ AV8BNA,
+ A_10A,
+ A_10C,
+ A_10C_2,
+ A_20G,
+ B_17G,
+ B_1B,
+ B_52H,
+ Bf_109K_4,
+ C_101CC,
+ FA_18C_hornet,
+ FW_190A8,
+ FW_190D9,
+ F_117A,
+ F_14B,
+ F_15C,
+ F_15E,
+ F_16A,
+ F_16C_50,
+ F_4E,
+ F_5E_3,
+ F_86F_Sabre,
+ F_A_18C,
+ JF_17,
+ J_11A,
+ Ju_88A4,
+ L_39ZA,
+ MQ_9_Reaper,
+ M_2000C,
+ MiG_15bis,
+ MiG_19P,
+ MiG_21Bis,
+ MiG_23MLD,
+ MiG_25PD,
+ MiG_27K,
+ MiG_29A,
+ MiG_29G,
+ MiG_29K,
+ MiG_29S,
+ MiG_31,
+ Mirage_2000_5,
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ P_51D,
+ P_51D_30_NA,
+ RQ_1A_Predator,
+ SpitfireLFMkIX,
+ SpitfireLFMkIXCW,
+ Su_17M4,
+ Su_24M,
+ Su_24MR,
+ Su_25,
+ Su_25T,
+ Su_25TM,
+ Su_27,
+ Su_30,
+ Su_33,
+ Su_34,
+ Tornado_GR4,
+ Tornado_IDS,
+ Tu_160,
+ Tu_22M3,
+ Tu_95MS,
+ WingLoong_I,
+)
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
@@ -7,6 +86,11 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
+# TODO: These lists really ought to be era (faction) dependent.
+# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
+# factions that also have F-4s should not.
+from pydcs_extensions.su57.su57 import Su_57
+
INTERCEPT_CAPABLE = [
MiG_21Bis,
MiG_25PD,
@@ -43,6 +127,7 @@ CAP_CAPABLE = [
JF_17,
Su_30,
Su_33,
+ Su_57,
M_2000C,
Mirage_2000_5,
@@ -77,6 +162,43 @@ CAP_CAPABLE = [
Rafale_M,
]
+CAP_PREFERRED = [
+ MiG_15bis,
+ MiG_19P,
+ MiG_21Bis,
+ MiG_23MLD,
+ MiG_25PD,
+ MiG_29A,
+ MiG_29G,
+ MiG_29S,
+ MiG_31,
+
+ Su_27,
+ J_11A,
+ Su_30,
+ Su_33,
+ Su_57,
+
+ M_2000C,
+ Mirage_2000_5,
+
+ F_86F_Sabre,
+ F_14B,
+ F_15C,
+
+ P_51D_30_NA,
+ P_51D,
+
+ SpitfireLFMkIXCW,
+ SpitfireLFMkIX,
+
+ Bf_109K_4,
+ FW_190D9,
+ FW_190A8,
+
+ Rafale_M,
+]
+
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
@@ -110,6 +232,8 @@ CAS_CAPABLE = [
F_16C_50,
FA_18C_hornet,
+ B_1B,
+
Tornado_IDS,
Tornado_GR4,
@@ -155,12 +279,65 @@ CAS_CAPABLE = [
RQ_1A_Predator
]
+CAS_PREFERRED = [
+ Su_17M4,
+ Su_24M,
+ Su_24MR,
+ Su_25,
+ Su_25T,
+ Su_25TM,
+ Su_34,
+
+ JF_17,
+
+ A_10A,
+ A_10C,
+ A_10C_2,
+ AV8BNA,
+
+ F_15E,
+
+ Tornado_GR4,
+
+ C_101CC,
+ MB_339PAN,
+ L_39ZA,
+ AJS37,
+
+ SA342M,
+ SA342L,
+ OH_58D,
+
+ AH_64A,
+ AH_64D,
+ AH_1W,
+
+ UH_1H,
+
+ Mi_8MT,
+ Mi_28N,
+ Mi_24V,
+ Ka_50,
+
+ P_47D_30,
+ P_47D_30bl1,
+ P_47D_40,
+ A_20G,
+
+ A_4E_C,
+ Rafale_A_S,
+
+ WingLoong_I,
+ MQ_9_Reaper,
+ RQ_1A_Predator
+]
+
# Aircraft used for SEAD / DEAD tasks
SEAD_CAPABLE = [
F_4E,
FA_18C_hornet,
F_15E,
- # F_16C_50, Not yet
+ F_16C_50,
AV8BNA,
JF_17,
@@ -179,6 +356,12 @@ SEAD_CAPABLE = [
Rafale_A_S
]
+SEAD_PREFERRED = [
+ F_4E,
+ Su_25T,
+ Tornado_IDS,
+]
+
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
MiG_15bis,
@@ -192,6 +375,10 @@ STRIKE_CAPABLE = [
Su_25T,
Su_34,
+ Tu_160,
+ Tu_22M3,
+ Tu_95MS,
+
JF_17,
M_2000C,
@@ -209,6 +396,10 @@ STRIKE_CAPABLE = [
F_16C_50,
FA_18C_hornet,
+ B_1B,
+ B_52H,
+ F_117A,
+
Tornado_IDS,
Tornado_GR4,
@@ -236,6 +427,20 @@ STRIKE_CAPABLE = [
]
+STRIKE_PREFERRED = [
+ AJS37,
+ A_20G,
+ B_17G,
+ B_1B,
+ B_52H,
+ F_117A,
+ F_15E,
+ Tornado_GR4,
+ Tu_160,
+ Tu_22M3,
+ Tu_95MS,
+]
+
ANTISHIP_CAPABLE = [
Su_24M,
Su_17M4,
diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py
new file mode 100644
index 00000000..a6045dde
--- /dev/null
+++ b/gen/flights/closestairfields.py
@@ -0,0 +1,51 @@
+"""Objective adjacency lists."""
+from typing import Dict, Iterator, List, Optional
+
+from theater import ConflictTheater, ControlPoint, MissionTarget
+
+
+class ClosestAirfields:
+ """Precalculates which control points are closes to the given target."""
+
+ def __init__(self, target: MissionTarget,
+ all_control_points: List[ControlPoint]) -> None:
+ self.target = target
+ self.closest_airfields: List[ControlPoint] = sorted(
+ all_control_points, key=lambda c: self.target.distance_to(c)
+ )
+
+ def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
+ """Iterates over all airfields within the given range of the target.
+
+ Note that this iterates over *all* airfields, not just friendly
+ airfields.
+ """
+ for cp in self.closest_airfields:
+ if cp.distance_to(self.target) < meters:
+ yield cp
+ else:
+ break
+
+
+class ObjectiveDistanceCache:
+ theater: Optional[ConflictTheater] = None
+ closest_airfields: Dict[str, ClosestAirfields] = {}
+
+ @classmethod
+ def set_theater(cls, theater: ConflictTheater) -> None:
+ if cls.theater is not None:
+ cls.closest_airfields = {}
+ cls.theater = theater
+
+ @classmethod
+ def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
+ if cls.theater is None:
+ raise RuntimeError(
+ "Call ObjectiveDistanceCache.set_theater before using"
+ )
+
+ if location.name not in cls.closest_airfields:
+ cls.closest_airfields[location.name] = ClosestAirfields(
+ location, cls.theater.controlpoints
+ )
+ return cls.closest_airfields[location.name]
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index 0c5c7956..2462a0a5 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -1,14 +1,23 @@
+from __future__ import annotations
+
+from datetime import timedelta
from enum import Enum
-from typing import List
+from typing import Dict, List, Optional, TYPE_CHECKING
+
+from dcs.mapping import Point
+from dcs.point import MovingPoint, PointAction
+from dcs.unittype import FlyingType
from game import db
-from dcs.unittype import UnitType
-from dcs.point import MovingPoint, PointAction
-from theater.controlpoint import ControlPoint
+from theater.controlpoint import ControlPoint, MissionTarget
+
+if TYPE_CHECKING:
+ from gen.ato import Package
+ from gen.flights.flightplan import FlightPlan
class FlightType(Enum):
- CAP = 0
+ CAP = 0 # Do not use. Use BARCAP or TARCAP.
TARCAP = 1
BARCAP = 2
CAS = 3
@@ -47,23 +56,26 @@ class FlightWaypointType(Enum):
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
-
-
-class PredefinedWaypointCategory(Enum):
- NOT_PREDEFINED = 0
- ALLY_CP = 1
- ENEMY_CP = 2
- FRONTLINE = 3
- ENEMY_BUILDING = 4
- ENEMY_UNIT = 5
- ALLY_BUILDING = 6
- ALLY_UNIT = 7
+ JOIN = 16
+ SPLIT = 17
+ LOITER = 18
+ INGRESS_ESCORT = 19
+ INGRESS_DEAD = 20
class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: int = 0) -> None:
+ """Creates a flight waypoint.
+
+ Args:
+ waypoint_type: The waypoint type.
+ x: X cooidinate of the waypoint.
+ y: Y coordinate of the waypoint.
+ alt: Altitude of the waypoint. By default this is AGL, but it can be
+ changed to MSL by setting alt_type to "RADIO".
+ """
self.waypoint_type = waypoint_type
self.x = x
self.y = y
@@ -71,20 +83,27 @@ class FlightWaypoint:
self.alt_type = "BARO"
self.name = ""
self.description = ""
- self.targets = []
- self.targetGroup = None
+ self.targets: List[MissionTarget] = []
self.obj_name = ""
self.pretty_name = ""
- self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
self.only_for_player = False
- self.data = None
+ # These are set very late by the air conflict generator (part of mission
+ # generation). We do it late so that we don't need to propagate changes
+ # to waypoint times whenever the player alters the package TOT or the
+ # flight's offset in the UI.
+ self.tot: Optional[timedelta] = None
+ self.departure_time: Optional[timedelta] = None
+
+ @property
+ def position(self) -> Point:
+ return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
- waypoint = FlightWaypoint(point.position.x, point.position.y,
- point.alt)
+ waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
+ point.position.y, point.alt)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
@@ -108,43 +127,36 @@ class FlightWaypoint:
class Flight:
- unit_type: UnitType = None
- from_cp = None
- points: List[FlightWaypoint] = []
- flight_type: FlightType = None
- count: int = 0
- client_count: int = 0
- targets = []
- use_custom_loadout = False
- loadout = {}
- preset_loadout_name = ""
- start_type = "Runway"
- group = False # Contains DCS Mission group data after mission has been generated
- # How long before this flight should take off
- scheduled_in = 0
-
- def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType):
+ def __init__(self, package: Package, unit_type: FlyingType, count: int,
+ from_cp: ControlPoint, flight_type: FlightType,
+ start_type: str) -> None:
+ self.package = package
self.unit_type = unit_type
self.count = count
self.from_cp = from_cp
self.flight_type = flight_type
- self.points = []
- self.targets = []
- self.loadout = {}
- self.start_type = "Runway"
+ # TODO: Replace with FlightPlan.
+ self.targets: List[MissionTarget] = []
+ self.loadout: Dict[str, str] = {}
+ self.start_type = start_type
+ self.use_custom_loadout = False
+ self.client_count = 0
+
+ # Will be replaced with a more appropriate FlightPlan by
+ # FlightPlanBuilder, but an empty flight plan the flight begins with an
+ # empty flight plan.
+ from gen.flights.flightplan import CustomFlightPlan
+ self.flight_plan: FlightPlan = CustomFlightPlan(
+ package=package,
+ flight=self,
+ custom_waypoints=[]
+ )
+
+ @property
+ def points(self) -> List[FlightWaypoint]:
+ return self.flight_plan.waypoints[1:]
def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
- + " in " + str(self.scheduled_in) + " minutes (" + str(len(self.points)) + " wpt)"
-
-
-# Test
-if __name__ == '__main__':
- from pydcs.dcs.planes import A_10C
- from theater import ControlPoint, Point, List
-
- from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0)
- f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
- f.scheduled_in = 50
- print(f)
+ + " (" + str(len(self.points)) + " wpt)"
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
new file mode 100644
index 00000000..e0df2b01
--- /dev/null
+++ b/gen/flights/flightplan.py
@@ -0,0 +1,1006 @@
+"""Flight plan generation.
+
+Flights are first planned generically by either the player or by the
+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
+
+from datetime import timedelta
+from functools import cached_property
+import logging
+import random
+from dataclasses import dataclass
+from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
+
+from dcs.mapping import Point
+from dcs.unit import Unit
+
+from game.data.doctrine import Doctrine
+from game.utils import nm_to_meter
+from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
+from .closestairfields import ObjectiveDistanceCache
+from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
+from .traveltime import GroundSpeed, TravelTime
+from .waypointbuilder import StrikeTarget, WaypointBuilder
+from ..conflictgen import Conflict
+
+if TYPE_CHECKING:
+ from game import Game
+ from gen.ato import Package
+
+
+INGRESS_TYPES = {
+ FlightWaypointType.INGRESS_CAS,
+ FlightWaypointType.INGRESS_ESCORT,
+ FlightWaypointType.INGRESS_SEAD,
+ FlightWaypointType.INGRESS_STRIKE,
+ FlightWaypointType.INGRESS_DEAD,
+}
+
+
+class PlanningError(RuntimeError):
+ """Raised when the flight planner was unable to create a flight plan."""
+
+
+class InvalidObjectiveLocation(PlanningError):
+ """Raised when the objective location is invalid for the mission type."""
+ def __init__(self, task: FlightType, location: MissionTarget) -> None:
+ super().__init__(
+ f"{location.name} is not valid for {task.name} missions."
+ )
+
+
+@dataclass(frozen=True)
+class FlightPlan:
+ package: Package
+ flight: Flight
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ """A list of all waypoints in the flight plan, in order."""
+ raise NotImplementedError
+
+ @property
+ def edges(self) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
+ """A list of all paths between waypoints, in order."""
+ return zip(self.waypoints, self.waypoints[1:])
+
+ def best_speed_between_waypoints(self, a: FlightWaypoint,
+ b: FlightWaypoint) -> int:
+ """Desired ground speed between points a and b."""
+ factor = 1.0
+ if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
+ # Flights that start airborne already have some altitude and a good
+ # amount of speed.
+ factor = 0.5
+ elif b.waypoint_type == FlightWaypointType.LOITER:
+ # On the way to the hold point the AI won't climb unless they're in
+ # formation, so slowing down the flight lead gives them more time to
+ # form up and climb.
+ # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/dcs-wishlist-aa/7121300-ai-flights-will-not-climb-to-hold-point-because-wingman-not-joined
+ #
+ # Plus, it's a loiter point so there's no reason to hurry.
+ factor = 0.75
+ # 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
+ # near 2000 ft MSL.
+ return int(
+ GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor)
+
+ def speed_between_waypoints(self, a: FlightWaypoint,
+ b: FlightWaypoint) -> int:
+ return self.best_speed_between_waypoints(a, b)
+
+ @property
+ def tot_waypoint(self) -> Optional[FlightWaypoint]:
+ """The waypoint that is associated with the package TOT, or None.
+
+ Note that the only flight plans that should have no target waypoints are
+ user-planned missions without any useful waypoints and flight plans that
+ failed to generate. Nevertheless, we have to defend against it.
+ """
+ raise NotImplementedError
+
+ # Not cached because changes to the package might alter the formation speed.
+ @property
+ def travel_time_to_target(self) -> Optional[timedelta]:
+ """The estimated time between the first waypoint and the target."""
+ if self.tot_waypoint is None:
+ return None
+ return self._travel_time_to_waypoint(self.tot_waypoint)
+
+ def _travel_time_to_waypoint(
+ self, destination: FlightWaypoint) -> timedelta:
+ total = timedelta()
+ for previous_waypoint, waypoint in self.edges:
+ total += self.travel_time_between_waypoints(previous_waypoint,
+ waypoint)
+ if waypoint == destination:
+ break
+ else:
+ raise PlanningError(
+ f"Did not find destination waypoint {destination} in "
+ f"waypoints for {self.flight}")
+ return total
+
+ def travel_time_between_waypoints(self, a: FlightWaypoint,
+ b: FlightWaypoint) -> timedelta:
+ return TravelTime.between_points(a.position, b.position,
+ self.speed_between_waypoints(a, b))
+
+ def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ raise NotImplementedError
+
+ def depart_time_for_waypoint(
+ self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ raise NotImplementedError
+
+ def request_escort_at(self) -> Optional[FlightWaypoint]:
+ return None
+
+ def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
+ return None
+
+
+@dataclass(frozen=True)
+class FormationFlightPlan(FlightPlan):
+ hold: FlightWaypoint
+ join: FlightWaypoint
+ split: FlightWaypoint
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ raise NotImplementedError
+
+ @property
+ def package_speed_waypoints(self) -> Set[FlightWaypoint]:
+ raise NotImplementedError
+
+ @property
+ def tot_waypoint(self) -> Optional[FlightWaypoint]:
+ raise NotImplementedError
+
+ def request_escort_at(self) -> Optional[FlightWaypoint]:
+ return self.join
+
+ def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
+ return self.split
+
+ @cached_property
+ def best_flight_formation_speed(self) -> int:
+ """The best speed this flight is capable at all formation waypoints.
+
+ To ease coordination with other flights, we aim to have a single mission
+ speed used by the formation for all waypoints. As such, this function
+ returns the highest ground speed that the flight is capable of flying at
+ all of its formation waypoints.
+ """
+ speeds = []
+ for previous_waypoint, waypoint in self.edges:
+ if waypoint in self.package_speed_waypoints:
+ speeds.append(self.best_speed_between_waypoints(
+ previous_waypoint, waypoint))
+ return min(speeds)
+
+ def speed_between_waypoints(self, a: FlightWaypoint,
+ b: FlightWaypoint) -> int:
+ if b in self.package_speed_waypoints:
+ # Should be impossible, as any package with at least one
+ # FormationFlightPlan flight needs a formation speed.
+ assert self.package.formation_speed is not None
+ return self.package.formation_speed
+ return super().speed_between_waypoints(a, b)
+
+ @property
+ def travel_time_to_rendezvous(self) -> timedelta:
+ """The estimated time between the first waypoint and the join point."""
+ return self._travel_time_to_waypoint(self.join)
+
+ @property
+ def join_time(self) -> timedelta:
+ raise NotImplementedError
+
+ @property
+ def split_time(self) -> timedelta:
+ raise NotImplementedError
+
+ def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.join:
+ return self.join_time
+ elif waypoint == self.split:
+ return self.split_time
+ return None
+
+ def depart_time_for_waypoint(
+ self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.hold:
+ return self.push_time
+ return None
+
+ @property
+ def push_time(self) -> timedelta:
+ return self.join_time - TravelTime.between_points(
+ self.hold.position,
+ self.join.position,
+ GroundSpeed.for_flight(self.flight, self.hold.alt)
+ )
+
+
+@dataclass(frozen=True)
+class PatrollingFlightPlan(FlightPlan):
+ patrol_start: FlightWaypoint
+ patrol_end: FlightWaypoint
+
+ #: Maximum time to remain on station.
+ patrol_duration: timedelta
+
+ @property
+ def patrol_start_time(self) -> timedelta:
+ return self.package.time_over_target
+
+ @property
+ def patrol_end_time(self) -> timedelta:
+ # TODO: This is currently wrong for CAS.
+ # CAS missions end when they're winchester or bingo. We need to
+ # configure push tasks for the escorts rather than relying on timing.
+ return self.patrol_start_time + self.patrol_duration
+
+ def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.patrol_start:
+ return self.patrol_start_time
+ return None
+
+ def depart_time_for_waypoint(
+ self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.patrol_end:
+ return self.patrol_end_time
+ return None
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ raise NotImplementedError
+
+ @property
+ def package_speed_waypoints(self) -> Set[FlightWaypoint]:
+ return {self.patrol_start, self.patrol_end}
+
+ @property
+ def tot_waypoint(self) -> Optional[FlightWaypoint]:
+ return self.patrol_start
+
+
+@dataclass(frozen=True)
+class BarCapFlightPlan(PatrollingFlightPlan):
+ takeoff: FlightWaypoint
+ ascent: FlightWaypoint
+ descent: FlightWaypoint
+ land: FlightWaypoint
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ return [
+ self.takeoff,
+ self.ascent,
+ self.patrol_start,
+ self.patrol_end,
+ self.descent,
+ self.land,
+ ]
+
+
+@dataclass(frozen=True)
+class CasFlightPlan(PatrollingFlightPlan):
+ takeoff: FlightWaypoint
+ ascent: FlightWaypoint
+ target: FlightWaypoint
+ descent: FlightWaypoint
+ land: FlightWaypoint
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ return [
+ self.takeoff,
+ self.ascent,
+ self.patrol_start,
+ self.target,
+ self.patrol_end,
+ self.descent,
+ self.land,
+ ]
+
+ def request_escort_at(self) -> Optional[FlightWaypoint]:
+ return self.patrol_start
+
+ def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
+ return self.patrol_end
+
+
+@dataclass(frozen=True)
+class FrontLineCapFlightPlan(PatrollingFlightPlan):
+ takeoff: FlightWaypoint
+ ascent: FlightWaypoint
+ descent: FlightWaypoint
+ land: FlightWaypoint
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ return [
+ self.takeoff,
+ self.ascent,
+ self.patrol_start,
+ self.patrol_end,
+ self.descent,
+ self.land,
+ ]
+
+ def depart_time_for_waypoint(
+ self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.patrol_end:
+ return self.patrol_end_time
+ return super().depart_time_for_waypoint(waypoint)
+
+ @property
+ def patrol_start_time(self) -> timedelta:
+ start = self.package.escort_start_time
+ if start is not None:
+ return start
+ return super().patrol_start_time
+
+ @property
+ def patrol_end_time(self) -> timedelta:
+ end = self.package.escort_end_time
+ if end is not None:
+ return end
+ return super().patrol_end_time
+
+
+@dataclass(frozen=True)
+class StrikeFlightPlan(FormationFlightPlan):
+ takeoff: FlightWaypoint
+ ascent: FlightWaypoint
+ hold: FlightWaypoint
+ join: FlightWaypoint
+ ingress: FlightWaypoint
+ targets: List[FlightWaypoint]
+ egress: FlightWaypoint
+ split: FlightWaypoint
+ descent: FlightWaypoint
+ land: FlightWaypoint
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ return [
+ self.takeoff,
+ self.ascent,
+ self.hold,
+ self.join,
+ self.ingress
+ ] + self.targets + [
+ self.egress,
+ self.split,
+ self.descent,
+ self.land,
+ ]
+
+ @property
+ def package_speed_waypoints(self) -> Set[FlightWaypoint]:
+ return {
+ self.ingress,
+ self.egress,
+ self.split,
+ } | set(self.targets)
+
+ def speed_between_waypoints(self, a: FlightWaypoint,
+ b: FlightWaypoint) -> int:
+ # FlightWaypoint is only comparable by identity, so adding
+ # target_area_waypoint to package_speed_waypoints is useless.
+ if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
+ # Should be impossible, as any package with at least one
+ # FormationFlightPlan flight needs a formation speed.
+ assert self.package.formation_speed is not None
+ return self.package.formation_speed
+ return super().speed_between_waypoints(a, b)
+
+ @property
+ def tot_waypoint(self) -> FlightWaypoint:
+ return self.targets[0]
+
+ @property
+ def target_area_waypoint(self) -> FlightWaypoint:
+ return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
+ self.package.target.position.x,
+ self.package.target.position.y, 0)
+
+ @property
+ def travel_time_to_target(self) -> timedelta:
+ """The estimated time between the first waypoint and the target."""
+ destination = self.tot_waypoint
+ total = timedelta()
+ for previous_waypoint, waypoint in self.edges:
+ if waypoint == self.tot_waypoint:
+ # For anything strike-like the TOT waypoint is the *flight's*
+ # mission target, but to synchronize with the rest of the
+ # package we need to use the travel time to the same position as
+ # the others.
+ total += self.travel_time_between_waypoints(
+ previous_waypoint, self.target_area_waypoint)
+ break
+ total += self.travel_time_between_waypoints(previous_waypoint,
+ waypoint)
+ else:
+ raise PlanningError(
+ f"Did not find destination waypoint {destination} in "
+ f"waypoints for {self.flight}")
+ return total
+
+ @property
+ def mission_speed(self) -> int:
+ return GroundSpeed.for_flight(self.flight, self.ingress.alt)
+
+ @property
+ def join_time(self) -> timedelta:
+ travel_time = self.travel_time_between_waypoints(
+ self.join, self.ingress)
+ return self.ingress_time - travel_time
+
+ @property
+ def split_time(self) -> timedelta:
+ travel_time = self.travel_time_between_waypoints(
+ self.egress, self.split)
+ return self.egress_time + travel_time
+
+ @property
+ def ingress_time(self) -> timedelta:
+ tot = self.package.time_over_target
+ travel_time = self.travel_time_between_waypoints(
+ self.ingress, self.target_area_waypoint)
+ return tot - travel_time
+
+ @property
+ def egress_time(self) -> timedelta:
+ tot = self.package.time_over_target
+ travel_time = self.travel_time_between_waypoints(
+ self.target_area_waypoint, self.egress)
+ return tot + travel_time
+
+ def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.ingress:
+ return self.ingress_time
+ elif waypoint == self.egress:
+ return self.egress_time
+ elif waypoint in self.targets:
+ return self.package.time_over_target
+ return super().tot_for_waypoint(waypoint)
+
+
+@dataclass(frozen=True)
+class CustomFlightPlan(FlightPlan):
+ custom_waypoints: List[FlightWaypoint]
+
+ @property
+ def waypoints(self) -> List[FlightWaypoint]:
+ return self.custom_waypoints
+
+ @property
+ def tot_waypoint(self) -> Optional[FlightWaypoint]:
+ target_types = (
+ FlightWaypointType.PATROL_TRACK,
+ FlightWaypointType.TARGET_GROUP_LOC,
+ FlightWaypointType.TARGET_POINT,
+ FlightWaypointType.TARGET_SHIP,
+ )
+ for waypoint in self.waypoints:
+ if waypoint in target_types:
+ return waypoint
+ return None
+
+ def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ if waypoint == self.tot_waypoint:
+ return self.package.time_over_target
+ return None
+
+ def depart_time_for_waypoint(
+ self, waypoint: FlightWaypoint) -> Optional[timedelta]:
+ return None
+
+
+class FlightPlanBuilder:
+ """Generates flight plans for flights."""
+
+ def __init__(self, game: Game, package: Package, is_player: bool) -> None:
+ # TODO: Plan similar altitudes for the in-country leg of the mission.
+ # Waypoint altitudes for a given flight *shouldn't* differ too much
+ # between the join and split points, so we don't need speeds for each
+ # leg individually since they should all be fairly similar. This doesn't
+ # hold too well right now since nothing is stopping each waypoint from
+ # jumping 20k feet each time, but that's a huge waste of energy we
+ # should be avoiding anyway.
+ self.game = game
+ self.package = package
+ self.is_player = is_player
+ if is_player:
+ faction = self.game.player_faction
+ else:
+ faction = self.game.enemy_faction
+ self.doctrine: Doctrine = faction.doctrine
+
+ def populate_flight_plan(
+ self, flight: Flight,
+ # TODO: Custom targets should be an attribute of the flight.
+ custom_targets: Optional[List[Unit]] = None) -> None:
+ """Creates a default flight plan for the given mission."""
+ if flight not in self.package.flights:
+ raise RuntimeError("Flight must be a part of the package")
+ if self.package.waypoints is None:
+ self.regenerate_package_waypoints()
+
+ try:
+ flight_plan = self.generate_flight_plan(flight, custom_targets)
+ except PlanningError:
+ logging.exception(f"Could not create flight plan")
+ return
+ flight.flight_plan = flight_plan
+
+ def generate_flight_plan(
+ self, flight: Flight,
+ custom_targets: Optional[List[Unit]]) -> FlightPlan:
+ # TODO: Flesh out mission types.
+ task = flight.flight_type
+ if task == FlightType.BARCAP:
+ return self.generate_barcap(flight)
+ elif task == FlightType.CAS:
+ return self.generate_cas(flight)
+ elif task == FlightType.DEAD:
+ return self.generate_dead(flight, custom_targets)
+ elif task == FlightType.ESCORT:
+ return self.generate_escort(flight)
+ elif task == FlightType.SEAD:
+ return self.generate_sead(flight, custom_targets)
+ elif task == FlightType.STRIKE:
+ return self.generate_strike(flight)
+ elif task == FlightType.TARCAP:
+ return self.generate_frontline_cap(flight)
+ elif task == FlightType.TROOP_TRANSPORT:
+ logging.error(
+ "Troop transport flight plan generation not implemented"
+ )
+ raise PlanningError(
+ f"{task.name} flight plan generation not implemented")
+
+ def regenerate_package_waypoints(self) -> None:
+ ingress_point = self._ingress_point()
+ egress_point = self._egress_point()
+ join_point = self._join_point(ingress_point)
+ split_point = self._split_point(egress_point)
+
+ from gen.ato import PackageWaypoints
+ self.package.waypoints = PackageWaypoints(
+ join_point,
+ ingress_point,
+ egress_point,
+ split_point,
+ )
+
+ def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
+ """Generates a strike flight plan.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ """
+ location = self.package.target
+
+ # TODO: Support airfield strikes.
+ if not isinstance(location, TheaterGroundObject):
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ targets: List[StrikeTarget] = []
+ if len(location.groups) > 0 and location.dcs_identifier == "AA":
+ # TODO: Replace with DEAD?
+ # Strike missions on SEAD targets target units.
+ for g in location.groups:
+ for j, u in enumerate(g.units):
+ targets.append(StrikeTarget(f"{u.type} #{j}", u))
+ else:
+ # TODO: Does this actually happen?
+ # ConflictTheater is built with the belief that multiple ground
+ # objects have the same name. If that's the case,
+ # TheaterGroundObject needs some refactoring because it behaves very
+ # differently for SAM sites than it does for strike targets.
+ buildings = self.game.theater.find_ground_objects_by_obj_name(
+ location.obj_name
+ )
+ for building in buildings:
+ if building.is_dead:
+ continue
+
+ targets.append(StrikeTarget(building.category, building))
+
+ return self.strike_flightplan(flight, location, targets)
+
+ def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
+ """Generate a BARCAP flight at a given location.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ """
+ location = self.package.target
+
+ if isinstance(location, FrontLine):
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ patrol_alt = random.randint(
+ self.doctrine.min_patrol_altitude,
+ self.doctrine.max_patrol_altitude
+ )
+
+ closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
+ for airfield in closest_cache.closest_airfields:
+ # If the mission is a BARCAP of an enemy airfield, find the *next*
+ # closest enemy airfield.
+ if airfield == self.package.target:
+ continue
+ if airfield.captured != self.is_player:
+ closest_airfield = airfield
+ break
+ else:
+ raise PlanningError("Could not find any enemy airfields")
+
+ heading = location.position.heading_between_point(
+ closest_airfield.position
+ )
+
+ min_distance_from_enemy = nm_to_meter(20)
+ distance_to_airfield = int(closest_airfield.position.distance_to_point(
+ self.package.target.position
+ ))
+ distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
+ min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
+ distance_to_no_fly)
+ max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
+ distance_to_no_fly)
+
+ end = location.position.point_from_heading(
+ heading,
+ random.randint(min_cap_distance, max_cap_distance)
+ )
+ diameter = random.randint(
+ self.doctrine.cap_min_track_length,
+ self.doctrine.cap_max_track_length
+ )
+ start = end.point_from_heading(heading - 180, diameter)
+
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ start, end = builder.race_track(start, end, patrol_alt)
+ descent, land = builder.rtb(flight.from_cp)
+
+ return BarCapFlightPlan(
+ package=self.package,
+ flight=flight,
+ patrol_duration=self.doctrine.cap_duration,
+ takeoff=builder.takeoff(flight.from_cp),
+ ascent=builder.ascent(flight.from_cp),
+ patrol_start=start,
+ patrol_end=end,
+ descent=descent,
+ land=land
+ )
+
+ def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
+ """Generate a CAP flight plan for the given front line.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ """
+ location = self.package.target
+
+ if not isinstance(location, FrontLine):
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ ally_cp, enemy_cp = location.control_points
+ patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
+ self.doctrine.max_patrol_altitude)
+
+ # Find targets waypoints
+ ingress, heading, distance = Conflict.frontline_vector(
+ ally_cp, enemy_cp, self.game.theater
+ )
+ center = ingress.point_from_heading(heading, distance / 2)
+ orbit_center = center.point_from_heading(
+ heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
+ )
+
+ combat_width = distance / 2
+ if combat_width > 500000:
+ combat_width = 500000
+ if combat_width < 35000:
+ combat_width = 35000
+
+ radius = combat_width*1.25
+ orbit0p = orbit_center.point_from_heading(heading, radius)
+ orbit1p = orbit_center.point_from_heading(heading + 180, radius)
+
+ # Create points
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+
+ start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
+ descent, land = builder.rtb(flight.from_cp)
+ return FrontLineCapFlightPlan(
+ package=self.package,
+ flight=flight,
+ # Note that this duration only has an effect if there are no
+ # flights in the package that have requested escort. If the package
+ # requests an escort the CAP flight will remain on station for the
+ # duration of the escorted mission, or until it is winchester/bingo.
+ patrol_duration=self.doctrine.cap_duration,
+ takeoff=builder.takeoff(flight.from_cp),
+ ascent=builder.ascent(flight.from_cp),
+ patrol_start=start,
+ patrol_end=end,
+ descent=descent,
+ land=land
+ )
+
+ def generate_dead(self, flight: Flight,
+ custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
+ """Generate a DEAD flight at a given location.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ custom_targets: Specific radar equipped units selected by the user.
+ """
+ location = self.package.target
+
+ if not isinstance(location, TheaterGroundObject):
+ logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}")
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ # TODO: Unify these.
+ # There doesn't seem to be any reason to treat the UI fragged missions
+ # different from the automatic missions.
+ targets: Optional[List[StrikeTarget]] = None
+ if custom_targets is not None:
+ targets = []
+ for target in custom_targets:
+ targets.append(StrikeTarget(location.name, target))
+
+ return self.strike_flightplan(flight, location, targets)
+
+ def generate_sead(self, flight: Flight,
+ custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
+ """Generate a SEAD flight at a given location.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ custom_targets: Specific radar equipped units selected by the user.
+ """
+ location = self.package.target
+
+ if not isinstance(location, TheaterGroundObject):
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ # TODO: Unify these.
+ # There doesn't seem to be any reason to treat the UI fragged missions
+ # different from the automatic missions.
+ targets: Optional[List[StrikeTarget]] = None
+ if custom_targets is not None:
+ targets = []
+ for target in custom_targets:
+ targets.append(StrikeTarget(location.name, target))
+
+ return self.strike_flightplan(flight, location, targets)
+
+ def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
+ assert self.package.waypoints is not None
+
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ ingress, target, egress = builder.escort(
+ self.package.waypoints.ingress, self.package.target,
+ self.package.waypoints.egress)
+ descent, land = builder.rtb(flight.from_cp)
+
+ return StrikeFlightPlan(
+ package=self.package,
+ flight=flight,
+ takeoff=builder.takeoff(flight.from_cp),
+ ascent=builder.ascent(flight.from_cp),
+ hold=builder.hold(self._hold_point(flight)),
+ join=builder.join(self.package.waypoints.join),
+ ingress=ingress,
+ targets=[target],
+ egress=egress,
+ split=builder.split(self.package.waypoints.split),
+ descent=descent,
+ land=land
+ )
+
+ def generate_cas(self, flight: Flight) -> CasFlightPlan:
+ """Generate a CAS flight plan for the given target.
+
+ Args:
+ flight: The flight to generate the flight plan for.
+ """
+ location = self.package.target
+
+ if not isinstance(location, FrontLine):
+ raise InvalidObjectiveLocation(flight.flight_type, location)
+
+ ingress, heading, distance = Conflict.frontline_vector(
+ location.control_points[0], location.control_points[1],
+ self.game.theater
+ )
+ center = ingress.point_from_heading(heading, distance / 2)
+ egress = ingress.point_from_heading(heading, distance)
+
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ descent, land = builder.rtb(flight.from_cp)
+
+ return CasFlightPlan(
+ package=self.package,
+ flight=flight,
+ patrol_duration=self.doctrine.cas_duration,
+ takeoff=builder.takeoff(flight.from_cp),
+ ascent=builder.ascent(flight.from_cp),
+ patrol_start=builder.ingress_cas(ingress, location),
+ target=builder.cas(center),
+ patrol_end=builder.egress(egress, location),
+ descent=descent,
+ land=land
+ )
+
+ @staticmethod
+ def target_waypoint(flight: Flight, builder: WaypointBuilder,
+ target: StrikeTarget) -> FlightWaypoint:
+ if flight.flight_type == FlightType.DEAD:
+ return builder.dead_point(target)
+ elif flight.flight_type == FlightType.SEAD:
+ return builder.sead_point(target)
+ else:
+ return builder.strike_point(target)
+
+ @staticmethod
+ def target_area_waypoint(flight: Flight, location: MissionTarget,
+ builder: WaypointBuilder) -> FlightWaypoint:
+ if flight.flight_type == FlightType.DEAD:
+ return builder.dead_area(location)
+ elif flight.flight_type == FlightType.SEAD:
+ return builder.sead_area(location)
+ else:
+ return builder.strike_area(location)
+
+ def _hold_point(self, flight: Flight) -> Point:
+ heading = flight.from_cp.position.heading_between_point(
+ self.package.target.position
+ )
+ return flight.from_cp.position.point_from_heading(
+ heading, nm_to_meter(15)
+ )
+
+ # TODO: Make a model for the waypoint builder and use that in the UI.
+ def generate_ascend_point(self, flight: Flight,
+ departure: ControlPoint) -> FlightWaypoint:
+ """Generate ascend point.
+
+ Args:
+ flight: The flight to generate the descend point for.
+ departure: Departure airfield or carrier.
+ """
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ return builder.ascent(departure)
+
+ def generate_descend_point(self, flight: Flight,
+ arrival: ControlPoint) -> FlightWaypoint:
+ """Generate approach/descend point.
+
+ Args:
+ flight: The flight to generate the descend point for.
+ arrival: Arrival airfield or carrier.
+ """
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ return builder.descent(arrival)
+
+ def generate_rtb_waypoint(self, flight: Flight,
+ arrival: ControlPoint) -> FlightWaypoint:
+ """Generate RTB landing point.
+
+ Args:
+ flight: The flight to generate the landing waypoint for.
+ arrival: Arrival airfield or carrier.
+ """
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
+ return builder.land(arrival)
+
+ def strike_flightplan(
+ self, flight: Flight, location: TheaterGroundObject,
+ targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
+ assert self.package.waypoints is not None
+ builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
+ targets)
+ # sead_types = {FlightType.DEAD, FlightType.SEAD}
+ if flight.flight_type is FlightType.SEAD:
+ ingress = builder.ingress_sead(self.package.waypoints.ingress,
+ location)
+
+ elif flight.flight_type is FlightType.DEAD:
+ ingress = builder.ingress_dead(self.package.waypoints.ingress,
+ location)
+ else:
+ ingress = builder.ingress_strike(self.package.waypoints.ingress,
+ location)
+
+ target_waypoints: List[FlightWaypoint] = []
+ if targets is not None:
+ for target in targets:
+ target_waypoints.append(
+ self.target_waypoint(flight, builder, target))
+ else:
+ target_waypoints.append(
+ self.target_area_waypoint(flight, location, builder))
+
+ descent, land = builder.rtb(flight.from_cp)
+ return StrikeFlightPlan(
+ package=self.package,
+ flight=flight,
+ takeoff=builder.takeoff(flight.from_cp),
+ ascent=builder.ascent(flight.from_cp),
+ hold=builder.hold(self._hold_point(flight)),
+ join=builder.join(self.package.waypoints.join),
+ ingress=ingress,
+ targets=target_waypoints,
+ egress=builder.egress(self.package.waypoints.egress, location),
+ split=builder.split(self.package.waypoints.split),
+ descent=descent,
+ land=land
+ )
+
+ def _join_point(self, ingress_point: Point) -> Point:
+ heading = self._heading_to_package_airfield(ingress_point)
+ return ingress_point.point_from_heading(heading,
+ -self.doctrine.join_distance)
+
+ def _split_point(self, egress_point: Point) -> Point:
+ heading = self._heading_to_package_airfield(egress_point)
+ return egress_point.point_from_heading(heading,
+ -self.doctrine.split_distance)
+
+ def _ingress_point(self) -> Point:
+ heading = self._target_heading_to_package_airfield()
+ return self.package.target.position.point_from_heading(
+ heading - 180 + 25, self.doctrine.ingress_egress_distance
+ )
+
+ def _egress_point(self) -> Point:
+ heading = self._target_heading_to_package_airfield()
+ return self.package.target.position.point_from_heading(
+ heading - 180 - 25, self.doctrine.ingress_egress_distance
+ )
+
+ def _target_heading_to_package_airfield(self) -> int:
+ return self._heading_to_package_airfield(self.package.target.position)
+
+ def _heading_to_package_airfield(self, point: Point) -> int:
+ return self.package_airfield().position.heading_between_point(point)
+
+ def package_airfield(self) -> ControlPoint:
+ # We'll always have a package, but if this is being planned via the UI
+ # it could be the first flight in the package.
+ if not self.package.flights:
+ raise RuntimeError(
+ "Cannot determine source airfield for package with no flights"
+ )
+
+ # The package airfield is either the flight's airfield (when there is no
+ # package) or the closest airfield to the objective that is the
+ # departure airfield for some flight in the package.
+ cache = ObjectiveDistanceCache.get_closest_airfields(
+ self.package.target
+ )
+ for airfield in cache.closest_airfields:
+ for flight in self.package.flights:
+ if flight.from_cp == airfield:
+ return airfield
+ raise RuntimeError(
+ "Could not find any airfield assigned to this package"
+ )
diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py
new file mode 100644
index 00000000..ee9a6c7e
--- /dev/null
+++ b/gen/flights/traveltime.py
@@ -0,0 +1,205 @@
+from __future__ import annotations
+
+import logging
+import math
+from datetime import timedelta
+from typing import Optional, TYPE_CHECKING
+
+from dcs.mapping import Point
+from dcs.unittype import FlyingType
+
+from game.utils import meter_to_nm
+from gen.flights.flight import Flight
+
+if TYPE_CHECKING:
+ from gen.ato import Package
+
+
+class GroundSpeed:
+
+ @classmethod
+ def for_flight(cls, flight: Flight, altitude: int) -> int:
+ if not issubclass(flight.unit_type, FlyingType):
+ raise TypeError("Flight has non-flying unit")
+
+ # TODO: Expose both a cruise speed and target speed.
+ # The cruise speed can be used for ascent, hold, join, and RTB to save
+ # on fuel, but mission speed will be fast enough to keep the flight
+ # safer.
+
+ c_sound_sea_level = 661.5
+
+ # DCS's max speed is in kph at 0 MSL. Convert to knots.
+ max_speed = flight.unit_type.max_speed * 0.539957
+ if max_speed > c_sound_sea_level:
+ # Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
+ # account for heavily loaded jets.
+ return int(cls.from_mach(0.8, altitude))
+
+ # For subsonic aircraft, assume the aircraft can reasonably perform at
+ # 80% of its maximum, and that it can maintain the same mach at altitude
+ # 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.
+ mach = max_speed * 0.8 / c_sound_sea_level
+ return int(cls.from_mach(mach, altitude)) # knots
+
+ @staticmethod
+ def from_mach(mach: float, altitude: int) -> float:
+ """Returns the ground speed in knots for the given mach and altitude.
+
+ Args:
+ mach: The mach number to convert to ground speed.
+ altitude: The altitude in feet.
+
+ Returns:
+ The ground speed corresponding to the given altitude and mach number
+ in knots.
+ """
+ # https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
+ if altitude <= 36152:
+ temperature_f = 59 - 0.00356 * altitude
+ else:
+ # There's another formula for altitudes over 82k feet, but we better
+ # not be planning waypoints that high...
+ temperature_f = -70
+
+ temperature_k = (temperature_f + 459.67) * (5 / 9)
+
+ # https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
+ # Dependent on temperature, but varies very little (+/-0.001)
+ # between -40F and 180F.
+ heat_capacity_ratio = 1.4
+
+ # https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
+ gas_constant = 286 # m^2/s^2/K
+ c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
+ # c_sound is in m/s, convert to knots.
+ return (c_sound * 1.944) * mach
+
+
+class TravelTime:
+ @staticmethod
+ def between_points(a: Point, b: Point, speed: float) -> timedelta:
+ error_factor = 1.1
+ distance = meter_to_nm(a.distance_to_point(b))
+ return timedelta(hours=distance / speed * error_factor)
+
+
+class TotEstimator:
+ # An extra five minutes given as wiggle room. Expected to be spent at the
+ # hold point performing any last minute configuration.
+ HOLD_TIME = timedelta(minutes=5)
+
+ def __init__(self, package: Package) -> None:
+ self.package = package
+
+ def mission_start_time(self, flight: Flight) -> timedelta:
+ takeoff_time = self.takeoff_time_for_flight(flight)
+ startup_time = self.estimate_startup(flight)
+ ground_ops_time = self.estimate_ground_ops(flight)
+ start_time = takeoff_time - startup_time - ground_ops_time
+ # In case FP math has given us some barely below zero time, round to
+ # zero.
+ if math.isclose(start_time.total_seconds(), 0):
+ return timedelta()
+ # Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
+ # and they're not interesting from a mission planning perspective so we
+ # don't want them in the UI.
+ #
+ # Round down so *barely* above zero start times are just zero.
+ return timedelta(seconds=math.floor(start_time.total_seconds()))
+
+ def takeoff_time_for_flight(self, flight: Flight) -> timedelta:
+ travel_time = self.travel_time_to_rendezvous_or_target(flight)
+ if travel_time is None:
+ logging.warning("Found no join point or patrol point. Cannot "
+ f"estimate takeoff time takeoff time for {flight}")
+ # Takeoff immediately.
+ return timedelta()
+
+ from gen.flights.flightplan import FormationFlightPlan
+ if isinstance(flight.flight_plan, FormationFlightPlan):
+ tot = flight.flight_plan.tot_for_waypoint(
+ flight.flight_plan.join)
+ if tot is None:
+ logging.warning(
+ "Could not determine the TOT of the join point. Takeoff "
+ f"time for {flight} will be immediate.")
+ return timedelta()
+ else:
+ tot = self.package.time_over_target
+ return tot - travel_time - self.HOLD_TIME
+
+ def earliest_tot(self) -> timedelta:
+ earliest_tot = max((
+ self.earliest_tot_for_flight(f) for f in self.package.flights
+ )) + self.HOLD_TIME
+
+ # Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
+ # and they're not interesting from a mission planning perspective so we
+ # don't want them in the UI.
+ #
+ # Round up so we don't get negative start times.
+ return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
+
+ def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
+ """Estimate fastest time from mission start to the target position.
+
+ For BARCAP flights, this is time to race track start. This ensures that
+ they are on station at the same time any other package members reach
+ their ingress point.
+
+ For other mission types this is the time to the mission target.
+
+ Args:
+ flight: The flight to get the earliest TOT time for.
+
+ Returns:
+ The earliest possible TOT for the given flight in seconds. Returns 0
+ if an ingress point cannot be found.
+ """
+ time_to_target = self.travel_time_to_target(flight)
+ if time_to_target is None:
+ logging.warning(f"Cannot estimate TOT for {flight}")
+ # Return 0 so this flight's travel time does not affect the rest
+ # of the package.
+ return timedelta()
+ startup = self.estimate_startup(flight)
+ ground_ops = self.estimate_ground_ops(flight)
+ return startup + ground_ops + time_to_target
+
+ @staticmethod
+ def estimate_startup(flight: Flight) -> timedelta:
+ if flight.start_type == "Cold":
+ if flight.client_count:
+ return timedelta(minutes=10)
+ else:
+ # The AI doesn't seem to have a real startup procedure.
+ return timedelta(minutes=2)
+ return timedelta()
+
+ @staticmethod
+ def estimate_ground_ops(flight: Flight) -> timedelta:
+ if flight.start_type in ("Runway", "In Flight"):
+ return timedelta()
+ if flight.from_cp.is_fleet:
+ return timedelta(minutes=2)
+ else:
+ return timedelta(minutes=5)
+
+ @staticmethod
+ def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
+ if flight.flight_plan is None:
+ return None
+ return flight.flight_plan.travel_time_to_target
+
+ @staticmethod
+ def travel_time_to_rendezvous_or_target(
+ flight: Flight) -> Optional[timedelta]:
+ if flight.flight_plan is None:
+ return None
+ from gen.flights.flightplan import FormationFlightPlan
+ if isinstance(flight.flight_plan, FormationFlightPlan):
+ return flight.flight_plan.travel_time_to_rendezvous
+ return flight.flight_plan.travel_time_to_target
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
new file mode 100644
index 00000000..ddc76b5f
--- /dev/null
+++ b/gen/flights/waypointbuilder.py
@@ -0,0 +1,367 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import List, Optional, Tuple, Union
+
+from dcs.mapping import Point
+from dcs.unit import Unit
+
+from game.data.doctrine import Doctrine
+from game.utils import nm_to_meter
+from game.weather import Conditions
+from theater import ControlPoint, MissionTarget, TheaterGroundObject
+from .flight import Flight, FlightWaypoint, FlightWaypointType
+from ..runways import RunwayAssigner
+
+
+@dataclass(frozen=True)
+class StrikeTarget:
+ name: str
+ target: Union[TheaterGroundObject, Unit]
+
+
+class WaypointBuilder:
+ def __init__(self, conditions: Conditions, flight: Flight,
+ doctrine: Doctrine,
+ targets: Optional[List[StrikeTarget]] = None) -> None:
+ self.conditions = conditions
+ self.flight = flight
+ self.doctrine = doctrine
+ self.targets = targets
+
+ @property
+ def is_helo(self) -> bool:
+ return getattr(self.flight.unit_type, "helicopter", False)
+
+ @staticmethod
+ def takeoff(departure: ControlPoint) -> FlightWaypoint:
+ """Create takeoff waypoint for the given arrival airfield or carrier.
+
+ Note that the takeoff waypoint will automatically be created by pydcs
+ when we create the group, but creating our own before generation makes
+ the planning code simpler.
+
+ Args:
+ departure: Departure airfield or carrier.
+ """
+ position = departure.position
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TAKEOFF,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "TAKEOFF"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Takeoff"
+ waypoint.pretty_name = "Takeoff"
+ return waypoint
+
+ def ascent(self, departure: ControlPoint) -> FlightWaypoint:
+ """Create ascent waypoint for the given departure airfield or carrier.
+
+ Args:
+ departure: Departure airfield or carrier.
+ """
+ heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
+ position = departure.position.point_from_heading(
+ heading, nm_to_meter(5)
+ )
+ waypoint = FlightWaypoint(
+ FlightWaypointType.ASCEND_POINT,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.pattern_altitude
+ )
+ waypoint.name = "ASCEND"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Ascend"
+ waypoint.pretty_name = "Ascend"
+ return waypoint
+
+ def descent(self, arrival: ControlPoint) -> FlightWaypoint:
+ """Create descent waypoint for the given arrival airfield or carrier.
+
+ Args:
+ arrival: Arrival airfield or carrier.
+ """
+ landing_heading = RunwayAssigner(self.conditions).landing_heading(
+ arrival)
+ heading = (landing_heading + 180) % 360
+ position = arrival.position.point_from_heading(
+ heading, nm_to_meter(5)
+ )
+ waypoint = FlightWaypoint(
+ FlightWaypointType.DESCENT_POINT,
+ position.x,
+ position.y,
+ 300 if self.is_helo else self.doctrine.pattern_altitude
+ )
+ waypoint.name = "DESCEND"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Descend to pattern altitude"
+ waypoint.pretty_name = "Descend"
+ return waypoint
+
+ @staticmethod
+ def land(arrival: ControlPoint) -> FlightWaypoint:
+ """Create descent waypoint for the given arrival airfield or carrier.
+
+ Args:
+ arrival: Arrival airfield or carrier.
+ """
+ position = arrival.position
+ waypoint = FlightWaypoint(
+ FlightWaypointType.LANDING_POINT,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "LANDING"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Land"
+ waypoint.pretty_name = "Land"
+ return waypoint
+
+ def hold(self, position: Point) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.LOITER,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.rendezvous_altitude
+ )
+ waypoint.pretty_name = "Hold"
+ waypoint.description = "Wait until push time"
+ waypoint.name = "HOLD"
+ return waypoint
+
+ def join(self, position: Point) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.JOIN,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.ingress_altitude
+ )
+ waypoint.pretty_name = "Join"
+ waypoint.description = "Rendezvous with package"
+ waypoint.name = "JOIN"
+ return waypoint
+
+ def split(self, position: Point) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.SPLIT,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.ingress_altitude
+ )
+ waypoint.pretty_name = "Split"
+ waypoint.description = "Depart from package"
+ waypoint.name = "SPLIT"
+ return waypoint
+
+ def ingress_cas(self, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ return self._ingress(FlightWaypointType.INGRESS_CAS, position,
+ objective)
+
+ def ingress_escort(self, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
+ objective)
+
+ def ingress_dead(self, position:Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
+ objective)
+
+ def ingress_sead(self, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
+ objective)
+
+ def ingress_strike(self, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
+ objective)
+
+ def _ingress(self, ingress_type: FlightWaypointType, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ ingress_type,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.ingress_altitude
+ )
+ waypoint.pretty_name = "INGRESS on " + objective.name
+ waypoint.description = "INGRESS on " + objective.name
+ waypoint.name = "INGRESS"
+ # TODO: This seems wrong, but it's what was there before.
+ waypoint.targets.append(objective)
+ return waypoint
+
+ def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.EGRESS,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.ingress_altitude
+ )
+ waypoint.pretty_name = "EGRESS from " + target.name
+ waypoint.description = "EGRESS from " + target.name
+ waypoint.name = "EGRESS"
+ return waypoint
+
+ def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
+ return self._target_point(target, f"STRIKE {target.name}")
+
+ def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
+ return self._target_point(target, f"STRIKE {target.name}")
+
+ def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
+ return self._target_point(target, f"STRIKE {target.name}")
+
+ @staticmethod
+ def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TARGET_POINT,
+ target.target.position.x,
+ target.target.position.y,
+ 0
+ )
+ waypoint.description = description
+ waypoint.pretty_name = description
+ waypoint.name = target.name
+ # The target waypoints are only for the player's benefit. AI tasks for
+ # the target are set on the ingress point so they begin their attack
+ # *before* reaching the target.
+ waypoint.only_for_player = True
+ return waypoint
+
+ def strike_area(self, target: MissionTarget) -> FlightWaypoint:
+ return self._target_area(f"STRIKE {target.name}", target)
+
+ def sead_area(self, target: MissionTarget) -> FlightWaypoint:
+ return self._target_area(f"SEAD on {target.name}", target)
+
+ def dead_area(self, target: MissionTarget) -> FlightWaypoint:
+ return self._target_area(f"DEAD on {target.name}", target)
+
+ @staticmethod
+ def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TARGET_GROUP_LOC,
+ location.position.x,
+ location.position.y,
+ 0
+ )
+ waypoint.description = name
+ waypoint.pretty_name = name
+ waypoint.name = name
+ # The target waypoints are only for the player's benefit. AI tasks for
+ # the target are set on the ingress point so they begin their attack
+ # *before* reaching the target.
+ waypoint.only_for_player = True
+ return waypoint
+
+ def cas(self, position: Point) -> FlightWaypoint:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.CAS,
+ position.x,
+ position.y,
+ 500 if self.is_helo else 1000
+ )
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Provide CAS"
+ waypoint.name = "CAS"
+ waypoint.pretty_name = "CAS"
+ return waypoint
+
+ @staticmethod
+ def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
+ """Creates a racetrack start waypoint.
+
+ Args:
+ position: Position of the waypoint.
+ altitude: Altitude of the racetrack in meters.
+ """
+ waypoint = FlightWaypoint(
+ FlightWaypointType.PATROL_TRACK,
+ position.x,
+ position.y,
+ altitude
+ )
+ waypoint.name = "RACETRACK START"
+ waypoint.description = "Orbit between this point and the next point"
+ waypoint.pretty_name = "Race-track start"
+ return waypoint
+
+ @staticmethod
+ def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
+ """Creates a racetrack end waypoint.
+
+ Args:
+ position: Position of the waypoint.
+ altitude: Altitude of the racetrack in meters.
+ """
+ waypoint = FlightWaypoint(
+ FlightWaypointType.PATROL,
+ position.x,
+ position.y,
+ altitude
+ )
+ waypoint.name = "RACETRACK END"
+ waypoint.description = "Orbit between this point and the previous point"
+ waypoint.pretty_name = "Race-track end"
+ return waypoint
+
+ def race_track(self, start: Point, end: Point,
+ altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
+ """Creates two waypoint for a racetrack orbit.
+
+ Args:
+ start: The beginning racetrack waypoint.
+ end: The ending racetrack waypoint.
+ altitude: The racetrack altitude.
+ """
+ return (self.race_track_start(start, altitude),
+ self.race_track_end(end, altitude))
+
+ def rtb(self,
+ arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]:
+ """Creates descent ant landing waypoints for the given control point.
+
+ Args:
+ arrival: Arrival airfield or carrier.
+ """
+ return self.descent(arrival), self.land(arrival)
+
+ def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
+ Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
+ """Creates the waypoints needed to escort the package.
+
+ Args:
+ ingress: The package ingress point.
+ target: The mission target.
+ egress: The package egress point.
+ """
+ # This would preferably be no points at all, and instead the Escort task
+ # would begin on the join point and end on the split point, however the
+ # escort task does not appear to work properly (see the longer
+ # description in gen.aircraft.JoinPointBuilder), so instead we give
+ # the escort flights a flight plan including the ingress point, target
+ # area, and egress point.
+ ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
+ target)
+
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TARGET_GROUP_LOC,
+ target.position.x,
+ target.position.y,
+ 500 if self.is_helo else self.doctrine.ingress_altitude
+ )
+ waypoint.name = "TARGET"
+ waypoint.description = "Escort the package"
+ waypoint.pretty_name = "Target area"
+
+ egress = self.egress(egress, target)
+ return ingress, waypoint, egress
diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py
index 877c9831..db1deb03 100644
--- a/gen/ground_forces/ai_ground_planner.py
+++ b/gen/ground_forces/ai_ground_planner.py
@@ -1,13 +1,13 @@
import random
from enum import Enum
+from typing import Dict, List
-from dcs.vehicles import *
-
-from gen import Conflict
-from gen.ground_forces.combat_stance import CombatStance
-from theater import ControlPoint
+from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
+from dcs.unittype import VehicleType
import pydcs_extensions.frenchpack.frenchpack as frenchpack
+from gen.ground_forces.combat_stance import CombatStance
+from theater import ControlPoint
TYPE_TANKS = [
Armor.MBT_T_55,
@@ -27,13 +27,14 @@ TYPE_TANKS = [
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
- Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
+ Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
- Armor.ST_Centaur_IV,
+ Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
+ Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
@@ -73,14 +74,15 @@ TYPE_IFV = [
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
- Armor.IFV_Sd_Kfz_234_2_Puma,
+ Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
- Armor.IFV_Sd_Kfz_234_2_Puma,
+ Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
+ Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,
@@ -207,8 +209,8 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
class CombatGroup:
- def __init__(self, role:CombatGroupRole):
- self.units = []
+ def __init__(self, role: CombatGroupRole):
+ self.units: List[VehicleType] = []
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
@@ -222,33 +224,22 @@ class CombatGroup:
class GroundPlanner:
- cp = None
- combat_groups_dict = {}
- connected_enemy_cp = []
-
- tank_groups = []
- apc_group = []
- ifv_group = []
- art_group = []
- shorad_groups = []
- logi_groups = []
-
def __init__(self, cp:ControlPoint, game):
self.cp = cp
self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
- self.tank_groups = []
- self.apc_group = []
- self.ifv_group = []
- self.art_group = []
- self.atgm_group = []
- self.logi_groups = []
- self.shorad_groups = []
+ self.tank_groups: List[CombatGroup] = []
+ self.apc_group: List[CombatGroup] = []
+ self.ifv_group: List[CombatGroup] = []
+ self.art_group: List[CombatGroup] = []
+ self.atgm_group: List[CombatGroup] = []
+ self.logi_groups: List[CombatGroup] = []
+ self.shorad_groups: List[CombatGroup] = []
- self.units_per_cp = {}
+ self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp:
self.units_per_cp[cp.id] = []
- self.reserve = []
+ self.reserve: List[CombatGroup] = []
def plan_groundwar(self):
diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py
index ddf2706e..1989452e 100644
--- a/gen/groundobjectsgen.py
+++ b/gen/groundobjectsgen.py
@@ -1,19 +1,358 @@
-from dcs.statics import *
-from dcs.unit import Ship, Vehicle
+"""Generators for creating the groups for ground objectives.
-from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS
+The classes in this file are responsible for creating the vehicle groups, ship
+groups, statics, missile sites, and AA sites for the mission. Each of these
+objectives is defined in the Theater by a TheaterGroundObject. These classes
+create the pydcs groups and statics for those areas and add them to the mission.
+"""
+from __future__ import annotations
+
+import logging
+import random
+from typing import Dict, Iterator, Optional, TYPE_CHECKING
+
+from dcs import Mission
+from dcs.country import Country
+from dcs.statics import fortification_map, warehouse_map
+from dcs.task import (
+ ActivateBeaconCommand,
+ ActivateICLSCommand,
+ EPLRS,
+ OptAlarmState,
+)
+from dcs.unit import Ship, Vehicle, Unit
+from dcs.unitgroup import Group, ShipGroup, StaticGroup
+from dcs.unittype import StaticType, UnitType
+
+from game import db
+from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
-from .airfields import RunwayData
-from .conflictgen import *
-from .naming import *
-from .radios import RadioRegistry
-from .tacan import TacanBand, TacanRegistry
+from theater import ControlPoint, TheaterGroundObject
+from theater.theatergroundobject import (
+ BuildingGroundObject, CarrierGroundObject,
+ GenericCarrierGroundObject,
+ LhaGroundObject, ShipGroundObject,
+)
+from .conflictgen import Conflict
+from .radios import RadioFrequency, RadioRegistry
+from .runways import RunwayData
+from .tacan import TacanBand, TacanChannel, TacanRegistry
+
+if TYPE_CHECKING:
+ from game import Game
+
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
+class GenericGroundObjectGenerator:
+ """An unspecialized ground object generator.
+
+ Currently used only for SAM and missile (V1/V2) sites.
+ """
+ def __init__(self, ground_object: TheaterGroundObject, country: Country,
+ game: Game, mission: Mission) -> None:
+ self.ground_object = ground_object
+ self.country = country
+ self.game = game
+ self.m = mission
+
+ def generate(self) -> None:
+ if self.game.position_culled(self.ground_object.position):
+ return
+
+ for group in self.ground_object.groups:
+ if not group.units:
+ logging.warning(f"Found empty group in {self.ground_object}")
+ continue
+
+ unit_type = unit_type_from_name(group.units[0].type)
+ if unit_type is None:
+ raise RuntimeError(
+ f"Unrecognized unit type: {group.units[0].type}")
+
+ vg = self.m.vehicle_group(self.country, group.name, unit_type,
+ position=group.position,
+ heading=group.units[0].heading)
+ vg.units[0].name = self.m.string(group.units[0].name)
+ vg.units[0].player_can_drive = True
+ for i, u in enumerate(group.units):
+ if i > 0:
+ vehicle = Vehicle(self.m.next_unit_id(),
+ self.m.string(u.name), u.type)
+ vehicle.position.x = u.position.x
+ vehicle.position.y = u.position.y
+ vehicle.heading = u.heading
+ vehicle.player_can_drive = True
+ vg.add_unit(vehicle)
+
+ self.enable_eplrs(vg, unit_type)
+ self.set_alarm_state(vg)
+
+ @staticmethod
+ def enable_eplrs(group: Group, unit_type: UnitType) -> None:
+ if hasattr(unit_type, 'eplrs'):
+ if unit_type.eplrs:
+ group.points[0].tasks.append(EPLRS(group.id))
+
+ def set_alarm_state(self, group: Group) -> None:
+ if self.game.settings.perf_red_alert_state:
+ group.points[0].tasks.append(OptAlarmState(2))
+ else:
+ group.points[0].tasks.append(OptAlarmState(1))
+
+
+class BuildingSiteGenerator(GenericGroundObjectGenerator):
+ """Generator for building sites.
+
+ Building sites are the primary type of non-airbase objective locations that
+ appear on the map. They come in a handful of variants each with different
+ types of buildings and ground units.
+ """
+
+ def generate(self) -> None:
+ if self.game.position_culled(self.ground_object.position):
+ return
+
+ if self.ground_object.dcs_identifier in warehouse_map:
+ static_type = warehouse_map[self.ground_object.dcs_identifier]
+ self.generate_static(static_type)
+ elif self.ground_object.dcs_identifier in fortification_map:
+ static_type = fortification_map[self.ground_object.dcs_identifier]
+ self.generate_static(static_type)
+ elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
+ for f in FORTIFICATION_UNITS:
+ if f.id == self.ground_object.dcs_identifier:
+ unit_type = f
+ self.generate_vehicle_group(unit_type)
+ break
+ else:
+ logging.error(
+ f"{self.ground_object.dcs_identifier} not found in static maps")
+
+ def generate_vehicle_group(self, unit_type: UnitType) -> None:
+ if not self.ground_object.is_dead:
+ self.m.vehicle_group(
+ country=self.country,
+ name=self.ground_object.group_name,
+ _type=unit_type,
+ position=self.ground_object.position,
+ heading=self.ground_object.heading,
+ )
+
+ def generate_static(self, static_type: StaticType) -> None:
+ self.m.static_group(
+ country=self.country,
+ name=self.ground_object.group_name,
+ _type=static_type,
+ position=self.ground_object.position,
+ heading=self.ground_object.heading,
+ dead=self.ground_object.is_dead,
+ )
+
+
+class GenericCarrierGenerator(GenericGroundObjectGenerator):
+ """Base type for carrier group generation.
+
+ Used by both CV(N) groups and LHA groups.
+ """
+ def __init__(self, ground_object: GenericCarrierGroundObject,
+ control_point: ControlPoint, country: Country, game: Game,
+ mission: Mission, radio_registry: RadioRegistry,
+ tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
+ runways: Dict[str, RunwayData]) -> None:
+ super().__init__(ground_object, country, game, mission)
+ self.ground_object = ground_object
+ self.control_point = control_point
+ self.radio_registry = radio_registry
+ self.tacan_registry = tacan_registry
+ self.icls_alloc = icls_alloc
+ self.runways = runways
+
+ def generate(self) -> None:
+ # TODO: Require single group?
+ for group in self.ground_object.groups:
+ if not group.units:
+ logging.warning(
+ f"Found empty carrier group in {self.control_point}")
+ continue
+
+ atc = self.radio_registry.alloc_uhf()
+ ship_group = self.configure_carrier(group, atc)
+ for unit in group.units[1:]:
+ ship_group.add_unit(self.create_ship(unit, atc))
+
+ tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
+ tacan_callsign = self.tacan_callsign()
+ icls = next(self.icls_alloc)
+
+ brc = self.steam_into_wind(ship_group)
+ self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
+ self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
+
+ def get_carrier_type(self, group: Group) -> UnitType:
+ unit_type = unit_type_from_name(group.units[0].type)
+ if unit_type is None:
+ raise RuntimeError(
+ f"Unrecognized carrier name: {group.units[0].type}")
+ return unit_type
+
+ def configure_carrier(self, group: Group,
+ atc_channel: RadioFrequency) -> ShipGroup:
+ unit_type = self.get_carrier_type(group)
+
+ ship_group = self.m.ship_group(self.country, group.name, unit_type,
+ position=group.position,
+ heading=group.units[0].heading)
+ ship_group.set_frequency(atc_channel.hertz)
+ ship_group.units[0].name = self.m.string(group.units[0].name)
+ return ship_group
+
+ def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
+ ship = Ship(self.m.next_unit_id(),
+ self.m.string(unit.name),
+ unit_type_from_name(unit.type))
+ ship.position.x = unit.position.x
+ ship.position.y = unit.position.y
+ ship.heading = unit.heading
+ # TODO: Verify.
+ ship.set_frequency(atc_channel.hertz)
+ return ship
+
+ def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
+ brc = self.m.weather.wind_at_ground.direction + 180
+ for attempt in range(5):
+ point = group.points[0].position.point_from_heading(
+ brc, 100000 - attempt * 20000)
+ if self.game.theater.is_in_sea(point):
+ group.add_waypoint(point)
+ return brc
+ return None
+
+ def tacan_callsign(self) -> str:
+ raise NotImplementedError
+
+ @staticmethod
+ def activate_beacons(group: ShipGroup, tacan: TacanChannel,
+ callsign: str, icls: int) -> None:
+ group.points[0].tasks.append(ActivateBeaconCommand(
+ channel=tacan.number,
+ modechannel=tacan.band.value,
+ callsign=callsign,
+ unit_id=group.units[0].id,
+ aa=False
+ ))
+ group.points[0].tasks.append(ActivateICLSCommand(
+ icls, unit_id=group.units[0].id
+ ))
+
+ def add_runway_data(self, brc: int, atc: RadioFrequency,
+ tacan: TacanChannel, callsign: str, icls: int) -> None:
+ # TODO: Make unit name usable.
+ # This relies on one control point mapping exactly
+ # to one LHA, carrier, or other usable "runway".
+ # This isn't wholly true, since the DD escorts of
+ # the carrier group are valid for helicopters, but
+ # they aren't exposed as such to the game. Should
+ # clean this up so that's possible. We can't use the
+ # unit name since it's an arbitrary ID.
+ self.runways[self.control_point.name] = RunwayData(
+ self.control_point.name,
+ brc,
+ "N/A",
+ atc=atc,
+ tacan=tacan,
+ tacan_callsign=callsign,
+ icls=icls,
+ )
+
+
+class CarrierGenerator(GenericCarrierGenerator):
+ """Generator for CV(N) groups."""
+
+ def get_carrier_type(self, group: Group) -> UnitType:
+ unit_type = super().get_carrier_type(group)
+ if self.game.settings.supercarrier:
+ unit_type = db.upgrade_to_supercarrier(unit_type,
+ self.control_point.name)
+ return unit_type
+
+ def tacan_callsign(self) -> str:
+ # TODO: Assign these properly.
+ return random.choice([
+ "STE",
+ "CVN",
+ "CVH",
+ "CCV",
+ "ACC",
+ "ARC",
+ "GER",
+ "ABR",
+ "LIN",
+ "TRU",
+ ])
+
+
+class LhaGenerator(GenericCarrierGenerator):
+ """Generator for LHA groups."""
+
+ def tacan_callsign(self) -> str:
+ # TODO: Assign these properly.
+ return random.choice([
+ "LHD",
+ "LHA",
+ "LHB",
+ "LHC",
+ "LHD",
+ "LDS",
+ ])
+
+
+class ShipObjectGenerator(GenericGroundObjectGenerator):
+ """Generator for non-carrier naval groups."""
+
+ def generate(self) -> None:
+ if self.game.position_culled(self.ground_object.position):
+ return
+
+ for group in self.ground_object.groups:
+ if not group.units:
+ logging.warning(f"Found empty group in {self.ground_object}")
+ continue
+
+ unit_type = unit_type_from_name(group.units[0].type)
+ if unit_type is None:
+ raise RuntimeError(
+ f"Unrecognized unit type: {group.units[0].type}")
+
+ self.generate_group(group, unit_type)
+
+ def generate_group(self, group_def: Group, unit_type: UnitType):
+ group = self.m.ship_group(self.country, group_def.name, unit_type,
+ position=group_def.position,
+ heading=group_def.units[0].heading)
+ group.units[0].name = self.m.string(group_def.units[0].name)
+ # TODO: Skipping the first unit looks like copy pasta from the carrier.
+ for unit in group_def.units[1:]:
+ unit_type = unit_type_from_name(unit.type)
+ ship = Ship(self.m.next_unit_id(),
+ self.m.string(unit.name), unit_type)
+ ship.position.x = unit.position.x
+ ship.position.y = unit.position.y
+ ship.heading = unit.heading
+ group.add_unit(ship)
+ self.set_alarm_state(group)
+
+
class GroundObjectsGenerator:
+ """Creates DCS groups and statics for the theater during mission generation.
+
+ Most of the work of group/static generation is delegated to the other
+ generator classes. This class is responsible for finding each of the
+ locations for spawning ground objects, determining their types, and creating
+ the appropriate generators.
+ """
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game,
@@ -26,7 +365,7 @@ class GroundObjectsGenerator:
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
- def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]:
+ def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
if self.conflict.is_vector:
center = self.conflict.center
heading = self.conflict.heading - 90
@@ -49,186 +388,34 @@ class GroundObjectsGenerator:
)
def generate(self):
-
for cp in self.game.theater.controlpoints:
-
if cp.captured:
- country = self.game.player_country
+ country_name = self.game.player_country
else:
- country = self.game.enemy_country
- side = self.m.country(country)
+ country_name = self.game.enemy_country
+ country = self.m.country(country_name)
for ground_object in cp.ground_objects:
- if ground_object.dcs_identifier == "AA":
-
- if self.game.position_culled(ground_object.position):
- continue
-
- for g in ground_object.groups:
- if len(g.units) > 0:
- utype = unit_type_from_name(g.units[0].type)
-
- if not ground_object.sea_object:
- vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
- vg.units[0].name = self.m.string(g.units[0].name)
- vg.units[0].player_can_drive = True
- for i, u in enumerate(g.units):
- if i > 0:
- vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type)
- vehicle.position.x = u.position.x
- vehicle.position.y = u.position.y
- vehicle.heading = u.heading
- vehicle.player_can_drive = True
- vg.add_unit(vehicle)
-
- if hasattr(utype, 'eplrs'):
- if utype.eplrs:
- vg.points[0].tasks.append(EPLRS(vg.id))
- else:
- vg = self.m.ship_group(side, g.name, utype, position=g.position,
- heading=g.units[0].heading)
- vg.units[0].name = self.m.string(g.units[0].name)
- for i, u in enumerate(g.units):
- utype = unit_type_from_name(u.type)
- if i > 0:
- ship = Ship(self.m.next_unit_id(), self.m.string(u.name), utype)
- ship.position.x = u.position.x
- ship.position.y = u.position.y
- ship.heading = u.heading
- vg.add_unit(ship)
-
- if self.game.settings.perf_red_alert_state:
- vg.points[0].tasks.append(OptAlarmState(2))
- else:
- vg.points[0].tasks.append(OptAlarmState(1))
-
-
- elif ground_object.dcs_identifier in ["CARRIER", "LHA"]:
- for g in ground_object.groups:
- if len(g.units) > 0:
-
- utype = unit_type_from_name(g.units[0].type)
- if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True:
- utype = db.upgrade_to_supercarrier(utype, cp.name)
-
- sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
- atc_channel = self.radio_registry.alloc_uhf()
- sg.set_frequency(atc_channel.hertz)
- sg.units[0].name = self.m.string(g.units[0].name)
-
- for i, u in enumerate(g.units):
- if i > 0:
- ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type))
- ship.position.x = u.position.x
- ship.position.y = u.position.y
- ship.heading = u.heading
- # TODO: Verify.
- ship.set_frequency(atc_channel.hertz)
- sg.add_unit(ship)
-
- # Find carrier direction (In the wind)
- found_carrier_destination = False
- attempt = 0
- while not found_carrier_destination and attempt < 5:
- point = sg.points[0].position.point_from_heading(self.m.weather.wind_at_ground.direction + 180, 100000-attempt*20000)
- if self.game.theater.is_in_sea(point):
- found_carrier_destination = True
- sg.add_waypoint(point)
- else:
- attempt = attempt + 1
-
- # Set UP TACAN and ICLS
- tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
- icls_channel = next(self.icls_alloc)
- # TODO: Assign these properly.
- if ground_object.dcs_identifier == "CARRIER":
- tacan_callsign = random.choice([
- "STE",
- "CVN",
- "CVH",
- "CCV",
- "ACC",
- "ARC",
- "GER",
- "ABR",
- "LIN",
- "TRU",
- ])
- else:
- tacan_callsign = random.choice([
- "LHD",
- "LHA",
- "LHB",
- "LHC",
- "LHD",
- "LDS",
- ])
- sg.points[0].tasks.append(ActivateBeaconCommand(
- channel=tacan.number,
- modechannel=tacan.band.value,
- callsign=tacan_callsign,
- unit_id=sg.units[0].id,
- aa=False
- ))
- sg.points[0].tasks.append(ActivateICLSCommand(
- icls_channel,
- unit_id=sg.units[0].id
- ))
- # TODO: Make unit name usable.
- # This relies on one control point mapping exactly
- # to one LHA, carrier, or other usable "runway".
- # This isn't wholly true, since the DD escorts of
- # the carrier group are valid for helicopters, but
- # they aren't exposed as such to the game. Should
- # clean this up so that's possible. We can't use the
- # unit name since it's an arbitrary ID.
- self.runways[cp.name] = RunwayData(
- cp.name,
- "N/A",
- atc=atc_channel,
- tacan=tacan,
- tacan_callsign=tacan_callsign,
- icls=icls_channel,
- )
-
+ if isinstance(ground_object, BuildingGroundObject):
+ generator = BuildingSiteGenerator(ground_object, country,
+ self.game, self.m)
+ elif isinstance(ground_object, CarrierGroundObject):
+ generator = CarrierGenerator(ground_object, cp, country,
+ self.game, self.m,
+ self.radio_registry,
+ self.tacan_registry,
+ self.icls_alloc, self.runways)
+ elif isinstance(ground_object, LhaGroundObject):
+ generator = CarrierGenerator(ground_object, cp, country,
+ self.game, self.m,
+ self.radio_registry,
+ self.tacan_registry,
+ self.icls_alloc, self.runways)
+ elif isinstance(ground_object, ShipGroundObject):
+ generator = ShipObjectGenerator(ground_object, country,
+ self.game, self.m)
else:
-
- if self.game.position_culled(ground_object.position):
- continue
-
- static_type = None
- if ground_object.dcs_identifier in warehouse_map:
- static_type = warehouse_map[ground_object.dcs_identifier]
- elif ground_object.dcs_identifier in fortification_map:
- static_type = fortification_map[ground_object.dcs_identifier]
- elif ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
- for f in FORTIFICATION_UNITS:
- if f.id == ground_object.dcs_identifier:
- unit_type = f
- break
- else:
- print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier))
- continue
-
- if static_type is None:
- if not ground_object.is_dead:
- group = self.m.vehicle_group(
- country=side,
- name=ground_object.string_identifier,
- _type=unit_type,
- position=ground_object.position,
- heading=ground_object.heading,
- )
- logging.info("generated {}object identifier {} with mission id {}".format(
- "dead " if ground_object.is_dead else "", group.name, group.id))
- else:
- group = self.m.static_group(
- country=side,
- name=ground_object.string_identifier,
- _type=static_type,
- position=ground_object.position,
- heading=ground_object.heading,
- dead=ground_object.is_dead,
- )
-
- logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id))
\ No newline at end of file
+ generator = GenericGroundObjectGenerator(ground_object,
+ country, self.game,
+ self.m)
+ generator.generate()
diff --git a/gen/kneeboard.py b/gen/kneeboard.py
index 6843e395..7a5794ab 100644
--- a/gen/kneeboard.py
+++ b/gen/kneeboard.py
@@ -22,25 +22,28 @@ https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can
only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
+import datetime
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
from dcs.unittype import FlyingType
from tabulate import tabulate
+from game.utils import meter_to_nm
from . import units
from .aircraft import AIRCRAFT_DATA, FlightData
-from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency
+from .runways import RunwayData
-
+if TYPE_CHECKING:
+ from game import Game
class KneeboardPageWriter:
"""Creates kneeboard images."""
@@ -82,6 +85,8 @@ class KneeboardPageWriter:
def table(self, cells: List[List[str]],
headers: Optional[List[str]] = None) -> None:
+ if headers is None:
+ headers = []
table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font)
@@ -104,9 +109,11 @@ class NumberedWaypoint:
class FlightPlanBuilder:
- def __init__(self) -> None:
+ def __init__(self, start_time: datetime.datetime) -> None:
+ self.start_time = start_time
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
+ self.last_waypoint: Optional[FlightWaypoint] = None
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
@@ -118,6 +125,7 @@ class FlightPlanBuilder:
self.target_points = []
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
+ self.last_waypoint = waypoint
def coalesce_target_points(self) -> None:
if len(self.target_points) <= 4:
@@ -131,16 +139,60 @@ class FlightPlanBuilder:
self.rows.append([
f"{first_waypoint_num}-{last_waypoint_num}",
"Target points",
- "0"
+ "0",
+ self._waypoint_distance(self.target_points[0].waypoint),
+ self._ground_speed(self.target_points[0].waypoint),
+ self._format_time(self.target_points[0].waypoint.tot),
+ self._format_time(self.target_points[0].waypoint.departure_time),
])
+ self.last_waypoint = self.target_points[-1].waypoint
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
- waypoint.number,
+ str(waypoint.number),
waypoint.waypoint.pretty_name,
- str(int(units.meters_to_feet(waypoint.waypoint.alt)))
+ str(int(units.meters_to_feet(waypoint.waypoint.alt))),
+ self._waypoint_distance(waypoint.waypoint),
+ self._ground_speed(waypoint.waypoint),
+ self._format_time(waypoint.waypoint.tot),
+ self._format_time(waypoint.waypoint.departure_time),
])
+ def _format_time(self, time: Optional[datetime.timedelta]) -> str:
+ if time is None:
+ return ""
+ local_time = self.start_time + time
+ return local_time.strftime(f"%H:%M:%S")
+
+ def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
+ if self.last_waypoint is None:
+ return "-"
+
+ distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
+ waypoint.position
+ ))
+ return f"{distance} NM"
+
+ def _ground_speed(self, waypoint: FlightWaypoint) -> str:
+ if self.last_waypoint is None:
+ return "-"
+
+ if waypoint.tot is None:
+ return "-"
+
+ if self.last_waypoint.departure_time is not None:
+ last_time = self.last_waypoint.departure_time
+ elif self.last_waypoint.tot is not None:
+ last_time = self.last_waypoint.tot
+ else:
+ return "-"
+
+ distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
+ waypoint.position
+ ))
+ duration = (waypoint.tot - last_time).total_seconds() / 3600
+ return f"{int(distance / duration)} kt"
+
def build(self) -> List[List[str]]:
return self.rows
@@ -149,12 +201,13 @@ class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo],
- jtacs: List[JtacInfo]) -> None:
+ jtacs: List[JtacInfo], start_time: datetime.datetime) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
+ self.start_time = start_time
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
@@ -170,11 +223,12 @@ class BriefingPage(KneeboardPage):
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
writer.heading("Flight Plan")
- flight_plan_builder = FlightPlanBuilder()
+ flight_plan_builder = FlightPlanBuilder(self.start_time)
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
- writer.table(flight_plan_builder.build(),
- headers=["STPT", "Action", "Alt"])
+ writer.table(flight_plan_builder.build(), headers=[
+ "#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
+ ])
writer.heading("Comm Ladder")
comms = []
@@ -194,7 +248,7 @@ class BriefingPage(KneeboardPage):
tankers.append([
tanker.callsign,
tanker.variant,
- tanker.tacan,
+ str(tanker.tacan),
self.format_frequency(tanker.freq),
])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
@@ -225,12 +279,22 @@ class BriefingPage(KneeboardPage):
atc = ""
if runway.atc is not None:
atc = self.format_frequency(runway.atc)
+ if runway.tacan is None:
+ tacan = ""
+ else:
+ tacan = str(runway.tacan)
+ if runway.ils is not None:
+ ils = str(runway.ils)
+ elif runway.icls is not None:
+ ils = str(runway.icls)
+ else:
+ ils = ""
return [
row_title,
runway.airfield_name,
atc,
- runway.tacan or "",
- runway.ils or runway.icls or "",
+ tacan,
+ ils,
runway.runway_name,
]
@@ -247,8 +311,8 @@ class BriefingPage(KneeboardPage):
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
- def __init__(self, mission: Mission) -> None:
- super().__init__(mission)
+ def __init__(self, mission: Mission, game: "Game") -> None:
+ super().__init__(mission, game)
def generate(self) -> None:
"""Generates a kneeboard per client flight."""
@@ -285,6 +349,11 @@ class KneeboardGenerator(MissionInfoGenerator):
"""Returns a list of kneeboard pages for the given flight."""
return [
BriefingPage(
- flight, self.comms, self.awacs, self.tankers, self.jtacs
+ flight,
+ self.comms,
+ self.awacs,
+ self.tankers,
+ self.jtacs,
+ self.mission.start_time
),
]
diff --git a/gen/locations/preset_control_point_locations.py b/gen/locations/preset_control_point_locations.py
new file mode 100644
index 00000000..90ae8204
--- /dev/null
+++ b/gen/locations/preset_control_point_locations.py
@@ -0,0 +1,22 @@
+from dataclasses import dataclass, field
+
+from typing import List
+
+from gen.locations.preset_locations import PresetLocation
+
+
+@dataclass
+class PresetControlPointLocations:
+ """A repository of preset locations for a given control point"""
+
+ # List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
+ ashore_locations: List[PresetLocation] = field(default_factory=list)
+
+ # List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
+ offshore_locations: List[PresetLocation] = field(default_factory=list)
+
+ # Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
+ antiship_locations: List[PresetLocation] = field(default_factory=list)
+
+ # List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
+ powerplant_locations: List[PresetLocation] = field(default_factory=list)
diff --git a/gen/locations/preset_location_finder.py b/gen/locations/preset_location_finder.py
new file mode 100644
index 00000000..41386d90
--- /dev/null
+++ b/gen/locations/preset_location_finder.py
@@ -0,0 +1,59 @@
+from pathlib import Path
+from typing import List
+
+from dcs import Mission, ships
+from dcs.vehicles import MissilesSS
+
+from gen.locations.preset_control_point_locations import PresetControlPointLocations
+from gen.locations.preset_locations import PresetLocation
+
+
+class PresetLocationFinder:
+
+ @staticmethod
+ def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
+ """
+ Extract the list of preset locations from miz data
+ :param terrain_name: Terrain/Map name
+ :param cp_name: Control Point / Airbase name
+ :return:
+ """
+
+ miz_file = Path("./resources/mizdata/", terrain_name.lower(), cp_name + ".miz")
+
+ offshore_locations: List[PresetLocation] = []
+ ashore_locations: List[PresetLocation] = []
+ powerplants_locations: List[PresetLocation] = []
+ antiship_locations: List[PresetLocation] = []
+
+ if miz_file.exists():
+ m = Mission()
+ m.load_file(miz_file.absolute())
+
+ for vehicle_group in m.country("USA").vehicle_group:
+ if len(vehicle_group.units) > 0:
+ ashore_locations.append(PresetLocation(vehicle_group.position,
+ vehicle_group.units[0].heading,
+ vehicle_group.name))
+
+ for ship_group in m.country("USA").ship_group:
+ if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
+ offshore_locations.append(PresetLocation(ship_group.position,
+ ship_group.units[0].heading,
+ ship_group.name))
+
+ for static_group in m.country("USA").static_group:
+ if len(static_group.units) > 0:
+ powerplants_locations.append(PresetLocation(static_group.position,
+ static_group.units[0].heading,
+ static_group.name))
+
+ if m.country("Iran") is not None:
+ for vehicle_group in m.country("Iran").vehicle_group:
+ if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
+ antiship_locations.append(PresetLocation(vehicle_group.position,
+ vehicle_group.units[0].heading,
+ vehicle_group.name))
+
+ return PresetControlPointLocations(ashore_locations, offshore_locations,
+ antiship_locations, powerplants_locations)
diff --git a/gen/locations/preset_locations.py b/gen/locations/preset_locations.py
new file mode 100644
index 00000000..2d8872c3
--- /dev/null
+++ b/gen/locations/preset_locations.py
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+
+from dcs import Point
+
+
+@dataclass
+class PresetLocation:
+ """A preset location"""
+ position: Point
+ heading: int
+ id: str
+
+ def __str__(self):
+ return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
+ self.id) + "-" * 10
diff --git a/gen/missiles/missiles_group_generator.py b/gen/missiles/missiles_group_generator.py
index c63fcca9..3b84037d 100644
--- a/gen/missiles/missiles_group_generator.py
+++ b/gen/missiles/missiles_group_generator.py
@@ -1,21 +1,23 @@
import logging
import random
from game import db
+from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = {
"V1GroupGenerator": V1GroupGenerator,
+ "ScudGenerator": ScudGenerator
}
-def generate_missile_group(game, ground_object, faction:str):
+def generate_missile_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
- faction = db.FACTIONS[faction]
- if "missiles" in faction.keys():
- generators = faction["missiles"]
+ faction = db.FACTIONS[faction_name]
+ if len(faction.missiles) > 0:
+ generators = faction.missiles
if len(generators) > 0:
gen = random.choice(generators)
if gen in MISSILES_MAP.keys():
diff --git a/gen/missiles/scud_site.py b/gen/missiles/scud_site.py
new file mode 100644
index 00000000..6b050c84
--- /dev/null
+++ b/gen/missiles/scud_site.py
@@ -0,0 +1,30 @@
+import random
+
+from dcs.vehicles import Unarmed, MissilesSS, AirDefence
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class ScudGenerator(GroupGenerator):
+
+ def __init__(self, game, ground_object, faction):
+ super(ScudGenerator, self).__init__(game, ground_object)
+ self.faction = faction
+
+ def generate(self):
+
+ # Scuds
+ self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
+ self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
+ self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
+
+ # Commander
+ self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
+ self.heading)
+
+ # Shorad
+ self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
+ self.heading)
+
+ self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
+ self.position.x + 200, self.position.y + 15, 90)
\ No newline at end of file
diff --git a/gen/radios.py b/gen/radios.py
index c0adb20c..c2180fe3 100644
--- a/gen/radios.py
+++ b/gen/radios.py
@@ -126,6 +126,25 @@ RADIOS: List[Radio] = [
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
+
+ # MiG-15bis
+ Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
+
+ # MiG-19P
+ Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
+
+ # MiG-21bis
+ Radio("RSIU-5V", MHz(100), MHz(150), step=MHz(1)),
+
+ # Ka-50
+ # Note: Also capable of 100MHz-150MHz, but we can't model gaps.
+ Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
+ Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
+
+ # UH-1H
+ Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
+ Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
+ Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
]
diff --git a/gen/runways.py b/gen/runways.py
new file mode 100644
index 00000000..5323c37b
--- /dev/null
+++ b/gen/runways.py
@@ -0,0 +1,139 @@
+"""Runway information and selection."""
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from typing import Iterator, Optional
+
+from dcs.terrain.terrain import Airport
+
+from game.weather import Conditions
+from theater import ControlPoint, ControlPointType
+from .airfields import AIRFIELD_DATA
+from .radios import RadioFrequency
+from .tacan import TacanChannel
+
+
+@dataclass(frozen=True)
+class RunwayData:
+ airfield_name: str
+ runway_heading: int
+ runway_name: str
+ atc: Optional[RadioFrequency] = None
+ tacan: Optional[TacanChannel] = None
+ tacan_callsign: Optional[str] = None
+ ils: Optional[RadioFrequency] = None
+ icls: Optional[int] = None
+
+ @classmethod
+ def for_airfield(cls, airport: Airport, runway_heading: int,
+ runway_name: str) -> RunwayData:
+ """Creates RunwayData for the given runway of an airfield.
+
+ Args:
+ airport: The airfield the runway belongs to.
+ runway_heading: Heading of the runway.
+ runway_name: Identifier of the runway to use. e.g. "03" or "20L".
+ """
+ atc: Optional[RadioFrequency] = None
+ tacan: Optional[TacanChannel] = None
+ tacan_callsign: Optional[str] = None
+ ils: Optional[RadioFrequency] = None
+ try:
+ airfield = AIRFIELD_DATA[airport.name]
+ if airfield.atc is not None:
+ atc = airfield.atc.uhf
+ else:
+ atc = None
+ tacan = airfield.tacan
+ tacan_callsign = airfield.tacan_callsign
+ ils = airfield.ils_freq(runway_name)
+ except KeyError:
+ logging.warning(f"No airfield data for {airport.name}")
+ return cls(
+ airfield_name=airport.name,
+ runway_heading=runway_heading,
+ runway_name=runway_name,
+ atc=atc,
+ tacan=tacan,
+ tacan_callsign=tacan_callsign,
+ ils=ils
+ )
+
+ @classmethod
+ def for_pydcs_airport(cls, airport: Airport) -> Iterator[RunwayData]:
+ for runway in airport.runways:
+ runway_number = runway.heading // 10
+ runway_side = ["", "L", "R"][runway.leftright]
+ runway_name = f"{runway_number:02}{runway_side}"
+ yield cls.for_airfield(airport, runway.heading, runway_name)
+
+ # pydcs only exposes one runway per physical runway, so to expose
+ # both sides of the runway we need to generate the other.
+ heading = (runway.heading + 180) % 360
+ runway_number = heading // 10
+ runway_side = ["", "R", "L"][runway.leftright]
+ runway_name = f"{runway_number:02}{runway_side}"
+ yield cls.for_airfield(airport, heading, runway_name)
+
+
+class RunwayAssigner:
+ def __init__(self, conditions: Conditions):
+ self.conditions = conditions
+
+ def angle_off_headwind(self, runway: RunwayData) -> int:
+ wind = self.conditions.weather.wind.at_0m.direction
+ ideal_heading = (wind + 180) % 360
+ return abs(runway.runway_heading - ideal_heading)
+
+ def get_preferred_runway(self, airport: Airport) -> RunwayData:
+ """Returns the preferred runway for the given airport.
+
+ Right now we're only selecting runways based on whether or not
+ they have
+ ILS, but we could also choose based on wind conditions, or which
+ direction flight plans should follow.
+ """
+ runways = list(RunwayData.for_pydcs_airport(airport))
+
+ # Find the runway with the best headwind first.
+ best_runways = [runways[0]]
+ best_angle_off_headwind = self.angle_off_headwind(best_runways[0])
+ for runway in runways[1:]:
+ angle_off_headwind = self.angle_off_headwind(runway)
+ if angle_off_headwind == best_angle_off_headwind:
+ best_runways.append(runway)
+ elif angle_off_headwind < best_angle_off_headwind:
+ best_runways = [runway]
+ best_angle_off_headwind = angle_off_headwind
+
+ for runway in best_runways:
+ # But if there are multiple runways with the same heading,
+ # prefer
+ # and ILS capable runway.
+ if runway.ils is not None:
+ return runway
+
+ # Otherwise the only difference between the two is the distance from
+ # parking, which we don't know, so just pick the first one.
+ return best_runways[0]
+
+ def takeoff_heading(self, departure: ControlPoint) -> int:
+ if departure.cptype == ControlPointType.AIRBASE:
+ return self.get_preferred_runway(departure.airport).runway_heading
+ elif departure.is_fleet:
+ # The carrier will be angled into the wind automatically.
+ return (self.conditions.weather.wind.at_0m.direction + 180) % 360
+ logging.warning(
+ f"Unhandled departure control point: {departure.cptype}")
+ return 0
+
+ def landing_heading(self, arrival: ControlPoint) -> int:
+ if arrival.cptype == ControlPointType.AIRBASE:
+ return self.get_preferred_runway(arrival.airport).runway_heading
+ elif arrival.is_fleet:
+ # The carrier will be angled into the wind automatically.
+ return (self.conditions.weather.wind.at_0m.direction + 180) % 360
+ logging.warning(
+ f"Unhandled departure control point: {arrival.cptype}")
+ return 0
diff --git a/gen/sam/aaa_flak18.py b/gen/sam/aaa_flak18.py
new file mode 100644
index 00000000..fea85f70
--- /dev/null
+++ b/gen/sam/aaa_flak18.py
@@ -0,0 +1,29 @@
+import random
+
+from dcs.vehicles import AirDefence, Unarmed
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class Flak18Generator(GroupGenerator):
+ """
+ This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
+ """
+
+ name = "WW2 Flak Site"
+ price = 40
+
+ def generate(self):
+
+ spacing = random.randint(30, 60)
+ index = 0
+
+ for i in range(3):
+ for j in range(2):
+ index = index + 1
+ self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
+ self.position.x + spacing * i + random.randint(1, 5),
+ self.position.y + spacing * j + random.randint(1, 5), self.heading)
+
+ # Add a commander truck
+ self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)
diff --git a/gen/sam/aaa_ww2_ally_flak.py b/gen/sam/aaa_ww2_ally_flak.py
new file mode 100644
index 00000000..7c449dba
--- /dev/null
+++ b/gen/sam/aaa_ww2_ally_flak.py
@@ -0,0 +1,34 @@
+import random
+
+from dcs.vehicles import AirDefence, Unarmed, Armor
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class AllyWW2FlakGenerator(GroupGenerator):
+ """
+ This generate an ally flak artillery group
+ """
+
+ name = "WW2 Ally Flak Site"
+ price = 140
+
+ def generate(self):
+
+ positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
+ for i, position in enumerate(positions):
+ self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
+
+ positions = self.get_circular_position(8, launcher_distance=100, coverage=360)
+ for i, position in enumerate(positions):
+ self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
+
+ positions = self.get_circular_position(8, launcher_distance=150, coverage=360)
+ for i, position in enumerate(positions):
+ self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
+
+ # Add a commander truck
+ self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360))
+ self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360))
+ self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
+ self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
diff --git a/gen/sam/cold_war_flak.py b/gen/sam/cold_war_flak.py
new file mode 100644
index 00000000..c0b7e81d
--- /dev/null
+++ b/gen/sam/cold_war_flak.py
@@ -0,0 +1,72 @@
+import random
+
+from dcs.vehicles import AirDefence, Unarmed
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class EarlyColdWarFlakGenerator(GroupGenerator):
+ """
+ This generator attempt to mimic an early cold-war era flak AAA site.
+ The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection.
+
+ This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
+ """
+
+ name = "Early Cold War Flak Site"
+ price = 58
+
+ def generate(self):
+
+ spacing = random.randint(30, 60)
+ index = 0
+
+ # Long range guns
+ for i in range(3):
+ for j in range(2):
+ index = index + 1
+ self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
+ self.position.x + spacing * i + random.randint(1, 5),
+ self.position.y + spacing * j + random.randint(1, 5), self.heading)
+
+ # Short range guns
+ self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
+ self.position.x - 40, self.position.y - 40, self.heading + 180),
+ self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
+ self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
+
+ # Add a truck
+ self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
+
+
+class ColdWarFlakGenerator(GroupGenerator):
+ """
+ This generator attempt to mimic a cold-war era flak AAA site.
+ The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection.
+ The site is also fitted with a P-19 radar for early detection.
+ """
+
+ name = "Cold War Flak Site"
+ price = 72
+
+ def generate(self):
+
+ spacing = random.randint(30, 60)
+ index = 0
+
+ # Long range guns
+ for i in range(3):
+ for j in range(2):
+ index = index + 1
+ self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
+ self.position.x + spacing * i + random.randint(1, 5),
+ self.position.y + spacing * j + random.randint(1, 5), self.heading)
+
+ # Short range guns
+ self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
+ self.position.x - 40, self.position.y - 40, self.heading + 180),
+ self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
+ self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
+
+ # Add a P19 Radar for EWR
+ self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
diff --git a/gen/sam/ewrs.py b/gen/sam/ewrs.py
new file mode 100644
index 00000000..2c60af22
--- /dev/null
+++ b/gen/sam/ewrs.py
@@ -0,0 +1,98 @@
+from dcs.vehicles import AirDefence
+from dcs.unittype import VehicleType
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class EwrGenerator(GroupGenerator):
+ @property
+ def unit_type(self) -> VehicleType:
+ raise NotImplementedError
+
+ def generate(self) -> None:
+ self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y,
+ self.heading)
+
+
+class BoxSpringGenerator(EwrGenerator):
+ """1L13 "Box Spring" EWR."""
+
+ unit_type = AirDefence.EWR_1L13
+
+
+class TallRackGenerator(EwrGenerator):
+ """55G6 "Tall Rack" EWR."""
+
+ unit_type = AirDefence.EWR_55G6
+
+
+class DogEarGenerator(EwrGenerator):
+ """9S80M1 "Dog Ear" EWR.
+
+ This is the SA-8 search radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.CP_9S80M1_Sborka
+
+
+class RolandEwrGenerator(EwrGenerator):
+ """Roland EWR.
+
+ This is the Roland search radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_Roland_EWR
+
+
+class FlatFaceGenerator(EwrGenerator):
+ """P-19 "Flat Face" EWR.
+
+ This is the SA-3 search radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_SR_P_19
+
+
+class PatriotEwrGenerator(EwrGenerator):
+ """Patriot EWR.
+
+ This is the Patriot search/track radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53
+
+
+class BigBirdGenerator(EwrGenerator):
+ """64H6E "Big Bird" EWR.
+
+ This is the SA-10 track radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
+
+
+class SnowDriftGenerator(EwrGenerator):
+ """9S18M1 "Snow Drift" EWR.
+
+ This is the SA-11 search radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1
+
+
+class StraightFlushGenerator(EwrGenerator):
+ """1S91 "Straight Flush" EWR.
+
+ This is the SA-6 search/track radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91
+
+
+class HawkEwrGenerator(EwrGenerator):
+ """Hawk EWR.
+
+ This is the Hawk search radar, but used as an early warning radar.
+ """
+
+ unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50
diff --git a/gen/sam/freya_ewr.py b/gen/sam/freya_ewr.py
new file mode 100644
index 00000000..70571e56
--- /dev/null
+++ b/gen/sam/freya_ewr.py
@@ -0,0 +1,39 @@
+import random
+
+from dcs.vehicles import AirDefence, Unarmed, Infantry
+
+from gen.sam.group_generator import GroupGenerator
+
+
+class FreyaGenerator(GroupGenerator):
+ """
+ This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
+ """
+
+ name = "Freya EWR Site"
+ price = 60
+
+ def generate(self):
+
+ # TODO : would be better with the Concrete structure that is supposed to protect it
+ self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
+
+ positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
+ for i, position in enumerate(positions):
+ self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
+
+ positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
+ for i, position in enumerate(positions):
+ self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
+
+ # Command/Logi
+ self.add_unit(Unarmed.KĂĽbelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
+ self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
+ self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
+
+ # Maschinensatz_33 and Kdo.g 40 Telemeter
+ self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
+ self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
+ self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
+ self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
+ self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
\ No newline at end of file
diff --git a/gen/sam/genericsam_group_generator.py b/gen/sam/genericsam_group_generator.py
new file mode 100644
index 00000000..8a35e51b
--- /dev/null
+++ b/gen/sam/genericsam_group_generator.py
@@ -0,0 +1,15 @@
+from abc import ABC
+
+from game import Game
+from gen.sam.group_generator import GroupGenerator
+from theater.theatergroundobject import SamGroundObject
+
+
+class GenericSamGroupGenerator(GroupGenerator, ABC):
+ """
+ This is the base for all SAM group generators
+ """
+
+ def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
+ ground_object.skynet_capable = True
+ super().__init__(game, ground_object)
diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py
index e6620211..94738eef 100644
--- a/gen/sam/group_generator.py
+++ b/gen/sam/group_generator.py
@@ -1,35 +1,47 @@
+from __future__ import annotations
import math
import random
+from typing import TYPE_CHECKING, Optional
from dcs import unitgroup
from dcs.point import PointAction
-from dcs.unit import Vehicle
+from dcs.unit import Vehicle, Ship
+from dcs.unittype import VehicleType
+
+from game.factions.faction import Faction
+from theater.theatergroundobject import TheaterGroundObject
+
+if TYPE_CHECKING:
+ from game.game import Game
-class GroupGenerator():
+# TODO: Generate a group description rather than a pydcs group.
+# It appears that all of this work gets redone at miz generation time (see
+# groundobjectsgen for an example). We can do less work and include the data we
+# care about in the format we want if we just generate our own group description
+# types rather than pydcs groups.
+class GroupGenerator:
- def __init__(self, game, ground_object):
+ def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
- self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_identifier)
-
+ self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
+ self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
def generate(self):
raise NotImplementedError
- def get_generated_group(self):
+ def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg
- def add_unit(self, unit_type, name, pos_x, pos_y, heading):
-
- nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name
-
+ def add_unit(self, unit_type: VehicleType, name: str, pos_x: float,
+ pos_y: float, heading: int) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(),
- nn, unit_type.id)
+ f"{self.go.group_name}|{name}", unit_type.id)
unit.position.x = pos_x
unit.position.y = pos_y
unit.heading = heading
@@ -71,3 +83,25 @@ class GroupGenerator():
current_offset += outer_offset
return positions
+
+class ShipGroupGenerator(GroupGenerator):
+ """Abstract class for other ship generator classes"""
+ def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
+ self.game = game
+ self.go = ground_object
+ self.position = ground_object.position
+ self.heading = random.randint(0, 359)
+ self.faction = faction
+ self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
+ self.go.group_name)
+ wp = self.vg.add_waypoint(self.position, 0)
+ wp.ETA_locked = True
+
+ def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
+ unit = Ship(self.game.next_unit_id(),
+ f"{self.go.group_name}|{name}", unit_type)
+ unit.position.x = pos_x
+ unit.position.y = pos_y
+ unit.heading = heading
+ self.vg.add_unit(unit)
+ return unit
diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py
index add0c9fa..1ff77cde 100644
--- a/gen/sam/sam_group_generator.py
+++ b/gen/sam/sam_group_generator.py
@@ -1,13 +1,31 @@
import random
-from typing import List
+from typing import List, Optional, Type
-from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
+from dcs.unitgroup import VehicleGroup
-from game import db
+from game import Game, db
from gen.sam.aaa_bofors import BoforsGenerator
from gen.sam.aaa_flak import FlakGenerator
+from gen.sam.aaa_flak18 import Flak18Generator
+from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
+from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator
+
+
+from gen.sam.ewrs import (
+ BigBirdGenerator,
+ BoxSpringGenerator,
+ DogEarGenerator,
+ FlatFaceGenerator,
+ HawkEwrGenerator,
+ PatriotEwrGenerator,
+ RolandEwrGenerator,
+ SnowDriftGenerator,
+ StraightFlushGenerator,
+ TallRackGenerator,
+)
+from gen.sam.group_generator import GroupGenerator
from gen.sam.sam_avenger import AvengerGenerator
from gen.sam.sam_chaparral import ChaparralGenerator
from gen.sam.sam_gepard import GepardGenerator
@@ -32,39 +50,43 @@ from gen.sam.sam_zsu23 import ZSU23Generator
from gen.sam.sam_zu23 import ZU23Generator
from gen.sam.sam_zu23_ural import ZU23UralGenerator
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
+from gen.sam.freya_ewr import FreyaGenerator
+from theater import TheaterGroundObject
+from theater.theatergroundobject import SamGroundObject
SAM_MAP = {
- AirDefence.SAM_Hawk_PCP: HawkGenerator,
- AirDefence.AAA_ZU_23_Emplacement: ZU23Generator,
- AirDefence.AAA_ZU_23_Closed: ZU23Generator,
- AirDefence.AAA_ZU_23_on_Ural_375: ZU23UralGenerator,
- AirDefence.AAA_ZU_23_Insurgent_on_Ural_375: ZU23UralInsurgentGenerator,
- AirDefence.AAA_ZU_23_Insurgent_Closed: ZU23InsurgentGenerator,
- AirDefence.AAA_ZU_23_Insurgent: ZU23InsurgentGenerator,
- AirDefence.SPAAA_ZSU_23_4_Shilka: ZSU23Generator,
- AirDefence.AAA_Vulcan_M163: VulcanGenerator,
- AirDefence.SAM_Linebacker_M6: LinebackerGenerator,
- AirDefence.Rapier_FSA_Launcher: RapierGenerator,
- AirDefence.SAM_Avenger_M1097: AvengerGenerator,
- AirDefence.SPAAA_Gepard: GepardGenerator,
- AirDefence.SAM_Roland_ADS: RolandGenerator,
- AirDefence.SAM_Patriot_LN_M901: PatriotGenerator,
- AirDefence.SAM_Patriot_EPP_III: PatriotGenerator,
- AirDefence.SAM_Chaparral_M48: ChaparralGenerator,
- AirDefence.AAA_Bofors_40mm: BoforsGenerator,
- AirDefence.AAA_8_8cm_Flak_36: FlakGenerator,
- AirDefence.SAM_SA_2_LN_SM_90: SA2Generator,
- AirDefence.SAM_SA_3_S_125_LN_5P73: SA3Generator,
- AirDefence.SAM_SA_6_Kub_LN_2P25: SA6Generator,
- AirDefence.SAM_SA_8_Osa_9A33: SA8Generator,
- AirDefence.SAM_SA_9_Strela_1_9P31: SA9Generator,
- AirDefence.SAM_SA_10_S_300PS_LN_5P85C: SA10Generator,
- AirDefence.SAM_SA_10_S_300PS_CP_54K6: SA10Generator,
- AirDefence.SAM_SA_11_Buk_LN_9A310M1: SA11Generator,
- AirDefence.SAM_SA_13_Strela_10M3_9A35M3: SA13Generator,
- AirDefence.SAM_SA_15_Tor_9A331: SA15Generator,
- AirDefence.SAM_SA_19_Tunguska_2S6: SA19Generator,
- AirDefence.HQ_7_Self_Propelled_LN: HQ7Generator
+ "HawkGenerator": HawkGenerator,
+ "ZU23Generator": ZU23Generator,
+ "ZU23UralGenerator": ZU23UralGenerator,
+ "ZU23UralInsurgentGenerator": ZU23UralInsurgentGenerator,
+ "ZU23InsurgentGenerator": ZU23InsurgentGenerator,
+ "ZSU23Generator": ZSU23Generator,
+ "VulcanGenerator": VulcanGenerator,
+ "LinebackerGenerator": LinebackerGenerator,
+ "RapierGenerator": RapierGenerator,
+ "AvengerGenerator": AvengerGenerator,
+ "GepardGenerator": GepardGenerator,
+ "RolandGenerator": RolandGenerator,
+ "PatriotGenerator": PatriotGenerator,
+ "ChaparralGenerator": ChaparralGenerator,
+ "BoforsGenerator": BoforsGenerator,
+ "FlakGenerator": FlakGenerator,
+ "SA2Generator": SA2Generator,
+ "SA3Generator": SA3Generator,
+ "SA6Generator": SA6Generator,
+ "SA8Generator": SA8Generator,
+ "SA9Generator": SA9Generator,
+ "SA10Generator": SA10Generator,
+ "SA11Generator": SA11Generator,
+ "SA13Generator": SA13Generator,
+ "SA15Generator": SA15Generator,
+ "SA19Generator": SA19Generator,
+ "HQ7Generator": HQ7Generator,
+ "Flak18Generator": Flak18Generator,
+ "ColdWarFlakGenerator": ColdWarFlakGenerator,
+ "EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
+ "FreyaGenerator": FreyaGenerator,
+ "AllyWW2FlakGenerator": AllyWW2FlakGenerator
}
SAM_PRICES = {
@@ -101,53 +123,80 @@ SAM_PRICES = {
AirDefence.HQ_7_Self_Propelled_LN: 35
}
-
-def get_faction_possible_sams_units(faction: str) -> List[UnitType]:
- """
- Return the list
- :param faction: Faction to search units for
- """
- return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()]
+EWR_MAP = {
+ "BoxSpringGenerator": BoxSpringGenerator,
+ "TallRackGenerator": TallRackGenerator,
+ "DogEarGenerator": DogEarGenerator,
+ "RolandEwrGenerator": RolandEwrGenerator,
+ "FlatFaceGenerator": FlatFaceGenerator,
+ "PatriotEwrGenerator": PatriotEwrGenerator,
+ "BigBirdGenerator": BigBirdGenerator,
+ "SnowDriftGenerator": SnowDriftGenerator,
+ "StraightFlushGenerator": StraightFlushGenerator,
+ "HawkEwrGenerator": HawkEwrGenerator,
+}
-def get_faction_possible_sams_generator(faction: str) -> List[UnitType]:
+def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
- :param faction: Faction to search units for
+ :param faction: Faction name to search units for
"""
- return [SAM_MAP[u] for u in get_faction_possible_sams_units(faction)]
+ return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP]
-def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
+def get_faction_possible_ewrs_generator(faction: str) -> List[Type[GroupGenerator]]:
+ """
+ Return the list of possible SAM generator for the given faction
+ :param faction: Faction name to search units for
+ """
+ return [EWR_MAP[s] for s in db.FACTIONS[faction].ewrs if s in EWR_MAP]
+
+
+def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
+ faction: str) -> Optional[VehicleGroup]:
"""
This generate a SAM group
- :param parentCp: The parent control point
- :param ground_object: The ground object which will own the sam group
- :param country: Owner country
- :return: Nothing, but put the group reference inside the ground object
+ :param game: The Game.
+ :param ground_object: The ground object which will own the sam group.
+ :param faction: Owner faction.
+ :return: The generated group, or None if one could not be generated.
"""
- possible_sams = get_faction_possible_sams_units(faction)
- if len(possible_sams) > 0:
- sam = random.choice(possible_sams)
- generator = SAM_MAP[sam](game, ground_object)
+ possible_sams_generators = get_faction_possible_sams_generator(faction)
+ if len(possible_sams_generators) > 0:
+ sam_generator_class = random.choice(possible_sams_generators)
+ generator = sam_generator_class(game, ground_object)
generator.generate()
return generator.get_generated_group()
return None
-def generate_shorad_group(game, parent_cp, ground_object, faction:str):
- if("shorad") in db.FACTIONS[faction].keys():
- shorad = db.FACTIONS[faction]["shorad"]
- sam = random.choice(shorad)
+def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
+ faction: str) -> Optional[VehicleGroup]:
+ """Generates an early warning radar group.
+
+ :param game: The Game.
+ :param ground_object: The ground object which will own the EWR group.
+ :param faction: Owner faction.
+ :return: The generated group, or None if one could not be generated.
+ """
+ generators = get_faction_possible_ewrs_generator(faction)
+ if len(generators) > 0:
+ generator_class = random.choice(generators)
+ generator = generator_class(game, ground_object)
+ generator.generate()
+ return generator.get_generated_group()
+ return None
+
+
+def generate_shorad_group(game: Game, ground_object: SamGroundObject,
+ faction_name: str) -> Optional[VehicleGroup]:
+ faction = db.FACTIONS[faction_name]
+
+ if len(faction.shorads) > 0:
+ sam = random.choice(faction.shorads)
generator = SAM_MAP[sam](game, ground_object)
generator.generate()
return generator.get_generated_group()
else:
- return generate_anti_air_group(game, parent_cp, ground_object, faction)
-
-
-
-
-
-
-
+ return generate_anti_air_group(game, ground_object, faction_name)
diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py
index 89c11bc0..da8c700a 100644
--- a/gen/sam/sam_hawk.py
+++ b/gen/sam/sam_hawk.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class HawkGenerator(GroupGenerator):
+class HawkGenerator(GenericSamGroupGenerator):
"""
This generate an HAWK group
"""
@@ -14,8 +14,8 @@ class HawkGenerator(GroupGenerator):
price = 115
def generate(self):
- self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading)
+ self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading)
# Triple A for close range defense
diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py
index f8a531ea..adba14b5 100644
--- a/gen/sam/sam_hq7.py
+++ b/gen/sam/sam_hq7.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class HQ7Generator(GroupGenerator):
+class HQ7Generator(GenericSamGroupGenerator):
"""
This generate an HQ7 group
"""
diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py
index b55dbaea..490e6f2f 100644
--- a/gen/sam/sam_patriot.py
+++ b/gen/sam/sam_patriot.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class PatriotGenerator(GroupGenerator):
+class PatriotGenerator(GenericSamGroupGenerator):
"""
This generate a Patriot group
"""
@@ -15,11 +15,11 @@ class PatriotGenerator(GroupGenerator):
def generate(self):
# Command Post
+ self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading)
self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_ECS_AN_MSQ_104, "MSQ", self.position.x + 30, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_ICC, "ICC", self.position.x + 60, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading)
- self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
@@ -28,6 +28,6 @@ class PatriotGenerator(GroupGenerator):
# Short range protection for high value site
num_launchers = random.randint(3, 4)
- positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
+ positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
for i, position in enumerate(positions):
- self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
+ self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
\ No newline at end of file
diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py
index 99b7b205..981a098e 100644
--- a/gen/sam/sam_rapier.py
+++ b/gen/sam/sam_rapier.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class RapierGenerator(GroupGenerator):
+class RapierGenerator(GenericSamGroupGenerator):
"""
This generate a Rapier Group
"""
diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py
index 9e31d5fe..1f970517 100644
--- a/gen/sam/sam_roland.py
+++ b/gen/sam/sam_roland.py
@@ -1,9 +1,9 @@
from dcs.vehicles import AirDefence, Unarmed
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class RolandGenerator(GroupGenerator):
+class RolandGenerator(GenericSamGroupGenerator):
"""
This generate a Roland group
"""
@@ -12,7 +12,7 @@ class RolandGenerator(GroupGenerator):
price = 40
def generate(self):
- self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading)
+ self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py
index ae2102f0..d3804a86 100644
--- a/gen/sam/sam_sa10.py
+++ b/gen/sam/sam_sa10.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class SA10Generator(GroupGenerator):
+class SA10Generator(GenericSamGroupGenerator):
"""
This generate a SA-10 group
"""
@@ -14,15 +14,15 @@ class SA10Generator(GroupGenerator):
price = 450
def generate(self):
- # Command Post
- self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)
-
# Search Radar
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_5N66M, "SR1", self.position.x, self.position.y + 40, self.heading)
# Search radar for missiles (optionnal)
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_64H6E, "SR2", self.position.x - 40, self.position.y, self.heading)
+ # Command Post
+ self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)
+
# 2 Tracking radars
self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
@@ -31,7 +31,7 @@ class SA10Generator(GroupGenerator):
# 2 different launcher type (C & D)
num_launchers = random.randint(6, 8)
- positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
+ positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
for i, position in enumerate(positions):
if i%2 == 0:
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2])
@@ -41,12 +41,12 @@ class SA10Generator(GroupGenerator):
# Then let's add short range protection to this high value site
# Sa-13 Strela are great for that
num_launchers = random.randint(2, 4)
- positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
+ positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "IR#" + str(i), position[0], position[1], position[2])
# And even some AA
num_launchers = random.randint(6, 8)
- positions = self.get_circular_position(num_launchers, launcher_distance=350, coverage=360)
+ positions = self.get_circular_position(num_launchers, launcher_distance=210, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), position[0], position[1], position[2])
diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py
index 3af6c242..e7634b92 100644
--- a/gen/sam/sam_sa11.py
+++ b/gen/sam/sam_sa11.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class SA11Generator(GroupGenerator):
+class SA11Generator(GenericSamGroupGenerator):
"""
This generate a SA-11 group
"""
@@ -14,8 +14,8 @@ class SA11Generator(GroupGenerator):
price = 180
def generate(self):
- self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading)
+ self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=180)
diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py
index c108c1e8..ff77265f 100644
--- a/gen/sam/sam_sa2.py
+++ b/gen/sam/sam_sa2.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class SA2Generator(GroupGenerator):
+class SA2Generator(GenericSamGroupGenerator):
"""
This generate a SA-2 group
"""
diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py
index 455bab19..e57f184c 100644
--- a/gen/sam/sam_sa3.py
+++ b/gen/sam/sam_sa3.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class SA3Generator(GroupGenerator):
+class SA3Generator(GenericSamGroupGenerator):
"""
This generate a SA-3 group
"""
diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py
index 7ec2afca..1028ed76 100644
--- a/gen/sam/sam_sa6.py
+++ b/gen/sam/sam_sa6.py
@@ -2,10 +2,10 @@ import random
from dcs.vehicles import AirDefence
-from gen.sam.group_generator import GroupGenerator
+from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
-class SA6Generator(GroupGenerator):
+class SA6Generator(GenericSamGroupGenerator):
"""
This generate a SA-6 group
"""
diff --git a/gen/triggergen.py b/gen/triggergen.py
index fbd8062e..ba87bb3e 100644
--- a/gen/triggergen.py
+++ b/gen/triggergen.py
@@ -1,19 +1,12 @@
-import typing
-import random
-from datetime import datetime, timedelta, time
-
+from dcs.action import MarkToAll
+from dcs.condition import TimeAfter
from dcs.mission import Mission
-from dcs.triggers import *
-from dcs.condition import *
-from dcs.action import *
+from dcs.task import Option
+from dcs.translation import String
+from dcs.triggers import Event, TriggerOnce
from dcs.unit import Skill
-from dcs.point import MovingPoint, PointProperties
-from dcs.action import *
-from game import db
-from theater import *
-from gen.airsupportgen import AirSupportConflictGenerator
-from gen import *
+from .conflictgen import Conflict
PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25
diff --git a/gen/visualgen.py b/gen/visualgen.py
index c3be7cad..efd0c1f9 100644
--- a/gen/visualgen.py
+++ b/gen/visualgen.py
@@ -1,18 +1,20 @@
-import typing
+from __future__ import annotations
+
import random
-from datetime import datetime, timedelta
+from typing import TYPE_CHECKING
+from dcs.mapping import Point
from dcs.mission import Mission
-from dcs.statics import *
from dcs.unit import Static
+from dcs.unittype import StaticType
-from theater import *
-from .conflictgen import *
-#from game.game import Game
-from game import db
+if TYPE_CHECKING:
+ from game import Game
+
+from .conflictgen import Conflict, FRONTLINE_LENGTH
-class MarkerSmoke(unittype.StaticType):
+class MarkerSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@@ -20,7 +22,7 @@ class MarkerSmoke(unittype.StaticType):
rate = 0.1
-class Smoke(unittype.StaticType):
+class Smoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@@ -28,7 +30,7 @@ class Smoke(unittype.StaticType):
rate = 1
-class BigSmoke(unittype.StaticType):
+class BigSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@@ -36,7 +38,7 @@ class BigSmoke(unittype.StaticType):
rate = 1
-class MassiveSmoke(unittype.StaticType):
+class MassiveSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@@ -44,7 +46,7 @@ class MassiveSmoke(unittype.StaticType):
rate = 1
-class Outpost(unittype.StaticType):
+class Outpost(StaticType):
id = "outpost"
name = "outpost"
category = "Fortifications"
@@ -90,15 +92,15 @@ def turn_heading(heading, fac):
class VisualGenerator:
- game = None # type: Game
-
- def __init__(self, mission: Mission, conflict: Conflict, game):
+ def __init__(self, mission: Mission, conflict: Conflict, game: Game):
self.mission = mission
self.conflict = conflict
self.game = game
def _generate_frontline_smokes(self):
- for from_cp, to_cp in self.game.theater.conflicts():
+ for front_line in self.game.theater.conflicts():
+ from_cp = front_line.control_point_a
+ to_cp = front_line.control_point_b
if from_cp.is_global or to_cp.is_global:
continue
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..045a50e6
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,12 @@
+[mypy]
+namespace_packages = True
+
+[mypy-dcs.*]
+follow_imports=silent
+ignore_missing_imports = True
+
+[mypy-PIL.*]
+ignore_missing_imports = True
+
+[mypy-winreg.*]
+ignore_missing_imports = True
\ No newline at end of file
diff --git a/pydcs b/pydcs
index c203e5a1..fa9195fb 160000
--- a/pydcs
+++ b/pydcs
@@ -1 +1 @@
-Subproject commit c203e5a1b8d5eb42d559dab074e668bf37fa5158
+Subproject commit fa9195fbccbf96775d108a22c13c3ee2375e4c0b
diff --git a/pydcs_extensions/highdigitsams/highdigitsams.py b/pydcs_extensions/highdigitsams/highdigitsams.py
new file mode 100644
index 00000000..3ff206d0
--- /dev/null
+++ b/pydcs_extensions/highdigitsams/highdigitsams.py
@@ -0,0 +1,169 @@
+from dcs import unittype
+
+
+class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType):
+ id = "S-300PMU1 40B6M tr"
+ name = "SAM SA-20 S-300PMU1 TR 30N6E"
+ detection_range = 160000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_20_S_300PMU1_TR_30N6E_truck(unittype.VehicleType):
+ id = "S-300PMU1 30N6E tr"
+ name = "SAM SA-20 S-300PMU1 TR 30N6E(truck)"
+ detection_range = 160000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_20_S_300PMU1_SR_5N66E(unittype.VehicleType):
+ id = "S-300PMU1 40B6MD sr"
+ name = "SAM SA-20 S-300PMU1 SR 5N66E"
+ detection_range = 120000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType):
+ id = "S-300PMU1 64N6E sr"
+ name = "SAM SA-20 S-300PMU1 SR 64N6E"
+ detection_range = 300000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType):
+ id = "S-300VM 9S15M2 sr"
+ name = "SAM SA-23 S-300VM 9S15M2 SR"
+ detection_range = 320000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_23_S_300VM_9S19M2_SR(unittype.VehicleType):
+ id = "S-300VM 9S19M2 sr"
+ name = "SAM SA-23 S-300VM 9S19M2 SR"
+ detection_range = 310000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType):
+ id = "S-300VM 9S32ME tr"
+ name = "SAM SA-23 S-300VM 9S32ME TR"
+ detection_range = 230000
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
+ id = "S-300PMU1 5P85CE ln"
+ name = "SAM SA-20 S-300PMU1 LN 5P85CE"
+ detection_range = 0
+ threat_range = 150000
+ air_weapon_dist = 150000
+
+
+class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
+ id = "S-300PMU1 5P85DE ln"
+ name = "SAM SA-20 S-300PMU1 LN 5P85DE"
+ detection_range = 0
+ threat_range = 150000
+ air_weapon_dist = 150000
+
+
+class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
+ id = "S-300PS 5P85CE ln"
+ name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
+ detection_range = 0
+ threat_range = 90000
+ air_weapon_dist = 90000
+
+
+class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
+ id = "S-300PS 5P85DE ln"
+ name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
+ detection_range = 0
+ threat_range = 90000
+ air_weapon_dist = 90000
+
+
+class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType):
+ id = "S-300VM 9A83ME ln"
+ name = "SAM SA-23 S-300VM 9A83ME LN"
+ detection_range = 0
+ threat_range = 90000
+ air_weapon_dist = 90000
+
+
+class SAM_SA_23_S_300VM_9A82ME_LN(unittype.VehicleType):
+ id = "S-300VM 9A82ME ln"
+ name = "SAM SA-23 S-300VM 9A82ME LN"
+ detection_range = 0
+ threat_range = 200000
+ air_weapon_dist = 200000
+
+
+class SAM_SA_17_Buk_M1_2_LN_9A310M1_2(unittype.VehicleType):
+ id = "SA-17 Buk M1-2 LN 9A310M1-2"
+ name = "SAM SA-17 Buk M1-2 LN 9A310M1-2"
+ detection_range = 120000
+ threat_range = 50000
+ air_weapon_dist = 50000
+
+
+class SAM_SA_2__V759__LN_SM_90(unittype.VehicleType):
+ id = "S_75M_Volhov_V759"
+ name = "SAM SA-2 (V759) LN SM-90"
+ detection_range = 0
+ threat_range = 50000
+ air_weapon_dist = 50000
+
+
+class SAM_HQ_2_LN_SM_90(unittype.VehicleType):
+ id = "HQ_2_Guideline_LN"
+ name = "SAM HQ-2 LN SM-90"
+ detection_range = 0
+ threat_range = 50000
+ air_weapon_dist = 50000
+
+
+class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType):
+ id = "5p73 V-601P ln"
+ name = "SAM SA-3 (V-601P) LN 5P73"
+ detection_range = 0
+ threat_range = 18000
+ air_weapon_dist = 18000
+
+
+class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
+ id = "S-300PMU1 54K6 cp"
+ name = "SAM SA-20 S-300PMU1 CP 54K6"
+ detection_range = 0
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
+ id = "S-300VM 9S457ME cp"
+ name = "SAM SA-23 S-300VM 9S457ME CP"
+ detection_range = 0
+ threat_range = 0
+ air_weapon_dist = 0
+
+
+class SAM_SA_24_Igla_S_manpad(unittype.VehicleType):
+ id = "SA-24 Igla-S manpad"
+ name = "SAM SA-24 Igla-S manpad"
+ detection_range = 5000
+ threat_range = 6000
+ air_weapon_dist = 6000
+
+
+class SAM_SA_14_Strela_3_manpad(unittype.VehicleType):
+ id = "SA-14 Strela-3 manpad"
+ name = "SAM SA-14 Strela-3 manpad"
+ detection_range = 5000
+ threat_range = 4500
+ air_weapon_dist = 4500
diff --git a/pydcs_extensions/mod_units.py b/pydcs_extensions/mod_units.py
new file mode 100644
index 00000000..cfa1b321
--- /dev/null
+++ b/pydcs_extensions/mod_units.py
@@ -0,0 +1,65 @@
+from pydcs_extensions.a4ec.a4ec import A_4E_C
+from pydcs_extensions.highdigitsams import highdigitsams
+from pydcs_extensions.mb339.mb339 import MB_339PAN
+from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
+from pydcs_extensions.su57.su57 import Su_57
+import pydcs_extensions.frenchpack.frenchpack as frenchpack
+
+MODDED_AIRPLANES = [A_4E_C, MB_339PAN, Rafale_A_S, Rafale_M, Su_57]
+MODDED_VEHICLES = [
+ frenchpack._FIELD_HIDE,
+ frenchpack._FIELD_HIDE_SMALL,
+ frenchpack.SMOKE_SAM_IR,
+ frenchpack.SmokeD1,
+ frenchpack.SmokeD3,
+ frenchpack.AMX_10RCR,
+ frenchpack.AMX_10RCR_SEPAR,
+ frenchpack.ERC_90,
+ frenchpack.MO_120_RT,
+ frenchpack._53T2,
+ frenchpack.TRM_2000,
+ frenchpack.TRM_2000_Fuel,
+ frenchpack.TRM_2000_53T2,
+ frenchpack.TRM_2000_PAMELA,
+ frenchpack.VAB_MEDICAL,
+ frenchpack.VAB,
+ frenchpack.VAB__50,
+ frenchpack.VAB_T20_13,
+ frenchpack.VAB_MEPHISTO,
+ frenchpack.VAB_MORTIER,
+ frenchpack.VBL__50,
+ frenchpack.VBL_AANF1,
+ frenchpack.VBL,
+ frenchpack.VBAE_CRAB,
+ frenchpack.VBAE_CRAB_MMP,
+ frenchpack.AMX_30B2,
+ frenchpack.Tracma_TD_1500,
+ frenchpack.Infantry_Soldier_JTAC,
+ frenchpack.Char_M551_Sheridan,
+ frenchpack.Leclerc_Serie_XXI,
+ frenchpack.DIM__TOYOTA_BLUE,
+ frenchpack.DIM__TOYOTA_GREEN,
+ frenchpack.DIM__TOYOTA_DESERT,
+ frenchpack.DIM__KAMIKAZE,
+ highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E,
+ highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck,
+ highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E,
+ highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E,
+ highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR,
+ highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR,
+ highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR,
+ highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
+ highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
+ highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
+ highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
+ highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN,
+ highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN,
+ highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
+ highdigitsams.SAM_SA_2__V759__LN_SM_90,
+ highdigitsams.SAM_HQ_2_LN_SM_90,
+ highdigitsams.SAM_SA_3__V_601P__LN_5P73,
+ highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
+ highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
+ highdigitsams.SAM_SA_24_Igla_S_manpad,
+ highdigitsams.SAM_SA_14_Strela_3_manpad
+]
\ No newline at end of file
diff --git a/pydcs_extensions/su57/su57.py b/pydcs_extensions/su57/su57.py
new file mode 100644
index 00000000..a586222e
--- /dev/null
+++ b/pydcs_extensions/su57/su57.py
@@ -0,0 +1,1736 @@
+from enum import Enum
+
+from dcs import task
+from dcs.planes import PlaneType
+from dcs.weapons_data import Weapons
+
+
+class Su57Weapons:
+ Kh_59MK2 = {"clsid": "{KH_59MK2}", "name": "Kh-59MK2", "weight": None}
+ RVV_AE = {"clsid": "{RVV-AE}", "name": "RVV-AE", "weight": 250}
+ RVV_BD = {"clsid": "{RVV-BD}", "name": "RVV-BD", "weight": 600}
+ RVV_L = {"clsid": "{RVV-L}", "name": "RVV-L", "weight": 748}
+ RVV_M = {"clsid": "{RVV-M}", "name": "RVV-M", "weight": 190}
+ Su_57_Fuel_Tank = {"clsid": "{SU_57Tank}", "name": "Su-57 Fuel Tank", "weight": 1561.421}
+
+
+class Su_57(PlaneType):
+ id = "Su-57"
+ flyable = True
+ height = 4.074
+ width = 13.95
+ length = 19.008
+ fuel_max = 10300
+ max_speed = 2499.984
+ chaff = 100
+ flare = 96
+ charge_total = 200
+ chaff_charge_size = 1
+ flare_charge_size = 1
+ category = "Interceptor" #{78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
+
+ class Liveries:
+
+ class USSR(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Georgia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Venezuela(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Australia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Israel(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Sudan(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Norway(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Romania(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Iran(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Ukraine(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Libya(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Belgium(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Slovakia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Greece(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class UK(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Third_Reich(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Hungary(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Abkhazia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Morocco(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class United_Nations_Peacekeepers(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Switzerland(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class SouthOssetia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Vietnam(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class China(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Yemen(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Kuwait(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Serbia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Oman(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class India(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Egypt(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class TheNetherlands(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Poland(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Syria(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Finland(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Kazakhstan(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Denmark(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Sweden(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Croatia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class CzechRepublic(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class GDR(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Yugoslavia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Bulgaria(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class SouthKorea(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Tunisia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Lebanon(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Portugal(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Cuba(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Insurgents(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class SaudiArabia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class France(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class USA(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Honduras(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Qatar(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Russia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class United_Arab_Emirates(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Italian_Social_Republi(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Austria(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Bahrain(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Italy(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Chile(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Turkey(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Philippines(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Algeria(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Pakistan(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Malaysia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Indonesia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Iraq(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Germany(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class South_Africa(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Jordan(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Mexico(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class USAFAggressors(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Brazil(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Spain(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Belarus(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Canada(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class NorthKorea(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Ethiopia(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Japan(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Thailand(Enum):
+ _22 = "22"
+ _20 = "20"
+ _24 = "24"
+ _23 = "23"
+ _25 = "25"
+ _26 = "26"
+ _21 = "21"
+ _13 = "13"
+ _3 = "3"
+ _14 = "14"
+ _10 = "10"
+ _11 = "11"
+ _6 = "6"
+ _12 = "12"
+ _8 = "8"
+ _15 = "15"
+
+ class Pylon1:
+ R_73 = (1, Weapons.R_73)
+ RVV_AE = (1, Su57Weapons.RVV_AE)
+ RVV_M = (1, Su57Weapons.RVV_M)
+ Smoke_Generator___red = (1, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (1, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (1, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (1, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (1, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (1, Weapons.Smoke_Generator___orange)
+
+ class Pylon2:
+ R_27R = (2, Weapons.R_27R)
+ R_27ER = (2, Weapons.R_27ER)
+ R_27T = (2, Weapons.R_27T)
+ R_27ET = (2, Weapons.R_27ET)
+ R_77 = (2, Weapons.R_77)
+ R_73 = (2, Weapons.R_73)
+ Kh_31P = (2, Weapons.Kh_31P)
+ Kh_31A = (2, Weapons.Kh_31A)
+ Kh_29L = (2, Weapons.Kh_29L)
+ Kh_29T = (2, Weapons.Kh_29T)
+ Kh_59M = (2, Weapons.Kh_59M)
+ MER_6_FAB_100 = (2, Weapons.MER_6_FAB_100)
+ B_8M1___20_S_8KOM = (2, Weapons.B_8M1___20_S_8KOM)
+ B_13L___5_S_13_OF = (2, Weapons.B_13L___5_S_13_OF)
+ S_25_OFM = (2, Weapons.S_25_OFM)
+ BetAB_500 = (2, Weapons.BetAB_500)
+ KMGU_2___96_AO_2_5RT = (2, Weapons.KMGU_2___96_AO_2_5RT)
+ KMGU_2___96_PTAB_2_5KO = (2, Weapons.KMGU_2___96_PTAB_2_5KO)
+ FAB_250 = (2, Weapons.FAB_250)
+ RBK_250_PTAB_2_5M = (2, Weapons.RBK_250_PTAB_2_5M)
+ FAB_500_M62 = (2, Weapons.FAB_500_M62)
+ RBK_500_255_PTAB_10_5 = (2, Weapons.RBK_500_255_PTAB_10_5)
+ KAB_500L = (2, Weapons.KAB_500L)
+ KAB_500kr = (2, Weapons.KAB_500kr)
+ FAB_1500_M54 = (2, Weapons.FAB_1500_M54)
+ KAB_1500L = (2, Weapons.KAB_1500L)
+ MER_6_FAB_250 = (2, Weapons.MER_6_FAB_250)
+ RVV_BD = (2, Su57Weapons.RVV_BD)
+ RVV_AE = (2, Su57Weapons.RVV_AE)
+ RVV_M = (2, Su57Weapons.RVV_M)
+ RVV_L = (2, Su57Weapons.RVV_L)
+ Fuel_tank_800L_Wing = (2, Weapons.Fuel_tank_800L_Wing)
+ RN_28 = (2, Weapons.RN_28)
+ Su_57_Fuel_Tank = (2, Su57Weapons.Su_57_Fuel_Tank)
+ Kh_59MK2 = (2, Su57Weapons.Kh_59MK2)
+ Smoke_Generator___red = (2, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (2, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (2, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (2, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (2, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (2, Weapons.Smoke_Generator___orange)
+
+ class Pylon3:
+ R_73 = (3, Weapons.R_73)
+ Smoke_Generator___red = (3, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (3, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (3, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (3, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (3, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (3, Weapons.Smoke_Generator___orange)
+
+ class Pylon4:
+ R_27R = (4, Weapons.R_27R)
+ R_27ER = (4, Weapons.R_27ER)
+ R_27T = (4, Weapons.R_27T)
+ R_27ET = (4, Weapons.R_27ET)
+ R_77 = (4, Weapons.R_77)
+ R_73 = (4, Weapons.R_73)
+ Kh_31P = (4, Weapons.Kh_31P)
+ Kh_31A = (4, Weapons.Kh_31A)
+ Kh_29L = (4, Weapons.Kh_29L)
+ Kh_29T = (4, Weapons.Kh_29T)
+ Kh_59M = (4, Weapons.Kh_59M)
+ MER_6_FAB_100 = (4, Weapons.MER_6_FAB_100)
+ B_8M1___20_S_8KOM = (4, Weapons.B_8M1___20_S_8KOM)
+ B_13L___5_S_13_OF = (4, Weapons.B_13L___5_S_13_OF)
+ S_25_OFM = (4, Weapons.S_25_OFM)
+ BetAB_500 = (4, Weapons.BetAB_500)
+ KMGU_2___96_AO_2_5RT = (4, Weapons.KMGU_2___96_AO_2_5RT)
+ KMGU_2___96_PTAB_2_5KO = (4, Weapons.KMGU_2___96_PTAB_2_5KO)
+ FAB_250 = (4, Weapons.FAB_250)
+ RBK_250_PTAB_2_5M = (4, Weapons.RBK_250_PTAB_2_5M)
+ FAB_500_M62 = (4, Weapons.FAB_500_M62)
+ RBK_500_255_PTAB_10_5 = (4, Weapons.RBK_500_255_PTAB_10_5)
+ KAB_500L = (4, Weapons.KAB_500L)
+ KAB_500kr = (4, Weapons.KAB_500kr)
+ FAB_1500_M54 = (4, Weapons.FAB_1500_M54)
+ KAB_1500L = (4, Weapons.KAB_1500L)
+ MER_6_FAB_250 = (4, Weapons.MER_6_FAB_250)
+ RVV_BD = (4, Su57Weapons.RVV_BD)
+ RVV_AE = (4, Su57Weapons.RVV_AE)
+ RVV_M = (4, Su57Weapons.RVV_M)
+ RVV_L = (4, Su57Weapons.RVV_L)
+ RN_28 = (4, Weapons.RN_28)
+ Su_57_Fuel_Tank = (4, Su57Weapons.Su_57_Fuel_Tank)
+ Kh_59MK2 = (4, Su57Weapons.Kh_59MK2)
+
+ class Pylon5:
+ R_77 = (5, Weapons.R_77)
+ RVV_AE = (5, Su57Weapons.RVV_AE)
+ RVV_M = (5, Su57Weapons.RVV_M)
+ Kh_59MK2 = (5, Su57Weapons.Kh_59MK2)
+
+ class Pylon6:
+ R_77 = (6, Weapons.R_77)
+ RVV_AE = (6, Su57Weapons.RVV_AE)
+ RVV_M = (6, Su57Weapons.RVV_M)
+ Kh_59MK2 = (6, Su57Weapons.Kh_59MK2)
+
+ class Pylon7:
+ R_77 = (7, Weapons.R_77)
+ RVV_AE = (7, Su57Weapons.RVV_AE)
+ RVV_M = (7, Su57Weapons.RVV_M)
+ Kh_59MK2 = (7, Su57Weapons.Kh_59MK2)
+
+ class Pylon8:
+ R_77 = (8, Weapons.R_77)
+ RVV_AE = (8, Su57Weapons.RVV_AE)
+ RVV_M = (8, Su57Weapons.RVV_M)
+ Kh_59MK2 = (8, Su57Weapons.Kh_59MK2)
+
+ class Pylon9:
+ R_27R = (9, Weapons.R_27R)
+ R_27ER = (9, Weapons.R_27ER)
+ R_27T = (9, Weapons.R_27T)
+ R_27ET = (9, Weapons.R_27ET)
+ R_77 = (9, Weapons.R_77)
+ R_73 = (9, Weapons.R_73)
+ Kh_31P = (9, Weapons.Kh_31P)
+ Kh_31A = (9, Weapons.Kh_31A)
+ Kh_29L = (9, Weapons.Kh_29L)
+ Kh_29T = (9, Weapons.Kh_29T)
+ Kh_59M = (9, Weapons.Kh_59M)
+ MER_6_FAB_100 = (9, Weapons.MER_6_FAB_100)
+ B_8M1___20_S_8KOM = (9, Weapons.B_8M1___20_S_8KOM)
+ B_13L___5_S_13_OF = (9, Weapons.B_13L___5_S_13_OF)
+ S_25_OFM = (9, Weapons.S_25_OFM)
+ BetAB_500 = (9, Weapons.BetAB_500)
+ KMGU_2___96_AO_2_5RT = (9, Weapons.KMGU_2___96_AO_2_5RT)
+ KMGU_2___96_PTAB_2_5KO = (9, Weapons.KMGU_2___96_PTAB_2_5KO)
+ FAB_250 = (9, Weapons.FAB_250)
+ RBK_250_PTAB_2_5M = (9, Weapons.RBK_250_PTAB_2_5M)
+ FAB_500_M62 = (9, Weapons.FAB_500_M62)
+ RBK_500_255_PTAB_10_5 = (9, Weapons.RBK_500_255_PTAB_10_5)
+ KAB_500L = (9, Weapons.KAB_500L)
+ KAB_500kr = (9, Weapons.KAB_500kr)
+ FAB_1500_M54 = (9, Weapons.FAB_1500_M54)
+ KAB_1500L = (9, Weapons.KAB_1500L)
+ MER_6_FAB_250 = (9, Weapons.MER_6_FAB_250)
+ RVV_BD = (9, Su57Weapons.RVV_BD)
+ RVV_AE = (9, Su57Weapons.RVV_AE)
+ RVV_M = (9, Su57Weapons.RVV_M)
+ RVV_L = (9, Su57Weapons.RVV_L)
+ RN_28 = (9, Weapons.RN_28)
+ Su_57_Fuel_Tank = (9, Su57Weapons.Su_57_Fuel_Tank)
+ Kh_59MK2 = (9, Su57Weapons.Kh_59MK2)
+
+ class Pylon10:
+ R_73 = (10, Weapons.R_73)
+ Smoke_Generator___red = (10, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (10, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (10, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (10, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (10, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (10, Weapons.Smoke_Generator___orange)
+
+ class Pylon11:
+ R_27R = (11, Weapons.R_27R)
+ R_27ER = (11, Weapons.R_27ER)
+ R_27T = (11, Weapons.R_27T)
+ R_27ET = (11, Weapons.R_27ET)
+ R_77 = (11, Weapons.R_77)
+ R_73 = (11, Weapons.R_73)
+ Kh_31P = (11, Weapons.Kh_31P)
+ Kh_31A = (11, Weapons.Kh_31A)
+ Kh_29L = (11, Weapons.Kh_29L)
+ Kh_29T = (11, Weapons.Kh_29T)
+ Kh_59M = (11, Weapons.Kh_59M)
+ MER_6_FAB_100 = (11, Weapons.MER_6_FAB_100)
+ B_8M1___20_S_8KOM = (11, Weapons.B_8M1___20_S_8KOM)
+ B_13L___5_S_13_OF = (11, Weapons.B_13L___5_S_13_OF)
+ S_25_OFM = (11, Weapons.S_25_OFM)
+ BetAB_500 = (11, Weapons.BetAB_500)
+ KMGU_2___96_AO_2_5RT = (11, Weapons.KMGU_2___96_AO_2_5RT)
+ KMGU_2___96_PTAB_2_5KO = (11, Weapons.KMGU_2___96_PTAB_2_5KO)
+ FAB_250 = (11, Weapons.FAB_250)
+ RBK_250_PTAB_2_5M = (11, Weapons.RBK_250_PTAB_2_5M)
+ FAB_500_M62 = (11, Weapons.FAB_500_M62)
+ RBK_500_255_PTAB_10_5 = (11, Weapons.RBK_500_255_PTAB_10_5)
+ KAB_500L = (11, Weapons.KAB_500L)
+ KAB_500kr = (11, Weapons.KAB_500kr)
+ FAB_1500_M54 = (11, Weapons.FAB_1500_M54)
+ KAB_1500L = (11, Weapons.KAB_1500L)
+ MER_6_FAB_250 = (11, Weapons.MER_6_FAB_250)
+#ERRR {R-33}
+ RVV_BD = (11,Su57Weapons.RVV_BD)
+ RVV_AE = (11, Su57Weapons.RVV_AE)
+ RVV_M = (11, Su57Weapons.RVV_M)
+ RVV_L = (11, Su57Weapons.RVV_L)
+ Fuel_tank_800L_Wing = (11, Weapons.Fuel_tank_800L_Wing)
+ Su_57_Fuel_Tank = (11, Su57Weapons.Su_57_Fuel_Tank)
+ RN_28 = (11, Weapons.RN_28)
+ Smoke_Generator___red = (11, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (11, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (11, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (11, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (11, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (11, Weapons.Smoke_Generator___orange)
+ Kh_59MK2 = (11, Su57Weapons.Kh_59MK2)
+
+ class Pylon12:
+ R_73 = (12, Weapons.R_73)
+ RVV_AE = (12, Su57Weapons.RVV_AE)
+ RVV_M = (12, Su57Weapons.RVV_M)
+ Smoke_Generator___red = (12, Weapons.Smoke_Generator___red)
+ Smoke_Generator___green = (12, Weapons.Smoke_Generator___green)
+ Smoke_Generator___blue = (12, Weapons.Smoke_Generator___blue)
+ Smoke_Generator___white = (12, Weapons.Smoke_Generator___white)
+ Smoke_Generator___yellow = (12, Weapons.Smoke_Generator___yellow)
+ Smoke_Generator___orange = (12, Weapons.Smoke_Generator___orange)
+
+ pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
+
+ tasks = [task.CAP, task.Intercept, task.Escort, task.FighterSweep, task.AFAC, task.GroundAttack, task.RunwayAttack, task.AntishipStrike, task.CAS]
+ task_default = task.CAP
diff --git a/pyinstaller.spec b/pyinstaller.spec
index 839c641f..e545d073 100644
--- a/pyinstaller.spec
+++ b/pyinstaller.spec
@@ -11,6 +11,7 @@ analysis = Analysis(
('resources', 'resources'),
('resources/caucasus.p', 'dcs/terrain/'),
('resources/nevada.p', 'dcs/terrain/'),
+ ('buildnumber', './')
],
hookspath=[],
runtime_hooks=[],
diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py
new file mode 100644
index 00000000..36ca6890
--- /dev/null
+++ b/qt_ui/dialogs.py
@@ -0,0 +1,67 @@
+"""Application-wide dialog management."""
+from typing import Optional
+
+from gen.flights.flight import Flight
+from theater.missiontarget import MissionTarget
+from .models import GameModel, PackageModel
+from .windows.mission.QEditFlightDialog import QEditFlightDialog
+from .windows.mission.QPackageDialog import (
+ QEditPackageDialog,
+ QNewPackageDialog,
+)
+
+
+class Dialog:
+ """Dialog management singleton.
+
+ Opens dialogs and keeps references to dialog windows so that their creators
+ do not need to worry about the lifetime of the dialog object, and can open
+ dialogs without needing to have their own reference to common data like the
+ game model.
+ """
+
+ #: The game model. Is only None before initialization, as the game model
+ #: itself is responsible for handling the case where no game is loaded.
+ game_model: Optional[GameModel] = None
+
+ new_package_dialog: Optional[QNewPackageDialog] = None
+ edit_package_dialog: Optional[QEditPackageDialog] = None
+ edit_flight_dialog: Optional[QEditFlightDialog] = None
+
+ @classmethod
+ def set_game(cls, game_model: GameModel) -> None:
+ """Sets the game model."""
+ cls.game_model = game_model
+
+ @classmethod
+ def open_new_package_dialog(cls, mission_target: MissionTarget, parent=None):
+ """Opens the dialog to create a new package with the given target."""
+ cls.new_package_dialog = QNewPackageDialog(
+ cls.game_model,
+ cls.game_model.ato_model,
+ mission_target,
+ parent=parent
+ )
+ cls.new_package_dialog.show()
+
+ @classmethod
+ def open_edit_package_dialog(cls, package_model: PackageModel):
+ """Opens the dialog to edit the given package."""
+ cls.edit_package_dialog = QEditPackageDialog(
+ cls.game_model,
+ cls.game_model.ato_model,
+ package_model
+ )
+ cls.edit_package_dialog.show()
+
+ @classmethod
+ def open_edit_flight_dialog(cls, package_model: PackageModel,
+ flight: Flight, parent=None) -> None:
+ """Opens the dialog to edit the given flight."""
+ cls.edit_flight_dialog = QEditFlightDialog(
+ cls.game_model,
+ package_model.package,
+ flight,
+ parent=parent
+ )
+ cls.edit_flight_dialog.show()
diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py
new file mode 100644
index 00000000..bec194fb
--- /dev/null
+++ b/qt_ui/displayoptions.py
@@ -0,0 +1,69 @@
+"""Visibility options for the game map."""
+from dataclasses import dataclass
+from typing import Iterator, Optional, Union
+
+
+@dataclass
+class DisplayRule:
+ name: str
+ _value: bool
+
+ @property
+ def menu_text(self) -> str:
+ return self.name
+
+ @property
+ def value(self) -> bool:
+ return self._value
+
+ @value.setter
+ def value(self, value: bool) -> None:
+ from qt_ui.widgets.map.QLiberationMap import QLiberationMap
+ self._value = value
+ QLiberationMap.instance.reload_scene()
+ QLiberationMap.instance.update()
+
+ def __bool__(self) -> bool:
+ return self.value
+
+
+class DisplayGroup:
+ def __init__(self, name: Optional[str]) -> None:
+ self.name = name
+
+ def __iter__(self) -> Iterator[DisplayRule]:
+ # Python 3.6 enforces that __dict__ is order preserving by default.
+ for value in self.__dict__.values():
+ if isinstance(value, DisplayRule):
+ yield value
+
+
+class FlightPathOptions(DisplayGroup):
+ def __init__(self) -> None:
+ super().__init__("Flight Paths")
+ self.hide = DisplayRule("Hide Flight Paths", False)
+ self.only_selected = DisplayRule("Show Selected Flight Path", False)
+ self.all = DisplayRule("Show All Flight Paths", True)
+
+
+class DisplayOptions:
+ ground_objects = DisplayRule("Ground Objects", True)
+ control_points = DisplayRule("Control Points", True)
+ lines = DisplayRule("Lines", True)
+ events = DisplayRule("Events", True)
+ sam_ranges = DisplayRule("Ally SAM Threat Range", False)
+ enemy_sam_ranges = DisplayRule("Enemy SAM Threat Range", True)
+ detection_range = DisplayRule("SAM Detection Range", False)
+ map_poly = DisplayRule("Map Polygon Debug Mode", False)
+ waypoint_info = DisplayRule("Waypoint Information", True)
+ culling = DisplayRule("Display Culling Zones", False)
+ flight_paths = FlightPathOptions()
+
+ @classmethod
+ def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
+ # Python 3.6 enforces that __dict__ is order preserving by default.
+ for value in cls.__dict__.values():
+ if isinstance(value, DisplayRule):
+ yield value
+ elif isinstance(value, DisplayGroup):
+ yield value
diff --git a/userdata/liberation_install.py b/qt_ui/liberation_install.py
similarity index 99%
rename from userdata/liberation_install.py
rename to qt_ui/liberation_install.py
index 5f19ec0a..0440043d 100644
--- a/userdata/liberation_install.py
+++ b/qt_ui/liberation_install.py
@@ -4,13 +4,14 @@ from shutil import copyfile
import dcs
-from userdata import persistency
+from game import persistency
global __dcs_saved_game_directory
global __dcs_installation_directory
PREFERENCES_FILE_PATH = "liberation_preferences.json"
+
def init():
global __dcs_saved_game_directory
global __dcs_installation_directory
diff --git a/userdata/liberation_theme.py b/qt_ui/liberation_theme.py
similarity index 68%
rename from userdata/liberation_theme.py
rename to qt_ui/liberation_theme.py
index 703d15ee..79714209 100644
--- a/userdata/liberation_theme.py
+++ b/qt_ui/liberation_theme.py
@@ -1,7 +1,7 @@
import json
+import logging
import os
-
-import qt_ui.uiconstants as CONST
+from typing import Dict
global __theme_index
@@ -10,29 +10,44 @@ THEME_PREFERENCES_FILE_PATH = "liberation_theme.json"
DEFAULT_THEME_INDEX = 1
+# new themes can be added here
+THEMES: Dict[int, Dict[str, str]] = {
+ 0: {'themeName': 'Vanilla',
+ 'themeFile': 'windows-style.css',
+ 'themeIcons': 'medium',
+ },
+
+ 1: {'themeName': 'DCS World',
+ 'themeFile': 'style-dcs.css',
+ 'themeIcons': 'light',
+ },
+
+}
+
+
def init():
global __theme_index
__theme_index = DEFAULT_THEME_INDEX
- print("init setting theme index to " + str(__theme_index))
if os.path.isfile(THEME_PREFERENCES_FILE_PATH):
try:
with(open(THEME_PREFERENCES_FILE_PATH)) as prefs:
pref_data = json.loads(prefs.read())
__theme_index = pref_data["theme_index"]
- print(__theme_index)
set_theme_index(__theme_index)
save_theme_config()
- print("file setting theme index to " + str(__theme_index))
except:
# is this necessary?
set_theme_index(DEFAULT_THEME_INDEX)
- print("except setting theme index to " + str(__theme_index))
+ logging.exception("Unable to change theme")
else:
# is this necessary?
set_theme_index(DEFAULT_THEME_INDEX)
- print("else setting theme index to " + str(__theme_index))
+ logging.error(
+ f"Using default theme because {THEME_PREFERENCES_FILE_PATH} "
+ "does not exist"
+ )
# set theme index then use save_theme_config to save to file
@@ -49,19 +64,19 @@ def get_theme_index():
# get theme name based on current index
def get_theme_name():
- theme_name = CONST.THEMES[get_theme_index()]['themeName']
+ theme_name = THEMES[get_theme_index()]['themeName']
return theme_name
# get theme icon sub-folder name based on current index
def get_theme_icons():
- theme_icons = CONST.THEMES[get_theme_index()]['themeIcons']
+ theme_icons = THEMES[get_theme_index()]['themeIcons']
return str(theme_icons)
# get theme stylesheet css based on current index
def get_theme_css_file():
- theme_file = CONST.THEMES[get_theme_index()]['themeFile']
+ theme_file = THEMES[get_theme_index()]['themeFile']
return str(theme_file)
diff --git a/qt_ui/logging_config.py b/qt_ui/logging_config.py
new file mode 100644
index 00000000..739e8ac0
--- /dev/null
+++ b/qt_ui/logging_config.py
@@ -0,0 +1,22 @@
+"""Logging APIs."""
+import logging
+import os
+from logging.handlers import RotatingFileHandler
+
+
+def init_logging(version: str) -> None:
+ """Initializes the logging configuration."""
+ if not os.path.isdir("./logs"):
+ os.mkdir("logs")
+
+ fmt = "%(asctime)s :: %(levelname)s :: %(message)s"
+ logging.basicConfig(level=logging.DEBUG, format=fmt)
+ logger = logging.getLogger()
+
+ handler = RotatingFileHandler('./logs/liberation.log', 'a', 5000000, 1)
+ handler.setLevel(logging.INFO)
+ handler.setFormatter(logging.Formatter(fmt))
+
+ logger.addHandler(handler)
+
+ logger.info(f"DCS Liberation {version}")
diff --git a/qt_ui/main.py b/qt_ui/main.py
index e019d32c..de179f6b 100644
--- a/qt_ui/main.py
+++ b/qt_ui/main.py
@@ -1,5 +1,3 @@
-from userdata import logging_config
-
import logging
import os
import sys
@@ -9,16 +7,24 @@ from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
-from qt_ui import uiconstants
+from game import db, persistency, VERSION
+from qt_ui import (
+ liberation_install,
+ liberation_theme,
+ logging_config,
+ uiconstants,
+)
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
-from qt_ui.windows.preferences.QLiberationFirstStartWindow import QLiberationFirstStartWindow
-from userdata import liberation_install, persistency, liberation_theme
+from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
+ QLiberationFirstStartWindow
# Logging setup
-logging_config.init_logging(uiconstants.VERSION_STRING)
+logging_config.init_logging(VERSION)
if __name__ == "__main__":
+ # Load eagerly to catch errors early.
+ db.FACTIONS.initialize()
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
app = QApplication(sys.argv)
diff --git a/qt_ui/models.py b/qt_ui/models.py
new file mode 100644
index 00000000..07b990d6
--- /dev/null
+++ b/qt_ui/models.py
@@ -0,0 +1,295 @@
+"""Qt data models for game objects."""
+import datetime
+from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
+
+from PySide2.QtCore import (
+ QAbstractListModel,
+ QModelIndex,
+ Qt,
+ Signal,
+)
+from PySide2.QtGui import QIcon
+
+from game import db
+from game.game import Game
+from gen.ato import AirTaskingOrder, Package
+from gen.flights.flight import Flight
+from gen.flights.traveltime import TotEstimator
+from qt_ui.uiconstants import AIRCRAFT_ICONS
+from theater.missiontarget import MissionTarget
+
+
+class DeletableChildModelManager:
+ """Manages lifetimes for child models.
+
+ Qt's data models don't have a good way of modeling related data aside from
+ lists, tables, or trees of similar objects. We could build one monolithic
+ GameModel that tracks all of the data in the game and use the parent/child
+ relationships of that model to index down into the ATO, packages, flights,
+ etc, but doing so is error prone because it requires us to manually manage
+ that relationship tree and keep our own mappings from row/column into
+ specific members.
+
+ However, creating child models outside of the tree means that removing an
+ item from the parent will not signal the child's deletion to any views, so
+ we must track this explicitly.
+
+ Any model which has child data types should use this class to track the
+ deletion of child models. All child model types must define a signal named
+ `deleted`. This signal will be emitted when the child model is being
+ deleted. Any views displaying such data should subscribe to those events and
+ update their display accordingly.
+ """
+
+ #: The type of data owned by models created by this class.
+ DataType = TypeVar("DataType")
+
+ #: The type of model managed by this class.
+ ModelType = TypeVar("ModelType")
+
+ ModelDict = Dict[DataType, ModelType]
+
+ def __init__(self, create_model: Callable[[DataType], ModelType]) -> None:
+ self.create_model = create_model
+ self.models: DeletableChildModelManager.ModelDict = {}
+
+ def acquire(self, data: DataType) -> ModelType:
+ """Returns a model for the given child data.
+
+ If a model has already been created for the given data, it will be
+ returned. The data type must be hashable.
+ """
+ if data in self.models:
+ return self.models[data]
+ model = self.create_model(data)
+ self.models[data] = model
+ return model
+
+ def release(self, data: DataType) -> None:
+ """Releases the model matching the given data, if one exists.
+
+ If the given data has had a model created for it, that model will be
+ deleted and its `deleted` signal will be emitted.
+ """
+ if data in self.models:
+ model = self.models[data]
+ del self.models[data]
+ model.deleted.emit()
+
+ def clear(self) -> None:
+ """Deletes all managed models."""
+ for data in list(self.models.keys()):
+ self.release(data)
+
+
+class NullListModel(QAbstractListModel):
+ """Generic empty list model."""
+
+ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
+ return 0
+
+ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
+ return None
+
+
+class PackageModel(QAbstractListModel):
+ """The model for an ATO package."""
+
+ FlightRole = Qt.UserRole
+
+ #: Emitted when this package is being deleted from the ATO.
+ deleted = Signal()
+
+ def __init__(self, package: Package) -> None:
+ super().__init__()
+ self.package = package
+
+ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
+ return len(self.package.flights)
+
+ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
+ if not index.isValid():
+ return None
+ flight = self.flight_at_index(index)
+ if role == Qt.DisplayRole:
+ return self.text_for_flight(flight)
+ if role == Qt.DecorationRole:
+ return self.icon_for_flight(flight)
+ elif role == PackageModel.FlightRole:
+ return flight
+ return None
+
+ def text_for_flight(self, flight: Flight) -> str:
+ """Returns the text that should be displayed for the flight."""
+ task = flight.flight_type.name
+ count = flight.count
+ name = db.unit_type_name(flight.unit_type)
+ estimator = TotEstimator(self.package)
+ delay = datetime.timedelta(
+ seconds=int(estimator.mission_start_time(flight).total_seconds()))
+ origin = flight.from_cp.name
+ return f"[{task}] {count} x {name} from {origin} in {delay}"
+
+ @staticmethod
+ def icon_for_flight(flight: Flight) -> Optional[QIcon]:
+ """Returns the icon that should be displayed for the flight."""
+ name = db.unit_type_name(flight.unit_type)
+ if name in AIRCRAFT_ICONS:
+ return QIcon(AIRCRAFT_ICONS[name])
+ return None
+
+ def add_flight(self, flight: Flight) -> None:
+ """Adds the given flight to the package."""
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.package.add_flight(flight)
+ self.endInsertRows()
+
+ def delete_flight_at_index(self, index: QModelIndex) -> None:
+ """Removes the flight at the given index from the package."""
+ self.delete_flight(self.flight_at_index(index))
+
+ def delete_flight(self, flight: Flight) -> None:
+ """Removes the given flight from the package.
+
+ If the flight is using claimed inventory, the caller is responsible for
+ returning that inventory.
+ """
+ index = self.package.flights.index(flight)
+ self.beginRemoveRows(QModelIndex(), index, index)
+ self.package.remove_flight(flight)
+ self.endRemoveRows()
+
+ def flight_at_index(self, index: QModelIndex) -> Flight:
+ """Returns the flight located at the given index."""
+ return self.package.flights[index.row()]
+
+ def update_tot(self, tot: datetime.timedelta) -> None:
+ self.package.time_over_target = tot
+ self.layoutChanged.emit()
+
+ @property
+ def mission_target(self) -> MissionTarget:
+ """Returns the mission target of the package."""
+ package = self.package
+ target = package.target
+ return target
+
+ @property
+ def description(self) -> str:
+ """Returns the description of the package."""
+ return self.package.package_description
+
+ @property
+ def flights(self) -> Iterator[Flight]:
+ """Iterates over the flights in the package."""
+ for flight in self.package.flights:
+ yield flight
+
+
+class AtoModel(QAbstractListModel):
+ """The model for an AirTaskingOrder."""
+
+ PackageRole = Qt.UserRole
+
+ client_slots_changed = Signal()
+
+ def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
+ super().__init__()
+ self.game = game
+ self.ato = ato
+ self.package_models = DeletableChildModelManager(PackageModel)
+
+ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
+ return len(self.ato.packages)
+
+ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
+ if not index.isValid():
+ return None
+ package = self.ato.packages[index.row()]
+ if role == Qt.DisplayRole:
+ return f"{package.package_description} {package.target.name}"
+ elif role == AtoModel.PackageRole:
+ return package
+ return None
+
+ def add_package(self, package: Package) -> None:
+ """Adds a package to the ATO."""
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
+ self.ato.add_package(package)
+ self.endInsertRows()
+ # noinspection PyUnresolvedReferences
+ self.client_slots_changed.emit()
+
+ def delete_package_at_index(self, index: QModelIndex) -> None:
+ """Removes the package at the given index from the ATO."""
+ self.delete_package(self.package_at_index(index))
+
+ def delete_package(self, package: Package) -> None:
+ """Removes the given package from the ATO."""
+ self.package_models.release(package)
+ index = self.ato.packages.index(package)
+ self.beginRemoveRows(QModelIndex(), index, index)
+ self.ato.remove_package(package)
+ for flight in package.flights:
+ self.game.aircraft_inventory.return_from_flight(flight)
+ self.endRemoveRows()
+ # noinspection PyUnresolvedReferences
+ self.client_slots_changed.emit()
+
+ def package_at_index(self, index: QModelIndex) -> Package:
+ """Returns the package at the given index."""
+ return self.ato.packages[index.row()]
+
+ def replace_from_game(self, game: Optional[Game], player: bool) -> None:
+ """Updates the ATO object to match the updated game object.
+
+ If the game is None (as is the case when no game has been loaded), an
+ empty ATO will be used.
+ """
+ self.beginResetModel()
+ self.game = game
+ self.package_models.clear()
+ if self.game is not None:
+ if player:
+ self.ato = game.blue_ato
+ else:
+ self.ato = game.red_ato
+ else:
+ self.ato = AirTaskingOrder()
+ self.endResetModel()
+ # noinspection PyUnresolvedReferences
+ self.client_slots_changed.emit()
+
+ def get_package_model(self, index: QModelIndex) -> PackageModel:
+ """Returns a model for the package at the given index."""
+ return self.package_models.acquire(self.package_at_index(index))
+
+ @property
+ def packages(self) -> Iterator[PackageModel]:
+ """Iterates over all the packages in the ATO."""
+ for package in self.ato.packages:
+ yield self.package_models.acquire(package)
+
+
+class GameModel:
+ """A model for the Game object.
+
+ This isn't a real Qt data model, but simplifies management of the game and
+ its ATO objects.
+ """
+ def __init__(self) -> None:
+ self.game: Optional[Game] = None
+ self.ato_model = AtoModel(self.game, AirTaskingOrder())
+ self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
+
+ def set(self, game: Optional[Game]) -> None:
+ """Updates the managed Game object.
+
+ The argument will be None when no game has been loaded. In this state,
+ much of the UI is still visible and needs to handle that behavior. To
+ simplify that case, the AtoModel will model an empty ATO when no game is
+ loaded.
+ """
+ self.game = game
+ self.ato_model.replace_from_game(self.game, player=True)
+ self.red_ato_model.replace_from_game(self.game, player=False)
diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py
index 1c6e84bc..b256705d 100644
--- a/qt_ui/uiconstants.py
+++ b/qt_ui/uiconstants.py
@@ -1,14 +1,12 @@
-# URL for UI links
import os
from typing import Dict
+from pathlib import Path
from PySide2.QtGui import QColor, QFont, QPixmap
-from game.event import UnitsDeliveryEvent, FrontlineAttackEvent
from theater.theatergroundobject import CATEGORY_MAP
-from userdata.liberation_theme import get_theme_icons
+from .liberation_theme import get_theme_icons
-VERSION_STRING = "2.1.5"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",
@@ -28,37 +26,32 @@ FONT_PRIMARY_I = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=True)
FONT_PRIMARY_B = QFont(FONT_NAME, FONT_SIZE, weight=75, italic=False)
FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False)
-# new themes can be added here
-THEMES: Dict[int, Dict[str, str]] = {
- 0: {'themeName': 'Vanilla',
- 'themeFile': 'windows-style.css',
- 'themeIcons': 'medium',
- },
-
- 1: {'themeName': 'DCS World',
- 'themeFile': 'style-dcs.css',
- 'themeIcons': 'light',
- },
-
-}
-
COLORS: Dict[str, QColor] = {
"white": QColor(255, 255, 255),
"white_transparent": QColor(255, 255, 255, 35),
"grey_transparent": QColor(150, 150, 150, 30),
+ "light_red": QColor(231, 92, 83, 90),
"red": QColor(200, 80, 80),
"dark_red": QColor(140, 20, 20),
"red_transparent": QColor(227, 32, 0, 20),
+ "transparent": QColor(255, 255, 255, 0),
+ "light_blue": QColor(105, 182, 240, 90),
"blue": QColor(0, 132, 255),
"dark_blue": QColor(45, 62, 80),
+ "sea_blue": QColor(52, 68, 85),
"blue_transparent": QColor(0, 132, 255, 20),
+ "purple": QColor(187, 137, 255),
+ "yellow": QColor(238, 225, 123),
+
"bright_red": QColor(150, 80, 80),
"super_red": QColor(227, 32, 0),
"green": QColor(128, 186, 128),
+ "light_green": QColor(223, 255, 173),
+ "light_green_transparent": QColor(180, 255, 140, 50),
"bright_green": QColor(64, 200, 64),
"black": QColor(0, 0, 0),
@@ -69,6 +62,10 @@ COLORS: Dict[str, QColor] = {
"night_overlay": QColor(12, 20, 69),
"dawn_dust_overlay": QColor(46, 38, 85),
+ "grey": QColor(150, 150, 150),
+ "dark_grey": QColor(75, 75, 75),
+ "dark_dark_grey": QColor(48, 48, 48),
+
}
CP_SIZE = 12
@@ -85,10 +82,10 @@ def load_icons():
ICONS["Hangar"] = QPixmap("./resources/ui/misc/hangar.png")
ICONS["Terrain_Caucasus"] = QPixmap("./resources/ui/terrain_caucasus.gif")
- ICONS["Terrain_Persian_Gulf"] = QPixmap("./resources/ui/terrain_pg.gif")
+ ICONS["Terrain_PersianGulf"] = QPixmap("./resources/ui/terrain_pg.gif")
ICONS["Terrain_Nevada"] = QPixmap("./resources/ui/terrain_nevada.gif")
ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif")
- ICONS["Terrain_Channel"] = QPixmap("./resources/ui/terrain_channel.gif")
+ ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif")
ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif")
ICONS["Dawn"] = QPixmap("./resources/ui/daytime/dawn.png")
@@ -115,6 +112,8 @@ def load_icons():
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
ICONS["Cheat"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/cheat.png")
+ ICONS["Plugins"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/plugins.png")
+ ICONS["PluginsOptions"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/pluginsoptions.png")
ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")
@@ -127,15 +126,12 @@ EVENT_ICONS: Dict[str, QPixmap] = {}
def load_event_icons():
for image in os.listdir("./resources/ui/events/"):
- print(image)
if image.endswith(".PNG"):
EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image))
def load_aircraft_icons():
for aircraft in os.listdir("./resources/ui/units/aircrafts/"):
- print(aircraft)
if aircraft.endswith(".jpg"):
- print(aircraft[:-7] + " : " + os.path.join("./resources/ui/units/aircrafts/", aircraft) + " ")
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft))
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
@@ -144,7 +140,5 @@ def load_aircraft_icons():
def load_vehicle_icons():
for vehicle in os.listdir("./resources/ui/units/vehicles/"):
- print(vehicle)
if vehicle.endswith(".jpg"):
- print(vehicle[:-7] + " : " + os.path.join("./resources/ui/units/vehicles/", vehicle) + " ")
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle))
diff --git a/qt_ui/widgets/QFlightSizeSpinner.py b/qt_ui/widgets/QFlightSizeSpinner.py
new file mode 100644
index 00000000..a2619507
--- /dev/null
+++ b/qt_ui/widgets/QFlightSizeSpinner.py
@@ -0,0 +1,13 @@
+"""Spin box for selecting the number of aircraft in a flight."""
+from PySide2.QtWidgets import QSpinBox
+
+
+class QFlightSizeSpinner(QSpinBox):
+ """Spin box for selecting the number of aircraft in a flight."""
+
+ def __init__(self, min_size: int = 1, max_size: int = 4,
+ default_size: int = 2) -> None:
+ super().__init__()
+ self.setMinimum(min_size)
+ self.setMaximum(max_size)
+ self.setValue(default_size)
diff --git a/qt_ui/widgets/QLabeledWidget.py b/qt_ui/widgets/QLabeledWidget.py
new file mode 100644
index 00000000..91bb52bb
--- /dev/null
+++ b/qt_ui/widgets/QLabeledWidget.py
@@ -0,0 +1,24 @@
+"""A layout containing a widget with an associated label."""
+from typing import Optional
+
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
+
+
+class QLabeledWidget(QHBoxLayout):
+ """A layout containing a widget with an associated label.
+
+ Best used for vertical forms, where the given widget is the input and the
+ label is used to name the input.
+ """
+
+ def __init__(self, text: str, widget: QWidget,
+ tooltip: Optional[str] = None) -> None:
+ super().__init__()
+ label = QLabel(text)
+ self.addWidget(label)
+ self.addStretch()
+ self.addWidget(widget, alignment=Qt.AlignRight)
+ if tooltip is not None:
+ label.setToolTip(tooltip)
+ widget.setToolTip(tooltip)
diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py
index 30725095..79f4e77e 100644
--- a/qt_ui/widgets/QTopPanel.py
+++ b/qt_ui/widgets/QTopPanel.py
@@ -1,28 +1,44 @@
-from PySide2.QtWidgets import QFrame, QHBoxLayout, QPushButton, QVBoxLayout, QGroupBox
+from typing import List, Optional
-from game import Game
-from qt_ui.widgets.QBudgetBox import QBudgetBox
-from qt_ui.widgets.QFactionsInfos import QFactionsInfos
-from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
-from qt_ui.windows.stats.QStatsWindow import QStatsWindow
-from qt_ui.widgets.QTurnCounter import QTurnCounter
+from PySide2.QtWidgets import (
+ QFrame,
+ QGroupBox,
+ QHBoxLayout,
+ QMessageBox,
+ QPushButton,
+)
import qt_ui.uiconstants as CONST
+from game import Game
+from game.event import CAP, CAS, FrontlineAttackEvent
+from gen.ato import Package
+from gen.flights.traveltime import TotEstimator
+from qt_ui.models import GameModel
+from qt_ui.widgets.QBudgetBox import QBudgetBox
+from qt_ui.widgets.QFactionsInfos import QFactionsInfos
+from qt_ui.widgets.QTurnCounter import QTurnCounter
+from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
-from qt_ui.windows.mission.QMissionPlanning import QMissionPlanning
+from qt_ui.windows.QWaitingForMissionResultWindow import \
+ QWaitingForMissionResultWindow
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
+from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QTopPanel(QFrame):
- def __init__(self, game: Game):
+ def __init__(self, game_model: GameModel):
super(QTopPanel, self).__init__()
- self.game = game
+ self.game_model = game_model
self.setMaximumHeight(70)
self.init_ui()
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
+ @property
+ def game(self) -> Optional[Game]:
+ return self.game_model.game
+
def init_ui(self):
self.turnCounter = QTurnCounter()
@@ -33,10 +49,10 @@ class QTopPanel(QFrame):
self.passTurnButton.setProperty("style", "btn-primary")
self.passTurnButton.clicked.connect(self.passTurn)
- self.proceedButton = QPushButton("Mission Planning")
+ self.proceedButton = QPushButton("Take off")
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
- self.proceedButton.setProperty("style", "btn-success")
- self.proceedButton.clicked.connect(self.proceed)
+ self.proceedButton.setProperty("style", "start-button")
+ self.proceedButton.clicked.connect(self.launch_mission)
if self.game and self.game.turn == 0:
self.proceedButton.setEnabled(False)
@@ -60,6 +76,8 @@ class QTopPanel(QFrame):
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
+ self.proceedBoxLayout.addLayout(
+ MaxPlayerCount(self.game_model.ato_model))
self.proceedBoxLayout.addWidget(self.passTurnButton)
self.proceedBoxLayout.addWidget(self.proceedButton)
self.proceedBox.setLayout(self.proceedBoxLayout)
@@ -75,17 +93,18 @@ class QTopPanel(QFrame):
self.layout.setContentsMargins(0,0,0,0)
self.setLayout(self.layout)
- def setGame(self, game:Game):
- self.game = game
- if game is not None:
- self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
- self.budgetBox.setGame(self.game)
- self.factionsInfos.setGame(self.game)
+ def setGame(self, game: Optional[Game]):
+ if game is None:
+ return
- if self.game and self.game.turn == 0:
- self.proceedButton.setEnabled(False)
- else:
- self.proceedButton.setEnabled(True)
+ self.turnCounter.setCurrentTurn(game.turn, game.conditions)
+ self.budgetBox.setGame(game)
+ self.factionsInfos.setGame(game)
+
+ if game and game.turn == 0:
+ self.proceedButton.setEnabled(False)
+ else:
+ self.proceedButton.setEnabled(True)
def openSettings(self):
self.subwindow = QSettingsWindow(self.game)
@@ -100,9 +119,121 @@ class QTopPanel(QFrame):
GameUpdateSignal.get_instance().updateGame(self.game)
self.proceedButton.setEnabled(True)
- def proceed(self):
- self.subwindow = QMissionPlanning(self.game)
- self.subwindow.show()
+ def negative_start_packages(self) -> List[Package]:
+ packages = []
+ for package in self.game_model.ato_model.ato.packages:
+ if not package.flights:
+ continue
+ estimator = TotEstimator(package)
+ for flight in package.flights:
+ if estimator.mission_start_time(flight).total_seconds() < 0:
+ packages.append(package)
+ break
+ return packages
+
+ @staticmethod
+ def fix_tots(packages: List[Package]) -> None:
+ for package in packages:
+ estimator = TotEstimator(package)
+ package.time_over_target = estimator.earliest_tot()
+
+ def ato_has_clients(self) -> bool:
+ for package in self.game.blue_ato.packages:
+ for flight in package.flights:
+ if flight.client_count > 0:
+ return True
+ return False
+
+ def confirm_no_client_launch(self) -> bool:
+ result = QMessageBox.question(
+ self,
+ "Continue without client slots?",
+ ("No client slots have been created for players. Continuing will "
+ "allow the AI to perform the mission, but players will be unable "
+ "to participate.
"
+ "
"
+ "To add client slots for players, select a package from the "
+ "Packages panel on the left of the main window, and then a flight "
+ "from the Flights panel below the Packages panel. The edit button "
+ "below the Flights panel will allow you to edit the number of "
+ "client slots in the flight. Each client slot allows one player.
"
+ "
Click 'Yes' to continue with an AI only mission"
+ "
Click 'No' if you'd like to make more changes."),
+ QMessageBox.No,
+ QMessageBox.Yes
+ )
+ return result == QMessageBox.Yes
+
+ def confirm_negative_start_time(self,
+ negative_starts: List[Package]) -> bool:
+ formatted = '
'.join(
+ [f"{p.primary_task.name} {p.target.name}" for p in negative_starts]
+ )
+ mbox = QMessageBox(
+ QMessageBox.Question,
+ "Continue with past start times?",
+ ("Some flights in the following packages have start times set "
+ "earlier than mission start time:
"
+ "
"
+ f"{formatted}
"
+ "
"
+ "Flight start times are estimated based on the package TOT, so it "
+ "is possible that not all flights will be able to reach the "
+ "target area at their assigned times.
"
+ "
"
+ "You can either continue with the mission as planned, with the "
+ "misplanned flights potentially flying too fast and/or missing "
+ "their rendezvous; automatically fix negative TOTs; or cancel "
+ "mission start and fix the packages manually."),
+ parent=self
+ )
+ auto = mbox.addButton("Fix TOTs automatically", QMessageBox.ActionRole)
+ ignore = mbox.addButton("Continue without fixing",
+ QMessageBox.DestructiveRole)
+ cancel = mbox.addButton(QMessageBox.Cancel)
+ mbox.setEscapeButton(cancel)
+ mbox.exec_()
+ clicked = mbox.clickedButton()
+ if clicked == auto:
+ self.fix_tots(negative_starts)
+ return True
+ elif clicked == ignore:
+ return True
+ return False
+
+ def launch_mission(self):
+ """Finishes planning and waits for mission completion."""
+ if not self.ato_has_clients() and not self.confirm_no_client_launch():
+ return
+
+ negative_starts = self.negative_start_packages()
+ if negative_starts:
+ if not self.confirm_negative_start_time(negative_starts):
+ return
+
+ # TODO: Refactor this nonsense.
+ game_event = None
+ for event in self.game.events:
+ if isinstance(event,
+ FrontlineAttackEvent) and event.is_player_attacking:
+ game_event = event
+ if game_event is None:
+ game_event = FrontlineAttackEvent(
+ self.game,
+ self.game.theater.controlpoints[0],
+ self.game.theater.controlpoints[0],
+ self.game.theater.controlpoints[0].position,
+ self.game.player_name,
+ self.game.enemy_name)
+ game_event.is_awacs_enabled = True
+ game_event.ca_slots = 1
+ game_event.departure_cp = self.game.theater.controlpoints[0]
+ game_event.player_attacking({CAS: {}, CAP: {}})
+ game_event.depart_from = self.game.theater.controlpoints[0]
+
+ self.game.initiate_event(game_event)
+ waiting = QWaitingForMissionResultWindow(game_event, self.game)
+ waiting.show()
def budget_update(self, game:Game):
self.budgetBox.setGame(game)
diff --git a/qt_ui/widgets/QTurnCounter.py b/qt_ui/widgets/QTurnCounter.py
index f7e6fd88..a26112e1 100644
--- a/qt_ui/widgets/QTurnCounter.py
+++ b/qt_ui/widgets/QTurnCounter.py
@@ -1,7 +1,8 @@
import datetime
-from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox
+from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
+from game.weather import Conditions, TimeOfDay
import qt_ui.uiconstants as CONST
@@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
def __init__(self):
super(QTurnCounter, self).__init__("Turn")
- self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]]
-
- self.daytime_icon = QLabel()
- self.daytime_icon.setPixmap(self.icons[0])
- self.turn_info = QLabel()
+ self.icons = {
+ TimeOfDay.Dawn: CONST.ICONS["Dawn"],
+ TimeOfDay.Day: CONST.ICONS["Day"],
+ TimeOfDay.Dusk: CONST.ICONS["Dusk"],
+ TimeOfDay.Night: CONST.ICONS["Night"],
+ }
self.layout = QHBoxLayout()
- self.layout.addWidget(self.daytime_icon)
- self.layout.addWidget(self.turn_info)
self.setLayout(self.layout)
- def setCurrentTurn(self, turn: int, current_day: datetime):
+ self.daytime_icon = QLabel()
+ self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
+ self.layout.addWidget(self.daytime_icon)
+
+ self.time_column = QVBoxLayout()
+ self.layout.addLayout(self.time_column)
+
+ self.date_display = QLabel()
+ self.time_column.addWidget(self.date_display)
+
+ self.time_display = QLabel()
+ self.time_column.addWidget(self.time_display)
+
+ def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
+ """Sets the turn information display.
+
+ :arg turn Current turn number.
+ :arg conditions Current time and weather conditions.
"""
- Set the money amount to display
- :arg turn Current turn number
- :arg current_day Current day
- """
- self.daytime_icon.setPixmap(self.icons[turn % 4])
- self.turn_info.setText(current_day.strftime("%d %b %Y"))
+ self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
+ self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
+ self.time_display.setText(
+ conditions.start_time.strftime("%H:%M:%S Local"))
self.setTitle("Turn " + str(turn + 1))
diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py
new file mode 100644
index 00000000..295c4ee9
--- /dev/null
+++ b/qt_ui/widgets/ato.py
@@ -0,0 +1,541 @@
+"""Widgets for displaying air tasking orders."""
+import logging
+from contextlib import contextmanager
+from typing import ContextManager, Optional
+
+from PySide2.QtCore import (
+ QItemSelectionModel,
+ QModelIndex,
+ QSize,
+ Qt,
+)
+from PySide2.QtGui import (
+ QContextMenuEvent,
+ QFont,
+ QFontMetrics,
+ QIcon,
+ QPainter,
+)
+from PySide2.QtWidgets import (
+ QAbstractItemView,
+ QAction,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QListView,
+ QMenu,
+ QPushButton,
+ QSplitter,
+ QStyle,
+ QStyleOptionViewItem,
+ QStyledItemDelegate,
+ QVBoxLayout,
+)
+
+from game import db
+from gen.ato import Package
+from gen.flights.flight import Flight
+from gen.flights.traveltime import TotEstimator
+from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
+from ..models import AtoModel, GameModel, NullListModel, PackageModel
+
+
+class FlightDelegate(QStyledItemDelegate):
+ FONT_SIZE = 10
+ HMARGIN = 4
+ VMARGIN = 4
+
+ def __init__(self, package: Package) -> None:
+ super().__init__()
+ self.package = package
+
+ def get_font(self, option: QStyleOptionViewItem) -> QFont:
+ font = QFont(option.font)
+ font.setPointSize(self.FONT_SIZE)
+ return font
+
+ @staticmethod
+ def flight(index: QModelIndex) -> Flight:
+ return index.data(PackageModel.FlightRole)
+
+ def first_row_text(self, index: QModelIndex) -> str:
+ flight = self.flight(index)
+ task = flight.flight_type.name
+ count = flight.count
+ name = db.unit_type_name(flight.unit_type)
+ estimator = TotEstimator(self.package)
+ delay = estimator.mission_start_time(flight)
+ return f"[{task}] {count} x {name} in {delay}"
+
+ def second_row_text(self, index: QModelIndex) -> str:
+ flight = self.flight(index)
+ origin = flight.from_cp.name
+ return f"From {origin}"
+
+ def paint(self, painter: QPainter, option: QStyleOptionViewItem,
+ index: QModelIndex) -> None:
+ # Draw the list item with all the default selection styling, but with an
+ # invalid index so text formatting is left to us.
+ super().paint(painter, option, QModelIndex())
+
+ rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
+ -self.VMARGIN)
+
+ with painter_context(painter):
+ painter.setFont(self.get_font(option))
+
+ icon: Optional[QIcon] = index.data(Qt.DecorationRole)
+ if icon is not None:
+ icon.paint(painter, rect, Qt.AlignLeft | Qt.AlignVCenter,
+ self.icon_mode(option),
+ self.icon_state(option))
+
+ rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN,
+ 0, 0, 0)
+ painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index))
+ line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
+ painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index))
+
+ clients = self.num_clients(index)
+ if clients:
+ painter.drawText(rect, Qt.AlignRight,
+ f"Player Slots: {clients}")
+
+ def num_clients(self, index: QModelIndex) -> int:
+ flight = self.flight(index)
+ return flight.client_count
+
+ @staticmethod
+ def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
+ if not (option.state & QStyle.State_Enabled):
+ return QIcon.Disabled
+ elif option.state & QStyle.State_Selected:
+ return QIcon.Selected
+ elif option.state & QStyle.State_Active:
+ return QIcon.Active
+ return QIcon.Normal
+
+ @staticmethod
+ def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
+ return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
+
+ @staticmethod
+ def icon_size(option: QStyleOptionViewItem) -> QSize:
+ icon_size: Optional[QSize] = option.decorationSize
+ if icon_size is None:
+ return QSize(0, 0)
+ else:
+ return icon_size
+
+ def sizeHint(self, option: QStyleOptionViewItem,
+ index: QModelIndex) -> QSize:
+ left = self.icon_size(option).width() + self.HMARGIN
+ metrics = QFontMetrics(self.get_font(option))
+ first = metrics.size(0, self.first_row_text(index))
+ second = metrics.size(0, self.second_row_text(index))
+ text_width = max(first.width(), second.width())
+ return QSize(left + text_width + 2 * self.HMARGIN,
+ first.height() + second.height() + 2 * self.VMARGIN)
+
+
+class QFlightList(QListView):
+ """List view for displaying the flights of a package."""
+
+ def __init__(self, game_model: GameModel,
+ package_model: Optional[PackageModel]) -> None:
+ super().__init__()
+ self.game_model = game_model
+ self.package_model = package_model
+ self.set_package(package_model)
+ if package_model is not None:
+ self.setItemDelegate(FlightDelegate(package_model.package))
+ self.setIconSize(QSize(91, 24))
+ self.setSelectionBehavior(QAbstractItemView.SelectItems)
+ self.doubleClicked.connect(self.on_double_click)
+
+ def set_package(self, model: Optional[PackageModel]) -> None:
+ """Sets the package model to display."""
+ if model is None:
+ self.disconnect_model()
+ else:
+ self.package_model = model
+ self.setItemDelegate(FlightDelegate(model.package))
+ self.setModel(model)
+ # noinspection PyUnresolvedReferences
+ model.deleted.connect(self.disconnect_model)
+ self.selectionModel().setCurrentIndex(
+ model.index(0, 0, QModelIndex()),
+ QItemSelectionModel.Select
+ )
+
+ def disconnect_model(self) -> None:
+ """Clears the listview of any model attachments.
+
+ Displays an empty list until set_package is called with a valid model.
+ """
+ model = self.model()
+ if model is not None and isinstance(model, PackageModel):
+ model.deleted.disconnect(self.disconnect_model)
+ self.setModel(NullListModel())
+
+ @property
+ def selected_item(self) -> Optional[Flight]:
+ """Returns the selected flight, if any."""
+ index = self.currentIndex()
+ if not index.isValid():
+ return None
+ return self.package_model.flight_at_index(index)
+
+ def on_double_click(self, index: QModelIndex) -> None:
+ if not index.isValid():
+ return
+ self.edit_flight(index)
+
+ def edit_flight(self, index: QModelIndex) -> None:
+ from qt_ui.dialogs import Dialog
+ Dialog.open_edit_flight_dialog(
+ self.package_model, self.package_model.flight_at_index(index),
+ parent=self.window()
+ )
+
+ def delete_flight(self, index: QModelIndex) -> None:
+ self.game_model.game.aircraft_inventory.return_from_flight(
+ self.selected_item)
+ self.package_model.delete_flight_at_index(index)
+ GameUpdateSignal.get_instance().redraw_flight_paths()
+
+ def contextMenuEvent(self, event: QContextMenuEvent) -> None:
+ index = self.indexAt(event.pos())
+
+ menu = QMenu("Menu")
+
+ edit_action = QAction("Edit")
+ edit_action.triggered.connect(lambda: self.edit_flight(index))
+ menu.addAction(edit_action)
+
+ delete_action = QAction(f"Delete")
+ delete_action.triggered.connect(lambda: self.delete_flight(index))
+ menu.addAction(delete_action)
+
+ menu.exec_(event.globalPos())
+
+
+class QFlightPanel(QGroupBox):
+ """The flight display portion of the ATO panel.
+
+ Displays the flights assigned to the selected package, and includes edit and
+ delete buttons for flight management.
+ """
+
+ def __init__(self, game_model: GameModel,
+ package_model: Optional[PackageModel] = None) -> None:
+ super().__init__("Flights")
+ self.game_model = game_model
+ self.package_model = package_model
+
+ self.vbox = QVBoxLayout()
+ self.setLayout(self.vbox)
+
+ self.tip = QLabel(
+ "To add flights to a package, edit the package by double clicking "
+ "it or pressing the edit button."
+ )
+ self.vbox.addWidget(self.tip)
+
+ self.flight_list = QFlightList(game_model, package_model)
+ self.vbox.addWidget(self.flight_list)
+
+ self.button_row = QHBoxLayout()
+ self.vbox.addLayout(self.button_row)
+
+ self.edit_button = QPushButton("Edit")
+ self.edit_button.clicked.connect(self.on_edit)
+ self.button_row.addWidget(self.edit_button)
+
+ self.delete_button = QPushButton("Delete")
+ # noinspection PyTypeChecker
+ self.delete_button.setProperty("style", "btn-danger")
+ self.delete_button.clicked.connect(self.on_delete)
+ self.button_row.addWidget(self.delete_button)
+
+ self.selection_changed.connect(self.on_selection_changed)
+ self.on_selection_changed()
+
+ def set_package(self, model: Optional[PackageModel]) -> None:
+ """Sets the package model to display."""
+ self.package_model = model
+ self.flight_list.set_package(model)
+ self.selection_changed.connect(self.on_selection_changed)
+ self.on_selection_changed()
+
+ @property
+ def selection_changed(self):
+ """Returns the signal emitted when the flight selection changes."""
+ return self.flight_list.selectionModel().selectionChanged
+
+ def on_selection_changed(self) -> None:
+ """Updates the status of the edit and delete buttons."""
+ index = self.flight_list.currentIndex()
+ enabled = index.isValid()
+ self.edit_button.setEnabled(enabled)
+ self.delete_button.setEnabled(enabled)
+ self.change_map_flight_selection(index)
+
+ @staticmethod
+ def change_map_flight_selection(index: QModelIndex) -> None:
+ if not index.isValid():
+ GameUpdateSignal.get_instance().select_flight(None)
+ return
+
+ GameUpdateSignal.get_instance().select_flight(index.row())
+
+ def on_edit(self) -> None:
+ """Opens the flight edit dialog."""
+ index = self.flight_list.currentIndex()
+ if not index.isValid():
+ logging.error(f"Cannot edit flight when no flight is selected.")
+ return
+ self.flight_list.edit_flight(index)
+
+ def on_delete(self) -> None:
+ """Removes the selected flight from the package."""
+ index = self.flight_list.currentIndex()
+ if not index.isValid():
+ logging.error(f"Cannot delete flight when no flight is selected.")
+ return
+ self.flight_list.delete_flight(index)
+
+
+@contextmanager
+def painter_context(painter: QPainter) -> ContextManager[None]:
+ try:
+ painter.save()
+ yield
+ finally:
+ painter.restore()
+
+
+class PackageDelegate(QStyledItemDelegate):
+ FONT_SIZE = 12
+ HMARGIN = 4
+ VMARGIN = 4
+
+ def get_font(self, option: QStyleOptionViewItem) -> QFont:
+ font = QFont(option.font)
+ font.setPointSize(self.FONT_SIZE)
+ return font
+
+ @staticmethod
+ def package(index: QModelIndex) -> Package:
+ return index.data(AtoModel.PackageRole)
+
+ def left_text(self, index: QModelIndex) -> str:
+ package = self.package(index)
+ return f"{package.package_description} {package.target.name}"
+
+ def right_text(self, index: QModelIndex) -> str:
+ package = self.package(index)
+ return f"TOT T+{package.time_over_target}"
+
+ def paint(self, painter: QPainter, option: QStyleOptionViewItem,
+ index: QModelIndex) -> None:
+ # Draw the list item with all the default selection styling, but with an
+ # invalid index so text formatting is left to us.
+ super().paint(painter, option, QModelIndex())
+
+ rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
+ -self.VMARGIN)
+
+ with painter_context(painter):
+ painter.setFont(self.get_font(option))
+
+ painter.drawText(rect, Qt.AlignLeft, self.left_text(index))
+ line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
+ painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
+
+ clients = self.num_clients(index)
+ if clients:
+ painter.drawText(rect, Qt.AlignRight,
+ f"Player Slots: {clients}")
+
+ def num_clients(self, index: QModelIndex) -> int:
+ package = self.package(index)
+ return sum(f.client_count for f in package.flights)
+
+ def sizeHint(self, option: QStyleOptionViewItem,
+ index: QModelIndex) -> QSize:
+ metrics = QFontMetrics(self.get_font(option))
+ left = metrics.size(0, self.left_text(index))
+ right = metrics.size(0, self.right_text(index))
+ return QSize(max(left.width(), right.width()) + 2 * self.HMARGIN,
+ left.height() + right.height() + 2 * self.VMARGIN)
+
+
+class QPackageList(QListView):
+ """List view for displaying the packages of an ATO."""
+
+ def __init__(self, model: AtoModel) -> None:
+ super().__init__()
+ self.ato_model = model
+ self.setModel(model)
+ self.setItemDelegate(PackageDelegate())
+ self.setIconSize(QSize(91, 24))
+ self.setSelectionBehavior(QAbstractItemView.SelectItems)
+ self.model().rowsInserted.connect(self.on_new_packages)
+ self.doubleClicked.connect(self.on_double_click)
+
+ @property
+ def selected_item(self) -> Optional[Package]:
+ """Returns the selected package, if any."""
+ index = self.currentIndex()
+ if not index.isValid():
+ return None
+ return self.ato_model.package_at_index(index)
+
+ def edit_package(self, index: QModelIndex) -> None:
+ from qt_ui.dialogs import Dialog
+ Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index))
+
+ def delete_package(self, index: QModelIndex) -> None:
+ self.ato_model.delete_package_at_index(index)
+ GameUpdateSignal.get_instance().redraw_flight_paths()
+
+ def on_new_packages(self, _parent: QModelIndex, first: int,
+ _last: int) -> None:
+ # Select the newly created pacakges. This should only ever happen due to
+ # the player saving a new package, so selecting it helps them view/edit
+ # it faster.
+ self.selectionModel().setCurrentIndex(self.model().index(first, 0),
+ QItemSelectionModel.Select)
+
+ def on_double_click(self, index: QModelIndex) -> None:
+ if not index.isValid():
+ return
+ self.edit_package(index)
+
+ def contextMenuEvent(self, event: QContextMenuEvent) -> None:
+ index = self.indexAt(event.pos())
+
+ menu = QMenu("Menu")
+
+ edit_action = QAction("Edit")
+ edit_action.triggered.connect(lambda: self.edit_package(index))
+ menu.addAction(edit_action)
+
+ delete_action = QAction(f"Delete")
+ delete_action.triggered.connect(lambda: self.delete_package(index))
+ menu.addAction(delete_action)
+
+ menu.exec_(event.globalPos())
+
+
+class QPackagePanel(QGroupBox):
+ """The package display portion of the ATO panel.
+
+ Displays the package assigned to the player's ATO, and includes edit and
+ delete buttons for package management.
+ """
+
+ def __init__(self, model: AtoModel) -> None:
+ super().__init__("Packages")
+ self.ato_model = model
+ self.ato_model.layoutChanged.connect(self.on_current_changed)
+
+ self.vbox = QVBoxLayout()
+ self.setLayout(self.vbox)
+
+ self.tip = QLabel(
+ "To create a new package, right click the mission target on the "
+ "map. To target airbase objectives, use\n"
+ "the attack button in the airbase view."
+ )
+ self.vbox.addWidget(self.tip)
+
+ self.package_list = QPackageList(self.ato_model)
+ self.vbox.addWidget(self.package_list)
+
+ self.button_row = QHBoxLayout()
+ self.vbox.addLayout(self.button_row)
+
+ self.edit_button = QPushButton("Edit")
+ self.edit_button.clicked.connect(self.on_edit)
+ self.button_row.addWidget(self.edit_button)
+
+ self.delete_button = QPushButton("Delete")
+ # noinspection PyTypeChecker
+ self.delete_button.setProperty("style", "btn-danger")
+ self.delete_button.clicked.connect(self.on_delete)
+ self.button_row.addWidget(self.delete_button)
+
+ self.current_changed.connect(self.on_current_changed)
+ self.on_current_changed()
+
+ @property
+ def current_changed(self):
+ """Returns the signal emitted when the flight selection changes."""
+ return self.package_list.selectionModel().currentChanged
+
+ def on_current_changed(self) -> None:
+ """Updates the status of the edit and delete buttons."""
+ index = self.package_list.currentIndex()
+ enabled = index.isValid()
+ self.edit_button.setEnabled(enabled)
+ self.delete_button.setEnabled(enabled)
+ self.change_map_package_selection(index)
+
+ def change_map_package_selection(self, index: QModelIndex) -> None:
+ if not index.isValid():
+ GameUpdateSignal.get_instance().select_package(None)
+ return
+
+ package = self.ato_model.get_package_model(index)
+ if package.rowCount() == 0:
+ GameUpdateSignal.get_instance().select_package(None)
+ else:
+ GameUpdateSignal.get_instance().select_package(index.row())
+
+ def on_edit(self) -> None:
+ """Opens the package edit dialog."""
+ index = self.package_list.currentIndex()
+ if not index.isValid():
+ logging.error(f"Cannot edit package when no package is selected.")
+ return
+ self.package_list.edit_package(index)
+
+ def on_delete(self) -> None:
+ """Removes the package from the ATO."""
+ index = self.package_list.currentIndex()
+ if not index.isValid():
+ logging.error(f"Cannot delete package when no package is selected.")
+ return
+ self.package_list.delete_package(index)
+
+
+class QAirTaskingOrderPanel(QSplitter):
+ """A split panel for displaying the packages and flights of an ATO.
+
+ Used as the left-bar of the main UI. The top half of the panel displays the
+ packages of the player's ATO, and the bottom half displays the flights of
+ the selected package.
+ """
+
+ def __init__(self, game_model: GameModel) -> None:
+ super().__init__(Qt.Vertical)
+ self.ato_model = game_model.ato_model
+
+ self.package_panel = QPackagePanel(self.ato_model)
+ self.package_panel.current_changed.connect(self.on_package_change)
+ self.addWidget(self.package_panel)
+
+ self.flight_panel = QFlightPanel(game_model)
+ self.addWidget(self.flight_panel)
+
+ def on_package_change(self) -> None:
+ """Sets the newly selected flight for display in the bottom panel."""
+ index = self.package_panel.package_list.currentIndex()
+ if index.isValid():
+ self.flight_panel.set_package(
+ self.ato_model.get_package_model(index)
+ )
+ else:
+ self.flight_panel.set_package(None)
diff --git a/qt_ui/widgets/clientslots.py b/qt_ui/widgets/clientslots.py
new file mode 100644
index 00000000..1c9fff9b
--- /dev/null
+++ b/qt_ui/widgets/clientslots.py
@@ -0,0 +1,28 @@
+"""Widgets for displaying client slots."""
+from PySide2.QtWidgets import QLabel
+
+from qt_ui.models import AtoModel
+from qt_ui.widgets.QLabeledWidget import QLabeledWidget
+
+
+class MaxPlayerCount(QLabeledWidget):
+ def __init__(self, ato_model: AtoModel) -> None:
+ self.ato_model = ato_model
+ self.slots_label = QLabel(str(self.count_client_slots))
+ self.ato_model.client_slots_changed.connect(self.update_count)
+ super().__init__(
+ "Max Players:", self.slots_label,
+ ("Total number of client slots. To add client slots, edit a flight "
+ "using the panel on the left.")
+ )
+
+ @property
+ def count_client_slots(self) -> int:
+ slots = 0
+ for package in self.ato_model.packages:
+ for flight in package.flights:
+ slots += flight.client_count
+ return slots
+
+ def update_count(self) -> None:
+ self.slots_label.setText(str(self.count_client_slots))
diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py
new file mode 100644
index 00000000..1f490e4d
--- /dev/null
+++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py
@@ -0,0 +1,16 @@
+"""Combo box for selecting aircraft types."""
+from typing import Iterable
+
+from PySide2.QtWidgets import QComboBox
+
+from dcs.planes import PlaneType
+
+
+class QAircraftTypeSelector(QComboBox):
+ """Combo box for selecting among the given aircraft types."""
+
+ def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
+ super().__init__()
+ for aircraft in aircraft_types:
+ self.addItem(f"{aircraft.id}", userData=aircraft)
+ self.model().sort(0)
diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py
new file mode 100644
index 00000000..d1a27382
--- /dev/null
+++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py
@@ -0,0 +1,108 @@
+"""Combo box for selecting a flight's task type."""
+import logging
+from typing import Iterator
+
+from PySide2.QtWidgets import QComboBox
+
+from gen.flights.flight import FlightType
+from theater import (
+ ConflictTheater,
+ ControlPoint,
+ FrontLine,
+ MissionTarget,
+ TheaterGroundObject,
+)
+
+
+class QFlightTypeComboBox(QComboBox):
+ """Combo box for selecting a flight task type."""
+
+ COMMON_ENEMY_MISSIONS = [
+ FlightType.ESCORT,
+ FlightType.SEAD,
+ FlightType.DEAD,
+ # TODO: FlightType.ELINT,
+ # TODO: FlightType.EWAR,
+ # TODO: FlightType.RECON,
+ ]
+
+ COMMON_FRIENDLY_MISSIONS = [
+ FlightType.BARCAP,
+ ]
+
+ FRIENDLY_AIRBASE_MISSIONS = [
+ # TODO: FlightType.INTERCEPTION
+ # TODO: FlightType.LOGISTICS
+ ] + COMMON_FRIENDLY_MISSIONS
+
+ FRIENDLY_CARRIER_MISSIONS = [
+ # TODO: FlightType.INTERCEPTION
+ # TODO: Buddy tanking for the A-4?
+ # TODO: Rescue chopper?
+ # TODO: Inter-ship logistics?
+ ] + COMMON_FRIENDLY_MISSIONS
+
+ ENEMY_CARRIER_MISSIONS = [
+ FlightType.ESCORT,
+ FlightType.BARCAP,
+ # TODO: FlightType.ANTISHIP
+ ]
+
+ ENEMY_AIRBASE_MISSIONS = [
+ FlightType.BARCAP,
+ # TODO: FlightType.STRIKE
+ ] + COMMON_ENEMY_MISSIONS
+
+ FRIENDLY_GROUND_OBJECT_MISSIONS = [
+ # TODO: FlightType.LOGISTICS
+ # TODO: FlightType.TROOP_TRANSPORT
+ ] + COMMON_FRIENDLY_MISSIONS
+
+ ENEMY_GROUND_OBJECT_MISSIONS = [
+ FlightType.BARCAP,
+ FlightType.STRIKE,
+ ] + COMMON_ENEMY_MISSIONS
+
+ FRONT_LINE_MISSIONS = [
+ FlightType.CAS,
+ FlightType.TARCAP,
+ # TODO: FlightType.TROOP_TRANSPORT
+ # TODO: FlightType.EVAC
+ ] + COMMON_ENEMY_MISSIONS
+
+ # TODO: Add BAI missions after we have useful BAI targets.
+
+ def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None:
+ super().__init__()
+ self.theater = theater
+ self.target = target
+ for mission_type in self.mission_types_for_target():
+ self.addItem(mission_type.name, userData=mission_type)
+
+ def mission_types_for_target(self) -> Iterator[FlightType]:
+ if isinstance(self.target, ControlPoint):
+ friendly = self.target.captured
+ fleet = self.target.is_fleet
+ if friendly:
+ if fleet:
+ yield from self.FRIENDLY_CARRIER_MISSIONS
+ else:
+ yield from self.FRIENDLY_AIRBASE_MISSIONS
+ else:
+ if fleet:
+ yield from self.ENEMY_CARRIER_MISSIONS
+ else:
+ yield from self.ENEMY_AIRBASE_MISSIONS
+ elif isinstance(self.target, TheaterGroundObject):
+ # TODO: Filter more based on the category.
+ friendly = self.target.control_point.captured
+ if friendly:
+ yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
+ else:
+ yield from self.ENEMY_GROUND_OBJECT_MISSIONS
+ elif isinstance(self.target, FrontLine):
+ yield from self.FRONT_LINE_MISSIONS
+ else:
+ logging.error(
+ f"Unhandled target type: {self.target.__class__.__name__}"
+ )
diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
new file mode 100644
index 00000000..b0530efc
--- /dev/null
+++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
@@ -0,0 +1,49 @@
+"""Combo box for selecting a departure airfield."""
+from typing import Iterable
+
+from PySide2.QtWidgets import QComboBox
+
+from dcs.planes import PlaneType
+from game.inventory import GlobalAircraftInventory
+from theater.controlpoint import ControlPoint
+
+
+class QOriginAirfieldSelector(QComboBox):
+ """A combo box for selecting a flight's departure airfield.
+
+ The combo box will automatically be populated with all departure airfields
+ that have unassigned inventory of the given aircraft type.
+ """
+
+ def __init__(self, global_inventory: GlobalAircraftInventory,
+ origins: Iterable[ControlPoint],
+ aircraft: PlaneType) -> None:
+ super().__init__()
+ self.global_inventory = global_inventory
+ self.origins = list(origins)
+ self.aircraft = aircraft
+ self.rebuild_selector()
+
+ def change_aircraft(self, aircraft: PlaneType) -> None:
+ if self.aircraft == aircraft:
+ return
+ self.aircraft = aircraft
+ self.rebuild_selector()
+
+ def rebuild_selector(self) -> None:
+ self.clear()
+ for origin in self.origins:
+ inventory = self.global_inventory.for_control_point(origin)
+ available = inventory.available(self.aircraft)
+ if available:
+ self.addItem(f"{origin.name} ({available} available)", origin)
+ self.model().sort(0)
+ self.update()
+
+ @property
+ def available(self) -> int:
+ origin = self.currentData()
+ if origin is None:
+ return 0
+ inventory = self.global_inventory.for_control_point(origin)
+ return inventory.available(self.aircraft)
diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
index 079aab99..72ece41e 100644
--- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
+++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
@@ -1,9 +1,8 @@
-from PySide2.QtCore import QSortFilterProxyModel, Qt, QModelIndex
from PySide2.QtGui import QStandardItem, QStandardItemModel
-from PySide2.QtWidgets import QComboBox, QCompleter
+
from game import Game
-from gen import Conflict, FlightWaypointType
-from gen.flights.flight import FlightWaypoint, PredefinedWaypointCategory
+from gen import BuildingGroundObject, Conflict, FlightWaypointType
+from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
from theater import ControlPointType
@@ -66,15 +65,13 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.description = "Frontline"
- wpt.data = [cp, ecp]
- wpt.category = PredefinedWaypointCategory.FRONTLINE
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_targets:
for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
for ground_object in cp.ground_objects:
- if not ground_object.is_dead and not ground_object.dcs_identifier == "AA":
+ if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject):
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
ground_object.position.x,
@@ -86,13 +83,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)
- wpt.data = ground_object
if cp.captured:
wpt.description = "Friendly Building"
- wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
else:
wpt.description = "Enemy Building"
- wpt.category = PredefinedWaypointCategory.ENEMY_BUILDING
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_units:
@@ -112,15 +106,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
wpt.pretty_name = wpt.name
wpt.targets.append(u)
- wpt.data = u
wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly unit : " + u.type
- wpt.category = PredefinedWaypointCategory.ALLY_UNIT
else:
wpt.description = "Enemy unit : " + u.type
- wpt.category = PredefinedWaypointCategory.ENEMY_UNIT
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:
@@ -134,13 +125,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
)
wpt.alt_type = "RADIO"
wpt.name = cp.name
- wpt.data = cp
if cp.captured:
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
- wpt.category = PredefinedWaypointCategory.ALLY_CP
else:
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
- wpt.category = PredefinedWaypointCategory.ENEMY_CP
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"
diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py
new file mode 100644
index 00000000..f1425893
--- /dev/null
+++ b/qt_ui/widgets/map/QFrontLine.py
@@ -0,0 +1,82 @@
+"""Common base for objects drawn on the game map."""
+from typing import Optional
+
+from PySide2.QtCore import Qt
+from PySide2.QtGui import QPen
+from PySide2.QtWidgets import (
+ QAction,
+ QGraphicsLineItem,
+ QGraphicsSceneContextMenuEvent,
+ QGraphicsSceneHoverEvent,
+ QGraphicsSceneMouseEvent,
+ QMenu,
+)
+
+import qt_ui.uiconstants as const
+from qt_ui.dialogs import Dialog
+from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
+from theater.missiontarget import MissionTarget
+
+
+class QFrontLine(QGraphicsLineItem):
+ """Base class for objects drawn on the game map.
+
+ Game map objects have an on_click behavior that triggers on left click, and
+ change the mouse cursor on hover.
+ """
+
+ def __init__(self, x1: float, y1: float, x2: float, y2: float,
+ mission_target: MissionTarget) -> None:
+ super().__init__(x1, y1, x2, y2)
+ self.mission_target = mission_target
+ self.new_package_dialog: Optional[QNewPackageDialog] = None
+ self.setAcceptHoverEvents(True)
+
+ pen = QPen(brush=const.COLORS["bright_red"])
+ pen.setColor(const.COLORS["orange"])
+ pen.setWidth(8)
+ self.setPen(pen)
+
+ def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
+ self.setCursor(Qt.PointingHandCursor)
+
+ def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self.on_click()
+
+ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
+ menu = QMenu("Menu")
+
+ object_details_action = QAction(self.object_dialog_text)
+ object_details_action.triggered.connect(self.on_click)
+ menu.addAction(object_details_action)
+
+ new_package_action = QAction(f"New package")
+ new_package_action.triggered.connect(self.open_new_package_dialog)
+ menu.addAction(new_package_action)
+
+ menu.exec_(event.screenPos())
+
+ @property
+ def object_dialog_text(self) -> str:
+ """Text to for the object's dialog in the context menu.
+
+ Right clicking a map object will open a context menu and the first item
+ will open the details dialog for this object. This menu action has the
+ same behavior as the on_click event.
+
+ Return:
+ The text that should be displayed for the menu item.
+ """
+ return "Details"
+
+ def on_click(self) -> None:
+ """The action to take when this map object is left-clicked.
+
+ Typically this should open a details view of the object.
+ """
+ raise NotImplementedError
+
+ def open_new_package_dialog(self) -> None:
+ """Opens the dialog for planning a new mission package."""
+ Dialog.open_new_package_dialog(self.mission_target)
diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py
index 41194ca0..a24b609b 100644
--- a/qt_ui/widgets/map/QLiberationMap.py
+++ b/qt_ui/widgets/map/QLiberationMap.py
@@ -1,51 +1,117 @@
-import typing
-from typing import Dict
+from __future__ import annotations
-from PySide2 import QtCore
-from PySide2.QtCore import Qt, QRect, QPointF
-from PySide2.QtGui import QPixmap, QBrush, QColor, QWheelEvent, QPen, QFont
-from PySide2.QtWidgets import QGraphicsView, QFrame, QGraphicsOpacityEffect
+import datetime
+import logging
+from typing import List, Optional, Tuple
+
+from PySide2.QtCore import QPointF, Qt
+from PySide2.QtGui import (
+ QBrush,
+ QColor,
+ QFont,
+ QPen,
+ QPixmap,
+ QPolygonF,
+ QWheelEvent,
+)
+from PySide2.QtWidgets import (
+ QFrame,
+ QGraphicsItem,
+ QGraphicsOpacityEffect,
+ QGraphicsScene,
+ QGraphicsView,
+)
from dcs import Point
from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST
from game import Game, db
-from game.data.radar_db import UNITS_WITH_RADAR
-from game.event import UnitsDeliveryEvent, Event, ControlPointType
+from game.utils import meter_to_feet
+from game.weather import TimeOfDay
from gen import Conflict
+from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
+from gen.flights.flightplan import FlightPlan
+from qt_ui.displayoptions import DisplayOptions
+from qt_ui.models import GameModel
+from qt_ui.widgets.map.QFrontLine import QFrontLine
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
-from theater import ControlPoint
+from theater import ControlPoint, FrontLine
+from theater.theatergroundobject import (
+ EwrGroundObject,
+ MissileSiteGroundObject,
+ TheaterGroundObject,
+)
class QLiberationMap(QGraphicsView):
+ WAYPOINT_SIZE = 4
- instance = None
- display_rules: Dict[str, bool] = {
- "cp": True,
- "go": True,
- "lines": True,
- "events": True,
- "sam": True,
- "flight_paths": False
- }
+ instance: Optional[QLiberationMap] = None
- def __init__(self, game: Game):
+ def __init__(self, game_model: GameModel):
super(QLiberationMap, self).__init__()
QLiberationMap.instance = self
+ self.game_model = game_model
+ self.game: Optional[Game] = game_model.game
- self.frontline_vector_cache = {}
+ self.waypoint_info_font = QFont()
+ self.waypoint_info_font.setPointSize(12)
- self.setMinimumSize(800,600)
+ self.flight_path_items: List[QGraphicsItem] = []
+ # A tuple of (package index, flight index), or none.
+ self.selected_flight: Optional[Tuple[int, int]] = None
+
+ self.setMinimumSize(800, 600)
self.setMaximumHeight(2160)
self._zoom = 0
self.factor = 1
self.factorized = 1
self.init_scene()
- self.connectSignals()
- self.setGame(game)
+ self.setGame(game_model.game)
+
+ GameUpdateSignal.get_instance().flight_paths_changed.connect(
+ lambda: self.draw_flight_plans(self.scene())
+ )
+
+ def update_package_selection(index: int) -> None:
+ # Optional[int] isn't a valid type for a Qt signal. None will be
+ # converted to zero automatically. We use -1 to indicate no
+ # selection.
+ if index == -1:
+ self.selected_flight = None
+ else:
+ self.selected_flight = index, 0
+ self.draw_flight_plans(self.scene())
+
+ GameUpdateSignal.get_instance().package_selection_changed.connect(
+ update_package_selection
+ )
+
+ def update_flight_selection(index: int) -> None:
+ if self.selected_flight is None:
+ if index != -1:
+ # We don't know what order update_package_selection and
+ # update_flight_selection will be called in when the last
+ # package is removed. If no flight is selected, it's not a
+ # problem to also have no package selected.
+ logging.error(
+ "Flight was selected with no package selected")
+ return
+
+ # Optional[int] isn't a valid type for a Qt signal. None will be
+ # converted to zero automatically. We use -1 to indicate no
+ # selection.
+ if index == -1:
+ self.selected_flight = self.selected_flight[0], None
+ self.selected_flight = self.selected_flight[0], index
+ self.draw_flight_plans(self.scene())
+
+ GameUpdateSignal.get_instance().flight_selection_changed.connect(
+ update_flight_selection
+ )
def init_scene(self):
scene = QLiberationScene(self)
@@ -56,17 +122,12 @@ class QLiberationMap(QGraphicsView):
self.setFrameShape(QFrame.NoFrame)
self.setDragMode(QGraphicsView.ScrollHandDrag)
- def connectSignals(self):
- GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
-
- def setGame(self, game: Game):
+ def setGame(self, game: Optional[Game]):
self.game = game
- print("Reloading Map Canvas")
if self.game is not None:
+ logging.debug("Reloading Map Canvas")
self.reload_scene()
-
-
"""
Uncomment to set up theather reference points
@@ -105,6 +166,28 @@ class QLiberationMap(QGraphicsView):
self.reload_scene()
"""
+ @staticmethod
+ def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
+ detection_range = 0
+ threat_range = 0
+ for g in ground_object.groups:
+ for u in g.units:
+ unit = db.unit_type_from_name(u.type)
+ if unit is None:
+ logging.error(f"Unknown unit type {u.type}")
+ continue
+
+ # Some units in pydcs have detection_range and threat_range
+ # defined, but explicitly set to None.
+ unit_detection_range = getattr(unit, "detection_range", None)
+ if unit_detection_range is not None:
+ detection_range = max(detection_range, unit_detection_range)
+
+ unit_threat_range = getattr(unit, "threat_range", None)
+ if unit_threat_range is not None:
+ threat_range = max(threat_range, unit_threat_range)
+
+ return detection_range, threat_range
def reload_scene(self):
scene = self.scene()
@@ -120,12 +203,25 @@ class QLiberationMap(QGraphicsView):
# text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False))
# text.setPos(0, i * 24)
+ # Display Culling
+ if DisplayOptions.culling and self.game.settings.perf_culling:
+ culling_points = self.game_model.game.get_culling_points()
+ culling_distance = self.game_model.game.settings.perf_culling_distance
+ for point in culling_points:
+ culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000)
+ distance_point = self._transform_point(culling_distance_point)
+ transformed = self._transform_point(point)
+ diameter = distance_point[0] - transformed[0]
+ scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
+
for cp in self.game.theater.controlpoints:
pos = self._transform_point(cp.position)
- scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE,
- CONST.CP_SIZE, cp, self.game))
+ scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2,
+ pos[1] - CONST.CP_SIZE / 2,
+ CONST.CP_SIZE,
+ CONST.CP_SIZE, cp, self.game_model))
if cp.captured:
pen = QPen(brush=CONST.COLORS[playerColor])
@@ -139,67 +235,50 @@ class QLiberationMap(QGraphicsView):
if ground_object.obj_name in added_objects:
continue
-
go_pos = self._transform_point(ground_object.position)
if not ground_object.airbase_group:
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
- if ground_object.category == "aa" and self.get_display_rule("sam"):
- max_range = 0
- has_radar = False
- if ground_object.groups:
- for g in ground_object.groups:
- for u in g.units:
- unit = db.unit_type_from_name(u.type)
- if unit in UNITS_WITH_RADAR:
- has_radar = True
- if unit.threat_range > max_range:
- max_range = unit.threat_range
- if has_radar:
- scene.addEllipse(go_pos[0] - max_range/300.0 + 8, go_pos[1] - max_range/300.0 + 8, max_range/150.0, max_range/150.0, CONST.COLORS["white_transparent"], CONST.COLORS["grey_transparent"])
+ is_missile = isinstance(ground_object, MissileSiteGroundObject)
+ is_aa = ground_object.category == "aa" and not is_missile
+ is_ewr = isinstance(ground_object, EwrGroundObject)
+ is_display_type = is_aa or is_ewr
+ should_display = ((DisplayOptions.sam_ranges and cp.captured)
+ or
+ (DisplayOptions.enemy_sam_ranges and not cp.captured))
+
+ if is_display_type and should_display:
+ detection_range, threat_range = self.aa_ranges(
+ ground_object
+ )
+ if threat_range:
+ threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
+ ground_object.position.y+threat_range))
+ threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
+
+ # Add threat range circle
+ scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
+ threat_radius, threat_radius, self.threat_pen(cp.captured))
+ if detection_range:
+ # Add detection range circle
+ detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
+ ground_object.position.y+detection_range))
+ detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
+ if DisplayOptions.detection_range:
+ scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
+ detection_radius, detection_radius, self.detection_pen(cp.captured))
added_objects.append(ground_object.obj_name)
for cp in self.game.theater.enemy_points():
- if self.get_display_rule("lines"):
+ if DisplayOptions.lines:
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
for cp in self.game.theater.player_points():
- if self.get_display_rule("lines"):
+ if DisplayOptions.lines:
self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
- for cp in self.game.theater.controlpoints:
-
- if cp.captured:
- pen = QPen(brush=CONST.COLORS[playerColor])
- brush = CONST.COLORS[playerColor+"_transparent"]
-
- flight_path_pen = QPen(brush=CONST.COLORS[playerColor])
- flight_path_pen.setColor(CONST.COLORS[playerColor])
-
- else:
- pen = QPen(brush=CONST.COLORS[enemyColor])
- brush = CONST.COLORS[enemyColor+"_transparent"]
-
- flight_path_pen = QPen(brush=CONST.COLORS[enemyColor])
- flight_path_pen.setColor(CONST.COLORS[enemyColor])
-
- flight_path_pen.setWidth(1)
- flight_path_pen.setStyle(Qt.DashDotLine)
-
- pos = self._transform_point(cp.position)
- if self.get_display_rule("flight_paths"):
- if cp.id in self.game.planners.keys():
- planner = self.game.planners[cp.id]
- for flight in planner.flights:
- scene.addEllipse(pos[0], pos[1], 4, 4)
- prev_pos = list(pos)
- for point in flight.points:
- new_pos = self._transform_point(Point(point.x, point.y))
- scene.addLine(prev_pos[0]+2, prev_pos[1]+2, new_pos[0]+2, new_pos[1]+2, flight_path_pen)
- scene.addEllipse(new_pos[0], new_pos[1], 4, 4, pen, brush)
- prev_pos = list(new_pos)
- scene.addLine(prev_pos[0] + 2, prev_pos[1] + 2, pos[0] + 2, pos[1] + 2, flight_path_pen)
+ self.draw_flight_plans(scene)
for cp in self.game.theater.controlpoints:
pos = self._transform_point(cp.position)
@@ -209,6 +288,117 @@ class QLiberationMap(QGraphicsView):
text.setDefaultTextColor(Qt.white)
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
+ def clear_flight_paths(self, scene: QGraphicsScene) -> None:
+ for item in self.flight_path_items:
+ try:
+ scene.removeItem(item)
+ except RuntimeError:
+ # Something may have caused those items to already be removed.
+ pass
+ self.flight_path_items.clear()
+
+ def draw_flight_plans(self, scene: QGraphicsScene) -> None:
+ self.clear_flight_paths(scene)
+ if DisplayOptions.flight_paths.hide:
+ return
+ packages = list(self.game_model.ato_model.packages)
+ if self.game.settings.show_red_ato:
+ packages.extend(self.game_model.red_ato_model.packages)
+ for p_idx, package_model in enumerate(packages):
+ for f_idx, flight in enumerate(package_model.flights):
+ if self.selected_flight is None:
+ selected = False
+ else:
+ selected = (p_idx, f_idx) == self.selected_flight
+ if DisplayOptions.flight_paths.only_selected and not selected:
+ continue
+ self.draw_flight_plan(scene, flight, selected)
+
+ def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
+ selected: bool) -> None:
+ is_player = flight.from_cp.captured
+ pos = self._transform_point(flight.from_cp.position)
+
+ self.draw_waypoint(scene, pos, is_player, selected)
+ prev_pos = tuple(pos)
+ drew_target = False
+ target_types = (
+ FlightWaypointType.TARGET_GROUP_LOC,
+ FlightWaypointType.TARGET_POINT,
+ FlightWaypointType.TARGET_SHIP,
+ )
+ for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
+ new_pos = self._transform_point(Point(point.x, point.y))
+ self.draw_flight_path(scene, prev_pos, new_pos, is_player,
+ selected)
+ self.draw_waypoint(scene, new_pos, is_player, selected)
+ if selected and DisplayOptions.waypoint_info:
+ if point.waypoint_type in target_types:
+ if drew_target:
+ # Don't draw dozens of targets over each other.
+ continue
+ drew_target = True
+ self.draw_waypoint_info(scene, idx + 1, point, new_pos,
+ flight.flight_plan)
+ prev_pos = tuple(new_pos)
+ self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
+
+ def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
+ player: bool, selected: bool) -> None:
+ waypoint_pen = self.waypoint_pen(player, selected)
+ waypoint_brush = self.waypoint_brush(player, selected)
+ self.flight_path_items.append(scene.addEllipse(
+ position[0], position[1], self.WAYPOINT_SIZE,
+ self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
+ ))
+
+ def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
+ waypoint: FlightWaypoint, position: Tuple[int, int],
+ flight_plan: FlightPlan) -> None:
+
+ altitude = meter_to_feet(waypoint.alt)
+ altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
+
+ prefix = "TOT"
+ time = flight_plan.tot_for_waypoint(waypoint)
+ if time is None:
+ prefix = "Depart"
+ time = flight_plan.depart_time_for_waypoint(waypoint)
+ if time is None:
+ tot = ""
+ else:
+ time = datetime.timedelta(seconds=int(time.total_seconds()))
+ tot = f"{prefix} T+{time}"
+
+ pen = QPen(QColor("black"), 0.3)
+ brush = QColor("white")
+
+ text = "\n".join([
+ f"{number} {waypoint.name}",
+ f"{altitude} ft {altitude_type}",
+ tot,
+ ])
+
+ item = scene.addSimpleText(text, self.waypoint_info_font)
+ item.setFlag(QGraphicsItem.ItemIgnoresTransformations)
+ item.setBrush(brush)
+ item.setPen(pen)
+ item.moveBy(position[0] + 8, position[1])
+ item.setZValue(2)
+ self.flight_path_items.append(item)
+
+ def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
+ pos1: Tuple[int, int], player: bool,
+ selected: bool) -> None:
+ flight_path_pen = self.flight_path_pen(player, selected)
+ # Draw the line to the *middle* of the waypoint.
+ offset = self.WAYPOINT_SIZE // 2
+ self.flight_path_items.append(scene.addLine(
+ pos0[0] + offset, pos0[1] + offset,
+ pos1[0] + offset, pos1[1] + offset,
+ flight_path_pen
+ ))
+
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
scene = self.scene()
pos = self._transform_point(cp.position)
@@ -234,31 +424,12 @@ class QLiberationMap(QGraphicsView):
p1 = point_from_heading(pos2[0], pos2[1], h+180, 25)
p2 = point_from_heading(pos2[0], pos2[1], h, 25)
- frontline_pen = QPen(brush=CONST.COLORS["bright_red"])
- frontline_pen.setColor(CONST.COLORS["orange"])
- frontline_pen.setWidth(8)
- scene.addLine(p1[0], p1[1], p2[0], p2[1], pen=frontline_pen)
+ scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1],
+ FrontLine(cp, connected_cp)))
else:
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
- def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint):
- # Cache mechanism to avoid performing frontline vector computation on every frame
- key = str(from_cp.id) + "_" + str(to_cp.id)
- if key in self.frontline_vector_cache:
- return self.frontline_vector_cache[key]
- else:
- frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater)
- self.frontline_vector_cache[key] = frontline
- return frontline
-
- def _frontline_center(self, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[Point]:
- frontline_vector = self._frontline_vector(from_cp, to_cp)
- if frontline_vector:
- return frontline_vector[0].point_from_heading(frontline_vector[1], frontline_vector[2]/2)
- else:
- return None
-
def wheelEvent(self, event: QWheelEvent):
if event.angleDelta().y() > 0:
@@ -308,41 +479,98 @@ class QLiberationMap(QGraphicsView):
return X > treshold and X or treshold, Y > treshold and Y or treshold
+ def highlight_color(self, transparent: Optional[bool] = False) -> QColor:
+ return QColor(255, 255, 0, 20 if transparent else 255)
+
+ def base_faction_color_name(self, player: bool) -> str:
+ if player:
+ return self.game.get_player_color()
+ else:
+ return self.game.get_enemy_color()
+
+ def waypoint_pen(self, player: bool, selected: bool) -> QColor:
+ if selected and DisplayOptions.flight_paths.all:
+ return self.highlight_color()
+ name = self.base_faction_color_name(player)
+ return CONST.COLORS[name]
+
+ def waypoint_brush(self, player: bool, selected: bool) -> QColor:
+ if selected and DisplayOptions.flight_paths.all:
+ return self.highlight_color(transparent=True)
+ name = self.base_faction_color_name(player)
+ return CONST.COLORS[f"{name}_transparent"]
+
+ def threat_pen(self, player: bool) -> QPen:
+ if player:
+ color = "blue"
+ else:
+ color = "red"
+ qpen = QPen(CONST.COLORS[color])
+ return qpen
+
+ def detection_pen(self, player: bool) -> QPen:
+ if player:
+ color = "purple"
+ else:
+ color = "yellow"
+ qpen = QPen(CONST.COLORS[color])
+ qpen.setStyle(Qt.DotLine)
+ return qpen
+
+ def flight_path_pen(self, player: bool, selected: bool) -> QPen:
+ if selected and DisplayOptions.flight_paths.all:
+ return self.highlight_color()
+
+ name = self.base_faction_color_name(player)
+ color = CONST.COLORS[name]
+ pen = QPen(brush=color)
+ pen.setColor(color)
+ pen.setWidth(1)
+ pen.setStyle(Qt.DashDotLine)
+ return pen
+
def addBackground(self):
scene = self.scene()
- bg = QPixmap("./resources/" + self.game.theater.overview_image)
- scene.addPixmap(bg)
+ if not DisplayOptions.map_poly:
+ bg = QPixmap("./resources/" + self.game.theater.overview_image)
+ scene.addPixmap(bg)
+
+ # Apply graphical effects to simulate current daytime
+ if self.game.current_turn_time_of_day == TimeOfDay.Day:
+ pass
+ elif self.game.current_turn_time_of_day == TimeOfDay.Night:
+ ov = QPixmap(bg.width(), bg.height())
+ ov.fill(CONST.COLORS["night_overlay"])
+ overlay = scene.addPixmap(ov)
+ effect = QGraphicsOpacityEffect()
+ effect.setOpacity(0.7)
+ overlay.setGraphicsEffect(effect)
+ else:
+ ov = QPixmap(bg.width(), bg.height())
+ ov.fill(CONST.COLORS["dawn_dust_overlay"])
+ overlay = scene.addPixmap(ov)
+ effect = QGraphicsOpacityEffect()
+ effect.setOpacity(0.3)
+ overlay.setGraphicsEffect(effect)
- # Apply graphical effects to simulate current daytime
- if self.game.current_turn_daytime == "day":
- pass
- elif self.game.current_turn_daytime == "night":
- ov = QPixmap(bg.width(), bg.height())
- ov.fill(CONST.COLORS["night_overlay"])
- overlay = scene.addPixmap(ov)
- effect = QGraphicsOpacityEffect()
- effect.setOpacity(0.7)
- overlay.setGraphicsEffect(effect)
else:
- ov = QPixmap(bg.width(), bg.height())
- ov.fill(CONST.COLORS["dawn_dust_overlay"])
- overlay = scene.addPixmap(ov)
- effect = QGraphicsOpacityEffect()
- effect.setOpacity(0.3)
- overlay.setGraphicsEffect(effect)
+ # Polygon display mode
+ if self.game.theater.landmap is not None:
+
+ for sea_zone in self.game.theater.landmap[2]:
+ print(sea_zone)
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone])
+ scene.addPolygon(poly, CONST.COLORS["sea_blue"], CONST.COLORS["sea_blue"])
+
+ for inclusion_zone in self.game.theater.landmap[0]:
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone])
+ scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
+
+ for exclusion_zone in self.game.theater.landmap[1]:
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone])
+ scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])
- @staticmethod
- def set_display_rule(rule: str, value: bool):
- QLiberationMap.display_rules[rule] = value
- QLiberationMap.instance.reload_scene()
- QLiberationMap.instance.update()
- @staticmethod
- def get_display_rules() -> Dict[str, bool]:
- return QLiberationMap.display_rules
- @staticmethod
- def get_display_rule(rule) -> bool:
- return QLiberationMap.display_rules[rule]
diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py
index 09061e16..e59cdbfb 100644
--- a/qt_ui/widgets/map/QMapControlPoint.py
+++ b/qt_ui/widgets/map/QMapControlPoint.py
@@ -1,100 +1,99 @@
-from PySide2.QtCore import QRect, Qt
+from typing import Optional
+
from PySide2.QtGui import QColor, QPainter
-from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsSceneHoverEvent, QGraphicsSceneContextMenuEvent, QMenu, \
- QAction, QGraphicsSceneMouseEvent
+from PySide2.QtWidgets import QAction, QMenu
-import qt_ui.uiconstants as CONST
-from game import Game
+import qt_ui.uiconstants as const
+from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
-from theater import ControlPoint, db
+from theater import ControlPoint
+from .QMapObject import QMapObject
+from ...displayoptions import DisplayOptions
+from ...windows.GameUpdateSignal import GameUpdateSignal
-class QMapControlPoint(QGraphicsRectItem):
-
- def __init__(self, parent, x: float, y: float, w: float, h: float, model: ControlPoint, game: Game):
- super(QMapControlPoint, self).__init__(x, y, w, h)
- self.model = model
- self.game = game
+class QMapControlPoint(QMapObject):
+ def __init__(self, parent, x: float, y: float, w: float, h: float,
+ control_point: ControlPoint, game_model: GameModel) -> None:
+ super().__init__(x, y, w, h, mission_target=control_point)
+ self.game_model = game_model
+ self.control_point = control_point
self.parent = parent
- self.setAcceptHoverEvents(True)
self.setZValue(1)
- self.setToolTip(self.model.name)
+ self.setToolTip(self.control_point.name)
+ self.base_details_dialog: Optional[QBaseMenu2] = None
+ self.capture_action = QAction(
+ f"CHEAT: Capture {self.control_point.name}")
+ self.capture_action.triggered.connect(self.cheat_capture)
-
- def paint(self, painter, option, widget=None):
- #super(QMapControlPoint, self).paint(painter, option, widget)
-
- if self.parent.get_display_rule("cp"):
+ def paint(self, painter, option, widget=None) -> None:
+ if DisplayOptions.control_points:
painter.save()
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(self.brush_color)
painter.setPen(self.pen_color)
- if self.model.has_runway():
+ if self.control_point.has_runway():
if self.isUnderMouse():
- painter.setBrush(CONST.COLORS["white"])
+ painter.setBrush(const.COLORS["white"])
painter.setPen(self.pen_color)
r = option.rect
painter.drawEllipse(r.x(), r.y(), r.width(), r.height())
-
- #gauge = QRect(r.x(),
- # r.y()+CONST.CP_SIZE/2 + 2,
- # r.width(),
- # CONST.CP_SIZE / 4)
-
- #painter.setBrush(CONST.COLORS["bright_red"])
- #painter.setPen(CONST.COLORS["black"])
- #painter.drawRect(gauge)
-
- #gauge2 = QRect(r.x(),
- # r.y() + CONST.CP_SIZE / 2 + 2,
- # r.width()*self.model.base.strength,
- # CONST.CP_SIZE / 4)
-
- #painter.setBrush(CONST.COLORS["green"])
- #painter.drawRect(gauge2)
- else:
- # TODO : not drawing sunk carriers. Can be improved to display sunk carrier.
- pass
+ # TODO: Draw sunk carriers differently.
+ # Either don't draw them at all, or perhaps use a sunk ship icon.
painter.restore()
- def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
- self.update()
- self.setCursor(Qt.PointingHandCursor)
-
- def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent):
- self.update()
-
- def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
- self.update()
-
- def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
- self.openBaseMenu()
- #self.contextMenuEvent(event)
-
- def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
-
- if self.model.captured:
- openBaseMenu = QAction("Open base menu")
- else:
- openBaseMenu = QAction("Open intel menu")
-
- openBaseMenu.triggered.connect(self.openBaseMenu)
-
- menu = QMenu("Menu", self.parent)
- menu.addAction(openBaseMenu)
- menu.exec_(event.screenPos())
-
-
@property
- def brush_color(self)->QColor:
- return self.model.captured and CONST.COLORS["blue"] or CONST.COLORS["super_red"]
+ def brush_color(self) -> QColor:
+ if self.control_point.captured:
+ return const.COLORS["blue"]
+ else:
+ return const.COLORS["super_red"]
@property
def pen_color(self) -> QColor:
- return self.model.captured and CONST.COLORS["white"] or CONST.COLORS["white"]
+ return const.COLORS["white"]
- def openBaseMenu(self):
- self.baseMenu = QBaseMenu2(self.window(), self.model, self.game)
- self.baseMenu.show()
\ No newline at end of file
+ @property
+ def object_dialog_text(self) -> str:
+ if self.control_point.captured:
+ return "Open base menu"
+ else:
+ return "Open intel menu"
+
+ def on_click(self) -> None:
+ self.base_details_dialog = QBaseMenu2(
+ self.window(),
+ self.control_point,
+ self.game_model
+ )
+ self.base_details_dialog.show()
+
+ def add_context_menu_actions(self, menu: QMenu) -> None:
+ if self.control_point.is_fleet:
+ return
+
+ if self.control_point.captured:
+ return
+
+ for connected in self.control_point.connected_points:
+ if connected.captured:
+ break
+ else:
+ return
+
+ menu.addAction(self.capture_action)
+
+ def cheat_capture(self) -> None:
+ self.control_point.capture(self.game_model.game, for_player=True)
+ # Reinitialized ground planners and the like.
+ self.game_model.game.initialize_turn()
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+
+ def open_new_package_dialog(self) -> None:
+ """Extends the default packagedialog to redirect to base menu for red air base."""
+ if not self.control_point.captured:
+ self.on_click()
+ else:
+ super(QMapControlPoint, self).open_new_package_dialog()
diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py
index a79ce1ab..a7d857f3 100644
--- a/qt_ui/widgets/map/QMapGroundObject.py
+++ b/qt_ui/widgets/map/QMapGroundObject.py
@@ -1,86 +1,102 @@
-from PySide2.QtCore import QPoint, QRect, QPointF, Qt
-from PySide2.QtGui import QPainter, QBrush
-from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsItem, QGraphicsSceneHoverEvent, QGraphicsSceneMouseEvent
+from typing import List, Optional
-import qt_ui.uiconstants as CONST
-from game import db, Game
+from PySide2.QtCore import QRect
+from PySide2.QtGui import QBrush
+from PySide2.QtWidgets import QGraphicsItem
+
+import qt_ui.uiconstants as const
+from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
+from game.db import REWARDS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
-from theater import TheaterGroundObject, ControlPoint
+from theater import ControlPoint, TheaterGroundObject
+from .QMapObject import QMapObject
+from ...displayoptions import DisplayOptions
-class QMapGroundObject(QGraphicsRectItem):
-
- def __init__(self, parent, x: float, y: float, w: float, h: float, cp: ControlPoint, model: TheaterGroundObject, game:Game, buildings=[]):
- super(QMapGroundObject, self).__init__(x, y, w, h)
- self.model = model
- self.cp = cp
+class QMapGroundObject(QMapObject):
+ def __init__(self, parent, x: float, y: float, w: float, h: float,
+ control_point: ControlPoint,
+ ground_object: TheaterGroundObject, game: Game,
+ buildings: Optional[List[TheaterGroundObject]] = None) -> None:
+ super().__init__(x, y, w, h, mission_target=ground_object)
+ self.ground_object = ground_object
+ self.control_point = control_point
self.parent = parent
self.game = game
- self.setAcceptHoverEvents(True)
self.setZValue(2)
- self.buildings = buildings
+ self.buildings = buildings if buildings is not None else []
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
+ self.ground_object_dialog: Optional[QGroundObjectMenu] = None
+ self.setToolTip(self.tooltip)
- if len(self.model.groups) > 0:
+ @property
+ def tooltip(self) -> str:
+ lines = [
+ f"[{self.ground_object.obj_name}]",
+ f"${self.production_per_turn} per turn",
+ ]
+ if self.ground_object.groups:
units = {}
- for g in self.model.groups:
- print(g)
+ for g in self.ground_object.groups:
for u in g.units:
- if u.type in units.keys():
+ if u.type in units:
units[u.type] = units[u.type]+1
else:
units[u.type] = 1
- tooltip = "[" + self.model.obj_name + "]" + "\n"
+
for unit in units.keys():
- tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n"
- self.setToolTip(tooltip[:-1])
+ lines.append(f"{unit} x {units[unit]}")
else:
- tooltip = "[" + self.model.obj_name + "]" + "\n"
- for building in buildings:
+ for building in self.buildings:
if not building.is_dead:
- tooltip = tooltip + str(building.dcs_identifier) + "\n"
- self.setToolTip(tooltip[:-1])
+ lines.append(f"{building.dcs_identifier}")
- def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
- self.openEditionMenu()
+ return "\n".join(lines)
- def paint(self, painter, option, widget=None):
- #super(QMapControlPoint, self).paint(painter, option, widget)
+ @property
+ def production_per_turn(self) -> int:
+ production = 0
+ for g in self.control_point.ground_objects:
+ if g.category in REWARDS.keys():
+ production += REWARDS[g.category]
+ return production
- playerIcons = "_blue"
- enemyIcons = ""
+ def paint(self, painter, option, widget=None) -> None:
+ player_icons = "_blue"
+ enemy_icons = ""
- if self.parent.get_display_rule("go"):
+ if DisplayOptions.ground_objects:
painter.save()
- cat = self.model.category
- if cat == "aa" and self.model.sea_object:
+ cat = self.ground_object.category
+ if cat == "aa" and self.ground_object.sea_object:
cat = "ship"
- rect = QRect(option.rect.x()+2,option.rect.y(),option.rect.width()-2,option.rect.height())
+ rect = QRect(option.rect.x() + 2, option.rect.y(),
+ option.rect.width() - 2, option.rect.height())
- is_dead = self.model.is_dead
+ is_dead = self.ground_object.is_dead
for building in self.buildings:
if not building.is_dead:
is_dead = False
break
- if not is_dead and not self.cp.captured:
- painter.drawPixmap(rect, CONST.ICONS[cat + enemyIcons])
+ if not is_dead and not self.control_point.captured:
+ painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
elif not is_dead:
- painter.drawPixmap(rect, CONST.ICONS[cat + playerIcons])
+ painter.drawPixmap(rect, const.ICONS[cat + player_icons])
else:
- painter.drawPixmap(rect, CONST.ICONS["destroyed"])
+ painter.drawPixmap(rect, const.ICONS["destroyed"])
- self.drawHealthGauge(painter, option)
+ self.draw_health_gauge(painter, option)
painter.restore()
- def drawHealthGauge(self, painter, option):
+ def draw_health_gauge(self, painter, option) -> None:
units_alive = 0
units_dead = 0
- if len(self.model.groups) == 0:
+ if len(self.ground_object.groups) == 0:
for building in self.buildings:
if building.dcs_identifier in FORTIFICATION_BUILDINGS:
continue
@@ -89,7 +105,7 @@ class QMapGroundObject(QGraphicsRectItem):
else:
units_alive += 1
- for g in self.model.groups:
+ for g in self.ground_object.groups:
units_alive += len(g.units)
if hasattr(g, "units_losts"):
units_dead += len(g.units_losts)
@@ -97,22 +113,18 @@ class QMapGroundObject(QGraphicsRectItem):
if units_dead + units_alive > 0:
ratio = float(units_alive)/(float(units_dead) + float(units_alive))
bar_height = ratio * option.rect.height()
- painter.fillRect(option.rect.x(), option.rect.y(), 2, option.rect.height(), QBrush(CONST.COLORS["dark_red"]))
- painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, QBrush(CONST.COLORS["green"]))
-
-
- def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
- self.update()
- self.setCursor(Qt.PointingHandCursor)
-
- def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent):
- self.update()
- self.setCursor(Qt.PointingHandCursor)
-
- def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
- self.update()
-
- def openEditionMenu(self):
- self.editionMenu = QGroundObjectMenu(self.window(), self.model, self.buildings, self.cp, self.game)
- self.editionMenu.show()
+ painter.fillRect(option.rect.x(), option.rect.y(), 2,
+ option.rect.height(),
+ QBrush(const.COLORS["dark_red"]))
+ painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height,
+ QBrush(const.COLORS["green"]))
+ def on_click(self) -> None:
+ self.ground_object_dialog = QGroundObjectMenu(
+ self.window(),
+ self.ground_object,
+ self.buildings,
+ self.control_point,
+ self.game
+ )
+ self.ground_object_dialog.show()
diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py
new file mode 100644
index 00000000..fa28c333
--- /dev/null
+++ b/qt_ui/widgets/map/QMapObject.py
@@ -0,0 +1,80 @@
+"""Common base for objects drawn on the game map."""
+from typing import Optional
+
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import (
+ QAction,
+ QGraphicsRectItem,
+ QGraphicsSceneContextMenuEvent,
+ QGraphicsSceneHoverEvent,
+ QGraphicsSceneMouseEvent,
+ QMenu,
+)
+
+from qt_ui.dialogs import Dialog
+from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
+from theater.missiontarget import MissionTarget
+
+
+class QMapObject(QGraphicsRectItem):
+ """Base class for objects drawn on the game map.
+
+ Game map objects have an on_click behavior that triggers on left click, and
+ change the mouse cursor on hover.
+ """
+
+ def __init__(self, x: float, y: float, w: float, h: float,
+ mission_target: MissionTarget) -> None:
+ super().__init__(x, y, w, h)
+ self.mission_target = mission_target
+ self.new_package_dialog: Optional[QNewPackageDialog] = None
+ self.setAcceptHoverEvents(True)
+
+ def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
+ self.setCursor(Qt.PointingHandCursor)
+
+ def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
+ if event.button() == Qt.LeftButton:
+ self.on_click()
+
+ def add_context_menu_actions(self, menu: QMenu) -> None:
+ pass
+
+ def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
+ menu = QMenu("Menu", self.parent)
+
+ object_details_action = QAction(self.object_dialog_text)
+ object_details_action.triggered.connect(self.on_click)
+ menu.addAction(object_details_action)
+
+ new_package_action = QAction(f"New package")
+ new_package_action.triggered.connect(self.open_new_package_dialog)
+ menu.addAction(new_package_action)
+
+ self.add_context_menu_actions(menu)
+
+ menu.exec_(event.screenPos())
+
+ @property
+ def object_dialog_text(self) -> str:
+ """Text to for the object's dialog in the context menu.
+
+ Right clicking a map object will open a context menu and the first item
+ will open the details dialog for this object. This menu action has the
+ same behavior as the on_click event.
+
+ Return:
+ The text that should be displayed for the menu item.
+ """
+ return "Details"
+
+ def on_click(self) -> None:
+ """The action to take when this map object is left-clicked.
+
+ Typically this should open a details view of the object.
+ """
+ raise NotImplementedError
+
+ def open_new_package_dialog(self) -> None:
+ """Opens the dialog for planning a new mission package."""
+ Dialog.open_new_package_dialog(self.mission_target)
diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py
index dd32dd58..529a7498 100644
--- a/qt_ui/windows/GameUpdateSignal.py
+++ b/qt_ui/windows/GameUpdateSignal.py
@@ -1,3 +1,7 @@
+from __future__ import annotations
+
+from typing import Optional, Tuple
+
from PySide2.QtCore import QObject, Signal
from game import Game
@@ -19,21 +23,41 @@ class GameUpdateSignal(QObject):
budgetupdated = Signal(Game)
debriefingReceived = Signal(DebriefingSignal)
+ flight_paths_changed = Signal()
+ package_selection_changed = Signal(int) # -1 indicates no selection.
+ flight_selection_changed = Signal(int) # -1 indicates no selection.
+
def __init__(self):
super(GameUpdateSignal, self).__init__()
GameUpdateSignal.instance = self
- def updateGame(self, game: Game):
+ def select_package(self, index: Optional[int]) -> None:
+ # noinspection PyUnresolvedReferences
+ self.package_selection_changed.emit(-1 if index is None else index)
+
+ def select_flight(self, index: Optional[int]) -> None:
+ # noinspection PyUnresolvedReferences
+ self.flight_selection_changed.emit(-1 if index is None else index)
+
+ def redraw_flight_paths(self) -> None:
+ # noinspection PyUnresolvedReferences
+ self.flight_paths_changed.emit()
+
+ def updateGame(self, game: Optional[Game]):
+ # noinspection PyUnresolvedReferences
self.gameupdated.emit(game)
def updateBudget(self, game: Game):
+ # noinspection PyUnresolvedReferences
self.budgetupdated.emit(game)
def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
sig = DebriefingSignal(game, gameEvent, debriefing)
+ # noinspection PyUnresolvedReferences
self.gameupdated.emit(game)
+ # noinspection PyUnresolvedReferences
self.debriefingReceived.emit(sig)
@staticmethod
- def get_instance():
+ def get_instance() -> GameUpdateSignal:
return GameUpdateSignal.instance
diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py
index 752d19d5..e0ecce57 100644
--- a/qt_ui/windows/QDebriefingWindow.py
+++ b/qt_ui/windows/QDebriefingWindow.py
@@ -1,8 +1,15 @@
from PySide2.QtGui import QIcon, QPixmap
-from PySide2.QtWidgets import QLabel, QDialog, QVBoxLayout, QGroupBox, QGridLayout, QPushButton
+from PySide2.QtWidgets import (
+ QDialog,
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QPushButton,
+ QVBoxLayout,
+)
-from game.game import Event, db, Game
-from userdata.debriefing import Debriefing
+from game.debriefing import Debriefing
+from game.game import Event, Game, db
class QDebriefingWindow(QDialog):
diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py
index bf086c5d..67dba6f8 100644
--- a/qt_ui/windows/QLiberationWindow.py
+++ b/qt_ui/windows/QLiberationWindow.py
@@ -1,23 +1,36 @@
import logging
-import sys
+import traceback
import webbrowser
+from typing import Optional
from PySide2.QtCore import Qt
-from PySide2.QtGui import QIcon
-from PySide2.QtWidgets import QWidget, QVBoxLayout, QMainWindow, QAction, QMessageBox, QDesktopWidget, \
- QSplitter, QFileDialog
+from PySide2.QtGui import QCloseEvent, QIcon
+from PySide2.QtWidgets import (
+ QAction,
+ QActionGroup, QDesktopWidget,
+ QFileDialog,
+ QMainWindow,
+ QMessageBox,
+ QSplitter,
+ QVBoxLayout,
+ QWidget,
+)
import qt_ui.uiconstants as CONST
-from game import Game
+from game import Game, VERSION, persistency
+from qt_ui.dialogs import Dialog
+from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
+from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel
+from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
-from qt_ui.windows.GameUpdateSignal import GameUpdateSignal, DebriefingSignal
+from qt_ui.windows.GameUpdateSignal import DebriefingSignal, GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
-from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
-from qt_ui.windows.preferences.QLiberationPreferencesWindow import QLiberationPreferencesWindow
-from userdata import persistency
+from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
+from qt_ui.windows.preferences.QLiberationPreferencesWindow import \
+ QLiberationPreferencesWindow
class QLiberationWindow(QMainWindow):
@@ -25,11 +38,15 @@ class QLiberationWindow(QMainWindow):
def __init__(self):
super(QLiberationWindow, self).__init__()
- self.info_panel = None
- self.setGame(persistency.restore_game())
+ self.game: Optional[Game] = None
+ self.game_model = GameModel()
+ Dialog.set_game(self.game_model)
+ self.ato_panel = QAirTaskingOrderPanel(self.game_model)
+ self.info_panel = QInfoPanel(self.game)
+ self.liberation_map = QLiberationMap(self.game_model)
self.setGeometry(300, 100, 270, 100)
- self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
+ self.setWindowTitle(f"DCS Liberation - v{VERSION}")
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage('Ready')
@@ -38,26 +55,29 @@ class QLiberationWindow(QMainWindow):
self.initMenuBar()
self.initToolbar()
self.connectSignals()
- self.onGameGenerated(self.game)
screen = QDesktopWidget().screenGeometry()
self.setGeometry(0, 0, screen.width(), screen.height())
self.setWindowState(Qt.WindowMaximized)
+ self.onGameGenerated(persistency.restore_game())
def initUi(self):
-
- self.liberation_map = QLiberationMap(self.game)
- self.info_panel = QInfoPanel(self.game)
-
hbox = QSplitter(Qt.Horizontal)
- hbox.addWidget(self.info_panel)
- hbox.addWidget(self.liberation_map)
- hbox.setSizes([2, 8])
+ vbox = QSplitter(Qt.Vertical)
+ hbox.addWidget(self.ato_panel)
+ hbox.addWidget(vbox)
+ vbox.addWidget(self.liberation_map)
+ vbox.addWidget(self.info_panel)
+
+ # Will make the ATO sidebar as small as necessary to fit the content. In
+ # practice this means it is sized by the hints in the panel.
+ hbox.setSizes([1, 10000000])
+ vbox.setSizes([600, 100])
vbox = QVBoxLayout()
vbox.setMargin(0)
- vbox.addWidget(QTopPanel(self.game))
+ vbox.addWidget(QTopPanel(self.game_model))
vbox.addWidget(hbox)
central_widget = QWidget()
@@ -115,48 +135,23 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator()
- #file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
- file_menu.addAction("E&xit" , lambda: self.exit())
+ file_menu.addAction("E&xit", self.close)
displayMenu = self.menu.addMenu("&Display")
- tg_cp_visibility = QAction('&Control Point', displayMenu)
- tg_cp_visibility.setCheckable(True)
- tg_cp_visibility.setChecked(True)
- tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked()))
-
- tg_go_visibility = QAction('&Ground Objects', displayMenu)
- tg_go_visibility.setCheckable(True)
- tg_go_visibility.setChecked(True)
- tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked()))
-
- tg_line_visibility = QAction('&Lines', displayMenu)
- tg_line_visibility.setCheckable(True)
- tg_line_visibility.setChecked(True)
- tg_line_visibility.toggled.connect(
- lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked()))
-
- tg_event_visibility = QAction('&Events', displayMenu)
- tg_event_visibility.setCheckable(True)
- tg_event_visibility.setChecked(True)
- tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked()))
-
- tg_sam_visibility = QAction('&SAM Range', displayMenu)
- tg_sam_visibility.setCheckable(True)
- tg_sam_visibility.setChecked(True)
- tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked()))
-
- tg_flight_path_visibility = QAction('&Flight Paths', displayMenu)
- tg_flight_path_visibility.setCheckable(True)
- tg_flight_path_visibility.setChecked(False)
- tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked()))
-
- displayMenu.addAction(tg_go_visibility)
- displayMenu.addAction(tg_cp_visibility)
- displayMenu.addAction(tg_line_visibility)
- displayMenu.addAction(tg_event_visibility)
- displayMenu.addAction(tg_sam_visibility)
- displayMenu.addAction(tg_flight_path_visibility)
+ last_was_group = True
+ for item in DisplayOptions.menu_items():
+ if isinstance(item, DisplayRule):
+ displayMenu.addAction(self.make_display_rule_action(item))
+ last_was_group = False
+ elif isinstance(item, DisplayGroup):
+ if not last_was_group:
+ displayMenu.addSeparator()
+ group = QActionGroup(displayMenu)
+ for display_rule in item:
+ displayMenu.addAction(
+ self.make_display_rule_action(display_rule, group))
+ last_was_group = True
help_menu = self.menu.addMenu("&Help")
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
@@ -169,6 +164,21 @@ class QLiberationWindow(QMainWindow):
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
+ @staticmethod
+ def make_display_rule_action(
+ display_rule, group: Optional[QActionGroup] = None) -> QAction:
+ def make_check_closure():
+ def closure():
+ display_rule.value = action.isChecked()
+
+ return closure
+
+ action = QAction(f"&{display_rule.menu_text}", group)
+ action.setCheckable(True)
+ action.setChecked(display_rule.value)
+ action.toggled.connect(make_check_closure())
+ return action
+
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
@@ -180,8 +190,7 @@ class QLiberationWindow(QMainWindow):
filter="*.liberation")
if file is not None:
game = persistency.load_game(file[0])
- self.setGame(game)
- GameUpdateSignal.get_instance().updateGame(self.game)
+ GameUpdateSignal.get_instance().updateGame(game)
def saveGame(self):
logging.info("Saving game")
@@ -203,20 +212,31 @@ class QLiberationWindow(QMainWindow):
self.game = game
GameUpdateSignal.get_instance().updateGame(self.game)
- def closeGame(self):
- self.game = None
- GameUpdateSignal.get_instance().updateGame(self.game)
-
- def exit(self):
- sys.exit(0)
-
- def setGame(self, game: Game):
- self.game = game
- if self.info_panel:
- self.info_panel.setGame(game)
+ def setGame(self, game: Optional[Game]):
+ try:
+ if game is not None:
+ game.on_load()
+ self.game = game
+ if self.info_panel is not None:
+ self.info_panel.setGame(game)
+ self.game_model.set(self.game)
+ if self.liberation_map is not None:
+ self.liberation_map.setGame(game)
+ except AttributeError:
+ logging.exception("Incompatible save game")
+ QMessageBox.critical(
+ self,
+ "Could not load save game",
+ "The save game you have loaded is incompatible with this "
+ "version of DCS Liberation.\n"
+ "\n"
+ f"{traceback.format_exc()}",
+ QMessageBox.Ok
+ )
+ GameUpdateSignal.get_instance().updateGame(None)
def showAboutDialog(self):
- text = "
DCS Liberation was originally developed by shdwp, DCS Liberation 2.0 is a partial rewrite based on this work by Khopa." \ @@ -241,3 +261,14 @@ class QLiberationWindow(QMainWindow): logging.info("On Debriefing") self.debriefing = QDebriefingWindow(debrief.debriefing, debrief.gameEvent, debrief.game) self.debriefing.show() + + def closeEvent(self, event: QCloseEvent) -> None: + result = QMessageBox.question( + self, "Quit Liberation?", + "Are you sure you want to quit? All unsaved progress will be lost.", + QMessageBox.Yes | QMessageBox.No + ) + if result == QMessageBox.Yes: + super().closeEvent(event) + else: + event.ignore() diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index be56c99c..c35a482e 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -2,15 +2,24 @@ import json import os from PySide2 import QtCore -from PySide2.QtCore import QObject, Signal, Qt -from PySide2.QtGui import QMovie, QIcon, QPixmap -from PySide2.QtWidgets import QLabel, QDialog, QGroupBox, QGridLayout, QPushButton, QFileDialog, QMessageBox, QTextEdit, \ - QHBoxLayout +from PySide2.QtCore import QObject, Qt, Signal +from PySide2.QtGui import QIcon, QMovie, QPixmap +from PySide2.QtWidgets import ( + QDialog, + QFileDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QTextEdit, +) +from game.debriefing import Debriefing, wait_for_debriefing from game.game import Event, Game, logging +from game.persistency import base_path from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from userdata.debriefing import wait_for_debriefing, Debriefing -from userdata.persistency import base_path class DebriefingFileWrittenSignal(QObject): @@ -163,7 +172,7 @@ class QWaitingForMissionResultWindow(QDialog): def process_debriefing(self): self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing) - self.game.pass_turn(ignored_cps=[self.gameEvent.to_cp, ]) + self.game.pass_turn() GameUpdateSignal.get_instance().sendDebriefing(self.game, self.gameEvent, self.debriefing) self.close() diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 678f6098..cf5e1a34 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -1,31 +1,31 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QPixmap -from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget, QDialog, QGridLayout +from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget -from game import Game -from game.event import ControlPointType +from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint +from theater import ControlPoint, ControlPointType class QBaseMenu2(QDialog): - def __init__(self, parent, cp: ControlPoint, game: Game): + def __init__(self, parent, cp: ControlPoint, game_model: GameModel): super(QBaseMenu2, self).__init__(parent) # Attrs self.cp = cp - self.game = game + self.game_model = game_model self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] self.objectName = "menuDialogue" # Widgets - self.qbase_menu_tab = QBaseMenuTabs(cp, game) + self.qbase_menu_tab = QBaseMenuTabs(cp, self.game_model) try: + game = self.game_model.game self.airport = game.theater.terrain.airport_by_id(self.cp.id) except: self.airport = None @@ -70,7 +70,9 @@ class QBaseMenu2(QDialog): self.mainLayout.addWidget(header, 0, 0) self.mainLayout.addWidget(self.topLayoutWidget, 1, 0) self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0) - totalBudget = QLabel(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + totalBudget = QLabel( + QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget) + ) totalBudget.setObjectName("budgetField") totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom) totalBudget.setProperty("style", "budget-label") @@ -78,7 +80,7 @@ class QBaseMenu2(QDialog): self.setLayout(self.mainLayout) def closeEvent(self, closeEvent:QCloseEvent): - GameUpdateSignal.get_instance().updateGame(self.game) + GameUpdateSignal.get_instance().updateGame(self.game_model.game) def get_base_image(self): if self.cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index dcb05ee6..0c82c86e 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -1,6 +1,6 @@ -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget -from game import Game +from qt_ui.models import GameModel from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ @@ -10,29 +10,29 @@ from theater import ControlPoint class QBaseMenuTabs(QTabWidget): - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): super(QBaseMenuTabs, self).__init__() self.cp = cp if cp: if not cp.captured: - self.intel = QIntelInfo(cp, game) - self.addTab(self.intel, "Intel") if not cp.is_carrier: - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Base Defenses") + self.intel = QIntelInfo(cp, game_model.game) + self.addTab(self.intel, "Intel") else: if cp.has_runway(): - self.airfield_command = QAirfieldCommand(cp, game) + self.airfield_command = QAirfieldCommand(cp, game_model) self.addTab(self.airfield_command, "Airfield Command") if not cp.is_carrier: - self.ground_forces_hq = QGroundForcesHQ(cp, game) + self.ground_forces_hq = QGroundForcesHQ(cp, game_model) self.addTab(self.ground_forces_hq, "Ground Forces HQ") - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Base Defenses") else: - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Fleet") else: diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index f180dd58..b41ac68a 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,13 +1,19 @@ +from PySide2.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QSpacerItem, +) import logging - -from PySide2.QtWidgets import QLabel, QPushButton, \ - QSizePolicy, QSpacerItem, QGroupBox, QHBoxLayout from dcs.unittype import UnitType from theater import db -class QRecruitBehaviour: + +class QRecruitBehaviour: game = None cp = None deliveryEvent = None @@ -17,14 +23,22 @@ class QRecruitBehaviour: recruitable_types = [] BUDGET_FORMAT = "Available Budget: ${}M" - def __init__(self): + def __init__(self) -> None: + self.deliveryEvent = None self.bought_amount_labels = {} self.existing_units_labels = {} self.recruitable_types = [] self.update_available_budget() - def add_purchase_row(self, unit_type, layout, row): + @property + def budget(self) -> int: + return self.game_model.game.budget + @budget.setter + def budget(self, value: int) -> None: + self.game_model.game.budget = value + + def add_purchase_row(self, unit_type, layout, row): exist = QGroupBox() exist.setProperty("style", "buy-box") exist.setMaximumHeight(36) @@ -102,7 +116,8 @@ class QRecruitBehaviour: parent = parent.parent() for child in parent.children(): if child.objectName() == "budgetField": - child.setText(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + child.setText( + QRecruitBehaviour.BUDGET_FORMAT.format(self.budget)) def buy(self, unit_type): @@ -113,9 +128,9 @@ class QRecruitBehaviour: return price = db.PRICES[unit_type] - if self.game.budget >= price: + if self.budget >= price: self.deliveryEvent.deliver({unit_type: 1}) - self.game.budget -= price + self.budget -= price else: # TODO : display modal warning logging.info("Not enough money !") @@ -125,13 +140,13 @@ class QRecruitBehaviour: def sell(self, unit_type): if self.deliveryEvent.units.get(unit_type, 0) > 0: price = db.PRICES[unit_type] - self.game.budget += price + self.budget += price self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1 if self.deliveryEvent.units[unit_type] == 0: del self.deliveryEvent.units[unit_type] elif self.cp.base.total_units_of_type(unit_type) > 0: price = db.PRICES[unit_type] - self.game.budget += price + self.budget += price self.cp.base.commit_losses({unit_type: 1}) self._update_count_label(unit_type) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index a4abf404..a01aaaa9 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -1,25 +1,40 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QScrollArea, QFrame, QWidget, QHBoxLayout, QLabel +from typing import Optional, Set -from game.event import UnitsDeliveryEvent +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QHBoxLayout, + QLabel, + QMessageBox, + QScrollArea, + QVBoxLayout, + QWidget, +) +from dcs.unittype import UnitType + +from game.event.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.uiconstants import ICONS from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint, CAP, CAS, db, ControlPointType -from game import Game +from theater import CAP, CAS, ControlPoint, db class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): - - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model + self.deliveryEvent: Optional[UnitsDeliveryEvent] = None - for event in self.game.events: + self.bought_amount_labels = {} + self.existing_units_labels = {} + + for event in self.game_model.game.events: if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: self.deliveryEvent = event if not self.deliveryEvent: - self.deliveryEvent = self.game.units_delivery_event(self.cp) + self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) # Determine maximum number of aircrafts that can be bought self.set_maximum_units(self.cp.available_aircraft_slots) @@ -35,25 +50,27 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): def init_ui(self): main_layout = QVBoxLayout() - units = { - CAP: db.find_unittype(CAP, self.game.player_name), - CAS: db.find_unittype(CAS, self.game.player_name), - } + tasks = [CAP, CAS] scroll_content = QWidget() task_box_layout = QGridLayout() row = 0 - for task_type in units.keys(): - units_column = list(set(units[task_type])) - if len(units_column) == 0: continue - units_column.sort(key=lambda x: db.PRICES[x]) - for unit_type in units_column: - if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE: + unit_types: Set[UnitType] = set() + for task in tasks: + units = db.find_unittype(task, self.game_model.game.player_name) + if not units: + continue + for unit in units: + if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE: continue - if self.cp.is_lha and not unit_type in db.LHA_CAPABLE: + if self.cp.is_lha and unit not in db.LHA_CAPABLE: continue - row = self.add_purchase_row(unit_type, task_box_layout, row) + unit_types.add(unit) + + sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u)) + for unit_type in sorted_units: + row = self.add_purchase_row(unit_type, task_box_layout, row) stretch = QVBoxLayout() stretch.addStretch() task_box_layout.addLayout(stretch, row, 0) @@ -72,7 +89,21 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): super().buy(unit_type) self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots) - def sell(self, unit_type): + def sell(self, unit_type: UnitType): + # Don't need to remove aircraft from the inventory if we're canceling + # orders. + if self.deliveryEvent.units.get(unit_type, 0) <= 0: + global_inventory = self.game_model.game.aircraft_inventory + inventory = global_inventory.for_control_point(self.cp) + try: + inventory.remove_aircraft(unit_type, 1) + except ValueError: + QMessageBox.critical( + self, "Could not sell aircraft", + f"Attempted to sell one {unit_type.id} at {self.cp.name} " + "but none are available. Are all aircraft currently " + "assigned to a mission?", QMessageBox.Ok) + return super().sell(unit_type) self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots) diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 74b3c973..9965115a 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -1,31 +1,30 @@ -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QHBoxLayout, QGroupBox, QVBoxLayout -from game import Game -from qt_ui.widgets.base.QAirportInformation import QAirportInformation -from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import QAircraftRecruitmentMenu +from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout + +from qt_ui.models import GameModel +from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \ + QAircraftRecruitmentMenu from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView from theater import ControlPoint class QAirfieldCommand(QFrame): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp:ControlPoint, game_model: GameModel): super(QAirfieldCommand, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game), 0, 0) + layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) - try: - planned = QGroupBox("Planned Flights") - planned_layout = QVBoxLayout() - planned_layout.addWidget(QPlannedFlightsView(self.game.planners[self.cp.id])) - planned.setLayout(planned_layout) - layout.addWidget(planned, 0, 1) - except: - pass + planned = QGroupBox("Planned Flights") + planned_layout = QVBoxLayout() + planned_layout.addWidget( + QPlannedFlightsView(self.game_model, self.cp) + ) + planned.setLayout(planned_layout) + layout.addWidget(planned, 0, 1) - #layout.addWidget(QAirportInformation(self.cp, self.game.theater.terrain.airport_by_id(self.cp.id)), 0, 2) self.setLayout(layout) diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py index 370cf65a..350cf5e8 100644 --- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py +++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py @@ -1,6 +1,7 @@ from PySide2.QtCore import Qt from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout +from qt_ui.dialogs import Dialog from qt_ui.uiconstants import VEHICLES_ICONS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from theater import ControlPoint, TheaterGroundObject @@ -23,13 +24,20 @@ class QBaseDefenseGroupInfo(QGroupBox): def init_ui(self): self.buildLayout() - manage_button = QPushButton("Manage") - manage_button.setProperty("style", "btn-success") - manage_button.setMaximumWidth(180) - manage_button.clicked.connect(self.onManage) - self.main_layout.addLayout(self.unit_layout) - self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft) + if not self.cp.captured and not self.ground_object.is_dead: + attack_button = QPushButton("Attack") + attack_button.setProperty("style", "btn-danger") + attack_button.setMaximumWidth(180) + attack_button.clicked.connect(self.onAttack) + self.main_layout.addWidget(attack_button, 0, Qt.AlignLeft) + + if self.cp.captured: + manage_button = QPushButton("Manage") + manage_button.setProperty("style", "btn-success") + manage_button.setMaximumWidth(180) + manage_button.clicked.connect(self.onManage) + self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft) self.setLayout(self.main_layout) @@ -66,6 +74,9 @@ class QBaseDefenseGroupInfo(QGroupBox): self.setLayout(self.main_layout) + + def onAttack(self): + Dialog.open_new_package_dialog(self.ground_object, parent=self.window()) def onManage(self): self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game) diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index e260d64c..ec1cabf6 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -1,27 +1,33 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QFrame, QWidget, QScrollArea +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) -from game import Game from game.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from theater import ControlPoint, PinpointStrike, db class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game.events: + for event in self.game_model.game.events: if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: self.deliveryEvent = event if not self.deliveryEvent: - self.deliveryEvent = self.game.units_delivery_event(self.cp) + self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) self.init_ui() @@ -29,7 +35,8 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): main_layout = QVBoxLayout() units = { - PinpointStrike: db.find_unittype(PinpointStrike, self.game.player_name), + PinpointStrike: db.find_unittype(PinpointStrike, + self.game_model.game.player_name), } scroll_content = QWidget() diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py index 1ea116e3..bb18594f 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py @@ -1,21 +1,24 @@ from PySide2.QtWidgets import QFrame, QGridLayout -from game import Game -from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import QArmorRecruitmentMenu -from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import QGroundForcesStrategy +from qt_ui.models import GameModel +from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \ + QArmorRecruitmentMenu +from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \ + QGroundForcesStrategy from theater import ControlPoint class QGroundForcesHQ(QFrame): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: super(QGroundForcesHQ, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game), 0, 0) - layout.addWidget(QGroundForcesStrategy(self.cp, self.game), 0, 1) + layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game_model), 0, 0) + layout.addWidget(QGroundForcesStrategy(self.cp, self.game_model.game), + 0, 1) self.setLayout(layout) diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py index 2ccdf40b..09c3fa5b 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py @@ -14,8 +14,8 @@ class QGroundForcesStrategySelector(QComboBox): self.cp.stances[enemy_cp.id] = CombatStance.DEFENSIVE for i, stance in enumerate(CombatStance): - self.addItem(stance.name, userData=stance.value) - if self.cp.stances[enemy_cp.id] == stance.value: + self.addItem(stance.name, userData=stance) + if self.cp.stances[enemy_cp.id] == stance: self.setCurrentIndex(i) self.currentTextChanged.connect(self.on_change) diff --git a/qt_ui/windows/mission/QChooseAirbase.py b/qt_ui/windows/mission/QChooseAirbase.py deleted file mode 100644 index 50a86538..00000000 --- a/qt_ui/windows/mission/QChooseAirbase.py +++ /dev/null @@ -1,32 +0,0 @@ -from PySide2.QtCore import Signal -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel - -from game import Game - - -class QChooseAirbase(QGroupBox): - - selected_airbase_changed = Signal(str) - - def __init__(self, game:Game, title=""): - super(QChooseAirbase, self).__init__(title) - self.game = game - - self.layout = QHBoxLayout() - self.depart_from_label = QLabel("Airbase : ") - self.depart_from = QComboBox() - - for i, cp in enumerate([b for b in self.game.theater.controlpoints if b.captured and b.id in self.game.planners]): - self.depart_from.addItem(str(cp.name), cp) - self.depart_from.setCurrentIndex(0) - self.depart_from.currentTextChanged.connect(self._on_airbase_selected) - self.layout.addWidget(self.depart_from_label) - self.layout.addWidget(self.depart_from) - self.setLayout(self.layout) - - def _on_airbase_selected(self): - selected = self.depart_from.currentText() - self.selected_airbase_changed.emit(selected) - - - diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py new file mode 100644 index 00000000..629a7836 --- /dev/null +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -0,0 +1,36 @@ +"""Dialog window for editing flights.""" +from PySide2.QtWidgets import ( + QDialog, + QVBoxLayout, +) + +from gen.ato import Package +from gen.flights.flight import Flight +from qt_ui.models import GameModel +from qt_ui.uiconstants import EVENT_ICONS +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner + + +class QEditFlightDialog(QDialog): + """Dialog window for editing flight plans and loadouts.""" + + def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None: + super().__init__(parent=parent) + + self.game_model = game_model + + self.setWindowTitle("Create flight") + self.setWindowIcon(EVENT_ICONS["strike"]) + + layout = QVBoxLayout() + + self.flight_planner = QFlightPlanner(package, flight, game_model.game) + layout.addWidget(self.flight_planner) + + self.setLayout(layout) + self.finished.connect(self.on_close) + + def on_close(self, _result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() + self.game_model.ato_model.client_slots_changed.emit() diff --git a/qt_ui/windows/mission/QFlightItem.py b/qt_ui/windows/mission/QFlightItem.py index 5e4c4c11..78966dbf 100644 --- a/qt_ui/windows/mission/QFlightItem.py +++ b/qt_ui/windows/mission/QFlightItem.py @@ -1,26 +1,28 @@ +import datetime + from PySide2.QtGui import QStandardItem, QIcon from game import db +from gen.ato import Package from gen.flights.flight import Flight +from gen.flights.traveltime import TotEstimator from qt_ui.uiconstants import AIRCRAFT_ICONS +# TODO: Replace with QFlightList. class QFlightItem(QStandardItem): - def __init__(self, flight:Flight): + def __init__(self, package: Package, flight: Flight): super(QFlightItem, self).__init__() + self.package = package self.flight = flight if db.unit_type_name(self.flight.unit_type).replace("/", " ") in AIRCRAFT_ICONS.keys(): icon = QIcon((AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)])) self.setIcon(icon) self.setEditable(False) + estimator = TotEstimator(self.package) + delay = estimator.mission_start_time(flight) self.setText("["+str(self.flight.flight_type.name[:6])+"] " + str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type) - + " in " + str(self.flight.scheduled_in) + " minutes") - - def update(self, flight): - self.flight = flight - self.setText("[" + str(self.flight.flight_type.name[:6]) + "] " - + str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type) - + " in " + str(self.flight.scheduled_in) + " minutes") \ No newline at end of file + + " in " + str(delay)) diff --git a/qt_ui/windows/mission/QMissionPlanning.py b/qt_ui/windows/mission/QMissionPlanning.py deleted file mode 100644 index 04ffdeb3..00000000 --- a/qt_ui/windows/mission/QMissionPlanning.py +++ /dev/null @@ -1,159 +0,0 @@ -from PySide2.QtCore import Qt, Slot, QItemSelectionModel, QPoint -from PySide2.QtWidgets import QDialog, QGridLayout, QScrollArea, QVBoxLayout, QPushButton, QHBoxLayout, QMessageBox -from game import Game -from game.event import CAP, CAS, FrontlineAttackEvent -from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow -from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView -from qt_ui.windows.mission.QChooseAirbase import QChooseAirbase -from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner - - -class QMissionPlanning(QDialog): - - def __init__(self, game: Game): - super(QMissionPlanning, self).__init__() - self.game = game - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setMinimumSize(1000, 440) - self.setWindowTitle("Mission Preparation") - self.setWindowIcon(EVENT_ICONS["strike"]) - self.init_ui() - print("DONE") - - def init_ui(self): - - self.captured_cp = [cp for cp in self.game.theater.controlpoints if cp.captured] - - self.layout = QGridLayout() - self.left_bar_layout = QVBoxLayout() - - self.select_airbase = QChooseAirbase(self.game) - self.select_airbase.selected_airbase_changed.connect(self.on_departure_cp_changed) - self.planned_flight_view = QPlannedFlightsView(None) - self.available_aircraft_at_selected_location = {} - if self.captured_cp[0].id in self.game.planners.keys(): - self.planner = self.game.planners[self.captured_cp[0].id] - self.planned_flight_view.set_flight_planner(self.planner) - self.selected_cp = self.captured_cp[0] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - - self.planned_flight_view.selectionModel().setCurrentIndex(self.planned_flight_view.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows) - self.planned_flight_view.selectionModel().selectionChanged.connect(self.on_flight_selection_change) - - if len(self.planned_flight_view.flight_planner.flights) > 0: - self.flight_planner = QFlightPlanner(self.planned_flight_view.flight_planner.flights[0], self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - else: - self.flight_planner = QFlightPlanner(None, self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - - self.add_flight_button = QPushButton("Add Flight") - self.add_flight_button.clicked.connect(self.on_add_flight) - self.delete_flight_button = QPushButton("Delete Selected") - self.delete_flight_button.setProperty("style", "btn-danger") - self.delete_flight_button.clicked.connect(self.on_delete_flight) - - self.button_layout = QHBoxLayout() - self.button_layout.addStretch() - self.button_layout.addWidget(self.delete_flight_button) - self.button_layout.addWidget(self.add_flight_button) - - self.mission_start_button = QPushButton("Take Off") - self.mission_start_button.setProperty("style", "start-button") - self.mission_start_button.clicked.connect(self.on_start) - - self.left_bar_layout.addWidget(self.select_airbase) - self.left_bar_layout.addWidget(self.planned_flight_view) - self.left_bar_layout.addLayout(self.button_layout) - - self.layout.addLayout(self.left_bar_layout, 0, 0) - self.layout.addWidget(self.flight_planner, 0, 1) - self.layout.addWidget(self.mission_start_button, 1, 1, alignment=Qt.AlignRight) - - self.setLayout(self.layout) - - @Slot(str) - def on_departure_cp_changed(self, cp_name): - cps = [cp for cp in self.game.theater.controlpoints if cp.name == cp_name] - - print(cps) - - if len(cps) == 1: - self.selected_cp = cps[0] - self.planner = self.game.planners[cps[0].id] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - self.planned_flight_view.set_flight_planner(self.planner) - else: - self.available_aircraft_at_selected_location = {} - self.planned_flight_view.set_flight_planner(None) - - def on_flight_selection_change(self): - - print("On flight selection change") - - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planned_flight_view.repaint() - - if self.flight_planner is not None: - self.flight_planner.on_planned_flight_changed.disconnect() - self.flight_planner.clearTabs() - - try: - flight = self.planner.flights[index] - except IndexError: - flight = None - self.flight_planner = QFlightPlanner(flight, self.game, self.planner, self.flight_planner.currentIndex()) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - self.layout.addWidget(self.flight_planner, 0, 1) - - def update_planned_flight_view(self): - self.planned_flight_view.update_content() - - def on_add_flight(self): - possible_aircraft_type = list(self.selected_cp.base.aircraft.keys()) - - if len(possible_aircraft_type) == 0: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("No more aircraft are available on " + self.selected_cp.name + " airbase.") - msg.setWindowTitle("No more aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() - else: - self.subwindow = QFlightCreator(self.game, self.selected_cp, possible_aircraft_type, self.planned_flight_view) - self.subwindow.show() - - def on_delete_flight(self): - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planner.remove_flight(index) - self.planned_flight_view.set_flight_planner(self.planner, index) - - - def on_start(self): - - # TODO : refactor this nonsense - self.gameEvent = None - for event in self.game.events: - if isinstance(event, FrontlineAttackEvent) and event.is_player_attacking: - self.gameEvent = event - if self.gameEvent is None: - self.gameEvent = FrontlineAttackEvent(self.game, self.game.theater.controlpoints[0], self.game.theater.controlpoints[0], - self.game.theater.controlpoints[0].position, self.game.player_name, self.game.enemy_name) - #if self.awacs_checkbox.isChecked() == 1: - # self.gameEvent.is_awacs_enabled = True - # self.game.awacs_expense_commit() - #else: - # self.gameEvent.is_awacs_enabled = False - self.gameEvent.is_awacs_enabled = True - self.gameEvent.ca_slots = 1 - self.gameEvent.departure_cp = self.game.theater.controlpoints[0] - self.gameEvent.player_attacking({CAS:{}, CAP:{}}) - self.gameEvent.depart_from = self.game.theater.controlpoints[0] - - self.game.initiate_event(self.gameEvent) - waiting = QWaitingForMissionResultWindow(self.gameEvent, self.game) - waiting.show() - self.close() diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py new file mode 100644 index 00000000..6298379f --- /dev/null +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -0,0 +1,242 @@ +"""Dialogs for creating and editing ATO packages.""" +import logging +from datetime import timedelta +from typing import Optional + +from PySide2.QtCore import QItemSelection, QTime, Signal +from PySide2.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QPushButton, + QTimeEdit, + QVBoxLayout, +) + +from game.game import Game +from gen.ato import Package +from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder +from gen.flights.traveltime import TotEstimator +from qt_ui.models import AtoModel, GameModel, PackageModel +from qt_ui.uiconstants import EVENT_ICONS +from qt_ui.widgets.ato import QFlightList +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator +from theater.missiontarget import MissionTarget + + +class QPackageDialog(QDialog): + """Base package management dialog. + + The dialogs for creating a new package and editing an existing dialog are + very similar, and this implements the shared behavior. + """ + + #: Emitted when a change is made to the package. + package_changed = Signal() + + def __init__(self, game_model: GameModel, model: PackageModel, parent=None) -> None: + super().__init__(parent) + self.game_model = game_model + self.package_model = model + self.add_flight_dialog: Optional[QFlightCreator] = None + + self.setMinimumSize(1000, 440) + self.setWindowTitle( + f"Mission Package: {self.package_model.mission_target.name}" + ) + self.setWindowIcon(EVENT_ICONS["strike"]) + + self.layout = QVBoxLayout() + + self.summary_row = QHBoxLayout() + self.layout.addLayout(self.summary_row) + + self.package_type_column = QHBoxLayout() + self.summary_row.addLayout(self.package_type_column) + + self.package_type_label = QLabel("Package Type:") + self.package_type_text = QLabel(self.package_model.description) + # noinspection PyUnresolvedReferences + self.package_changed.connect(lambda: self.package_type_text.setText( + self.package_model.description + )) + self.package_type_column.addWidget(self.package_type_label) + self.package_type_column.addWidget(self.package_type_text) + + self.summary_row.addStretch(1) + + self.tot_column = QHBoxLayout() + self.summary_row.addLayout(self.tot_column) + + self.tot_label = QLabel("Time Over Target:") + self.tot_column.addWidget(self.tot_label) + + self.tot_spinner = QTimeEdit(self.tot_qtime()) + self.tot_spinner.setMinimumTime(QTime(0, 0)) + self.tot_spinner.setDisplayFormat("T+hh:mm:ss") + self.tot_spinner.timeChanged.connect(self.save_tot) + self.tot_column.addWidget(self.tot_spinner) + + self.reset_tot_button = QPushButton("ASAP") + self.reset_tot_button.setToolTip( + "Sets the package TOT to the earliest time that all flights can " + "arrive at the target." + ) + self.reset_tot_button.clicked.connect(self.reset_tot) + self.tot_column.addWidget(self.reset_tot_button) + + self.package_view = QFlightList(self.game_model, self.package_model) + self.package_view.selectionModel().selectionChanged.connect( + self.on_selection_changed + ) + self.layout.addWidget(self.package_view) + + self.button_layout = QHBoxLayout() + self.layout.addLayout(self.button_layout) + + self.add_flight_button = QPushButton("Add Flight") + self.add_flight_button.clicked.connect(self.on_add_flight) + self.button_layout.addWidget(self.add_flight_button) + + self.delete_flight_button = QPushButton("Delete Selected") + self.delete_flight_button.setProperty("style", "btn-danger") + self.delete_flight_button.clicked.connect(self.on_delete_flight) + self.delete_flight_button.setEnabled(model.rowCount() > 0) + self.button_layout.addWidget(self.delete_flight_button) + + self.button_layout.addStretch() + + self.setLayout(self.layout) + + self.accepted.connect(self.on_save) + self.finished.connect(self.on_close) + self.rejected.connect(self.on_cancel) + + @property + def game(self) -> Game: + return self.game_model.game + + def tot_qtime(self) -> QTime: + delay = int(self.package_model.package.time_over_target.total_seconds()) + hours = delay // 3600 + minutes = delay // 60 % 60 + seconds = delay % 60 + return QTime(hours, minutes, seconds) + + def on_cancel(self) -> None: + pass + + @staticmethod + def on_close(_result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() + + def on_save(self) -> None: + self.save_tot() + + def save_tot(self) -> None: + time = self.tot_spinner.time() + seconds = time.hour() * 3600 + time.minute() * 60 + time.second() + self.package_model.update_tot(timedelta(seconds=seconds)) + + def reset_tot(self) -> None: + if not list(self.package_model.flights): + self.package_model.update_tot(timedelta()) + else: + self.package_model.update_tot( + TotEstimator(self.package_model.package).earliest_tot()) + self.tot_spinner.setTime(self.tot_qtime()) + + def on_selection_changed(self, selected: QItemSelection, + _deselected: QItemSelection) -> None: + """Updates the state of the delete button.""" + self.delete_flight_button.setEnabled(not selected.empty()) + + def on_add_flight(self) -> None: + """Opens the new flight dialog.""" + self.add_flight_dialog = QFlightCreator(self.game, + self.package_model.package, + parent=self.window()) + self.add_flight_dialog.created.connect(self.add_flight) + self.add_flight_dialog.show() + + def add_flight(self, flight: Flight) -> None: + """Adds the new flight to the package.""" + self.game.aircraft_inventory.claim_for_flight(flight) + self.package_model.add_flight(flight) + planner = FlightPlanBuilder(self.game, self.package_model.package, + is_player=True) + planner.populate_flight_plan(flight) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + + def on_delete_flight(self) -> None: + """Removes the selected flight from the package.""" + flight = self.package_view.selected_item + if flight is None: + logging.error(f"Cannot delete flight when no flight is selected.") + return + self.game.aircraft_inventory.return_from_flight(flight) + self.package_model.delete_flight(flight) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + + +class QNewPackageDialog(QPackageDialog): + """Dialog window for creating a new package. + + New packages do not affect the ATO model until they are saved. + """ + + def __init__(self, game_model: GameModel, model: AtoModel, + target: MissionTarget, parent=None) -> None: + super().__init__(game_model, PackageModel(Package(target)), parent=parent) + self.ato_model = model + + self.save_button = QPushButton("Save") + self.save_button.setProperty("style", "start-button") + self.save_button.clicked.connect(self.accept) + self.button_layout.addWidget(self.save_button) + + def on_save(self) -> None: + """Saves the created package. + + Empty packages may be created. They can be modified later, and will have + no effect if empty when the mission is generated. + """ + super().on_save() + self.ato_model.add_package(self.package_model.package) + + def on_cancel(self) -> None: + super().on_cancel() + for flight in self.package_model.package.flights: + self.game.aircraft_inventory.return_from_flight(flight) + + +class QEditPackageDialog(QPackageDialog): + """Dialog window for editing an existing package. + + Changes to existing packages occur immediately. + """ + + def __init__(self, game_model: GameModel, model: AtoModel, + package: PackageModel) -> None: + super().__init__(game_model, package) + self.ato_model = model + + self.delete_button = QPushButton("Delete package") + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_layout.addWidget(self.delete_button) + + self.done_button = QPushButton("Done") + self.done_button.setProperty("style", "start-button") + self.done_button.clicked.connect(self.accept) + self.button_layout.addWidget(self.done_button) + + def on_delete(self) -> None: + """Removes the viewed package from the ATO.""" + # The ATO model returns inventory for us when deleting a package. + self.ato_model.delete_package(self.package_model.package) + self.close() diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py index 0dcc8a81..2c602d56 100644 --- a/qt_ui/windows/mission/QPlannedFlightsView.py +++ b/qt_ui/windows/mission/QPlannedFlightsView.py @@ -1,37 +1,36 @@ -from PySide2.QtCore import QSize, QItemSelectionModel, QPoint +from PySide2.QtCore import QItemSelectionModel, QSize from PySide2.QtGui import QStandardItemModel -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtWidgets import QAbstractItemView, QListView -from gen.flights.ai_flight_planner import FlightPlanner +from qt_ui.models import GameModel from qt_ui.windows.mission.QFlightItem import QFlightItem +from theater.controlpoint import ControlPoint class QPlannedFlightsView(QListView): - def __init__(self, flight_planner: FlightPlanner): + def __init__(self, game_model: GameModel, cp: ControlPoint) -> None: super(QPlannedFlightsView, self).__init__() + self.game_model = game_model + self.cp = cp self.model = QStandardItemModel(self) self.setModel(self.model) - self.flightitems = [] + self.flight_items = [] self.setIconSize(QSize(91, 24)) self.setSelectionBehavior(QAbstractItemView.SelectItems) - if flight_planner: - self.set_flight_planner(flight_planner) + self.set_flight_planner() - def update_content(self): - for i, f in enumerate(self.flight_planner.flights): - self.flightitems[i].update(f) + def setup_content(self): + self.flight_items = [] + for package in self.game_model.ato_model.packages: + for flight in package.flights: + if flight.from_cp == self.cp: + item = QFlightItem(package.package, flight) + self.model.appendRow(item) + self.flight_items.append(item) + self.set_selected_flight(0) - def setup_content(self, row=0): - self.flightitems = [] - for i, f in enumerate(self.flight_planner.flights): - item = QFlightItem(f) - self.model.appendRow(item) - self.flightitems.append(item) - self.setSelectedFlight(row) - self.repaint() - - def setSelectedFlight(self, row): + def set_selected_flight(self, row): self.selectionModel().clearSelection() index = self.model.index(row, 0) if not index.isValid(): @@ -42,8 +41,6 @@ class QPlannedFlightsView(QListView): def clear_layout(self): self.model.removeRows(0, self.model.rowCount()) - def set_flight_planner(self, flight_planner: FlightPlanner, row=0): + def set_flight_planner(self) -> None: self.clear_layout() - self.flight_planner = flight_planner - if self.flight_planner: - self.setup_content(row) + self.setup_content() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 293ba75f..ee4b3354 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -1,122 +1,126 @@ -from typing import List +from typing import Optional -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QPushButton, QSpinBox, \ - QMessageBox -from dcs import Point -from dcs.unittype import UnitType +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import ( + QDialog, + QMessageBox, + QPushButton, + QVBoxLayout, +) +from dcs.planes import PlaneType from game import Game -from gen.flights.ai_flight_planner import FlightPlanner -from gen.flights.flight import Flight, FlightWaypoint, FlightType +from gen.ato import Package +from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox +from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner +from qt_ui.widgets.QLabeledWidget import QLabeledWidget +from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector +from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox +from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector from theater import ControlPoint -PREDEFINED_WAYPOINT_CATEGORIES = [ - "Frontline (CAS AREA)", - "Building", - "Units", - "Airbase" -] - class QFlightCreator(QDialog): + created = Signal(Flight) + + def __init__(self, game: Game, package: Package, parent=None) -> None: + super().__init__(parent=parent) - def __init__(self, game: Game, from_cp:ControlPoint, possible_aircraft_type:List[UnitType], flight_view=None): - super(QFlightCreator, self).__init__() self.game = game - self.from_cp = from_cp - self.flight_view = flight_view - self.planner = self.game.planners[from_cp.id] - self.available = self.planner.get_available_aircraft() + self.package = package - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setModal(True) self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) - self.select_type_aircraft = QComboBox() - for aircraft_type in self.planner.get_available_aircraft().keys(): - print(aircraft_type) - print(aircraft_type.name) - if self.available[aircraft_type] > 0: - self.select_type_aircraft.addItem(aircraft_type.id, userData=aircraft_type) - self.select_type_aircraft.setCurrentIndex(0) - - self.select_flight_type = QComboBox() - self.select_flight_type.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP) - self.select_flight_type.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP) - self.select_flight_type.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP) - self.select_flight_type.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION) - self.select_flight_type.addItem("CAS [Close Air Support]", userData=FlightType.CAS) - self.select_flight_type.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI) - self.select_flight_type.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD) - self.select_flight_type.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD) - self.select_flight_type.addItem("STRIKE [Strike]", userData=FlightType.STRIKE) - self.select_flight_type.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP) - self.select_flight_type.setCurrentIndex(0) - - self.select_count_of_aircraft = QSpinBox() - self.select_count_of_aircraft.setMinimum(1) - self.select_count_of_aircraft.setMaximum(4) - self.select_count_of_aircraft.setValue(2) - - aircraft_type = self.select_type_aircraft.currentData() - if aircraft_type is not None: - self.select_count_of_aircraft.setValue(min(self.available[aircraft_type], 2)) - self.select_count_of_aircraft.setMaximum(min(self.available[aircraft_type], 4)) - - self.add_button = QPushButton("Add") - self.add_button.clicked.connect(self.create_flight) - - self.init_ui() - - - def init_ui(self): layout = QVBoxLayout() - type_layout = QHBoxLayout() - type_layout.addWidget(QLabel("Type of Aircraft : ")) - type_layout.addStretch() - type_layout.addWidget(self.select_type_aircraft, alignment=Qt.AlignRight) + self.task_selector = QFlightTypeComboBox( + self.game.theater, package.target + ) + self.task_selector.setCurrentIndex(0) + layout.addLayout(QLabeledWidget("Task:", self.task_selector)) - count_layout = QHBoxLayout() - count_layout.addWidget(QLabel("Count : ")) - count_layout.addStretch() - count_layout.addWidget(self.select_count_of_aircraft, alignment=Qt.AlignRight) + self.aircraft_selector = QAircraftTypeSelector( + self.game.aircraft_inventory.available_types_for_player + ) + self.aircraft_selector.setCurrentIndex(0) + self.aircraft_selector.currentIndexChanged.connect( + self.on_aircraft_changed) + layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector)) - flight_type_layout = QHBoxLayout() - flight_type_layout.addWidget(QLabel("Task : ")) - flight_type_layout.addStretch() - flight_type_layout.addWidget(self.select_flight_type, alignment=Qt.AlignRight) + self.airfield_selector = QOriginAirfieldSelector( + self.game.aircraft_inventory, + [cp for cp in game.theater.controlpoints if cp.captured], + self.aircraft_selector.currentData() + ) + self.airfield_selector.currentIndexChanged.connect(self.update_max_size) + layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector)) + + self.flight_size_spinner = QFlightSizeSpinner() + self.update_max_size() + layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner)) + + self.client_slots_spinner = QFlightSizeSpinner( + min_size=0, + max_size=self.flight_size_spinner.value(), + default_size=0 + ) + self.flight_size_spinner.valueChanged.connect( + lambda v: self.client_slots_spinner.setMaximum(v) + ) + layout.addLayout( + QLabeledWidget("Client Slots:", self.client_slots_spinner)) - layout.addLayout(type_layout) - layout.addLayout(count_layout) - layout.addLayout(flight_type_layout) layout.addStretch() - layout.addWidget(self.add_button, alignment=Qt.AlignRight) + + self.create_button = QPushButton("Create") + self.create_button.clicked.connect(self.create_flight) + layout.addWidget(self.create_button, alignment=Qt.AlignRight) self.setLayout(layout) - def create_flight(self): - aircraft_type = self.select_type_aircraft.currentData() - count = self.select_count_of_aircraft.value() + def verify_form(self) -> Optional[str]: + aircraft: PlaneType = self.aircraft_selector.currentData() + origin: ControlPoint = self.airfield_selector.currentData() + size: int = self.flight_size_spinner.value() + if not origin.captured: + return f"{origin.name} is not owned by your coalition." + available = origin.base.aircraft.get(aircraft, 0) + if not available: + return f"{origin.name} has no {aircraft.id} available." + if size > available: + return f"{origin.name} has only {available} {aircraft.id} available." + return None - if self.available[aircraft_type] < count: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("Not enough aircraft of this type are available. Only " + str(self.available[aircraft_type]) + " available.") - msg.setWindowTitle("Not enough aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() + def create_flight(self) -> None: + error = self.verify_form() + if error is not None: + QMessageBox.critical(self, "Could not create flight", error, + QMessageBox.Ok) return - else: - flight = Flight(aircraft_type, count, self.from_cp, self.select_flight_type.currentData()) - self.planner.flights.append(flight) - self.planner.custom_flights.append(flight) - if self.flight_view is not None: - self.flight_view.set_flight_planner(self.planner, len(self.planner.flights)-1) - self.close() + task = self.task_selector.currentData() + aircraft = self.aircraft_selector.currentData() + origin = self.airfield_selector.currentData() + size = self.flight_size_spinner.value() + + if self.game.settings.perf_ai_parking_start: + start_type = "Cold" + else: + start_type = "Warm" + flight = Flight(self.package, aircraft, size, origin, task, start_type) + flight.client_count = self.client_slots_spinner.value() + + # noinspection PyUnresolvedReferences + self.created.emit(flight) + self.close() + + def on_aircraft_changed(self, index: int) -> None: + new_aircraft = self.aircraft_selector.itemData(index) + self.airfield_selector.change_aircraft(new_aircraft) + + def update_max_size(self) -> None: + self.flight_size_spinner.setMaximum( + min(self.airfield_selector.available, 4) + ) diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index 6e422b93..b4eb9b36 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -1,42 +1,36 @@ from PySide2.QtCore import Signal -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QTabWidget -from gen.flights.flight import Flight from game import Game -from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import QFlightPayloadTab -from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import QGeneralFlightSettingsTab -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import QFlightWaypointTab +from gen.ato import Package +from gen.flights.flight import Flight +from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ + QFlightPayloadTab +from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \ + QGeneralFlightSettingsTab +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \ + QFlightWaypointTab class QFlightPlanner(QTabWidget): on_planned_flight_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner, selected_tab): - super(QFlightPlanner, self).__init__() + def __init__(self, package: Package, flight: Flight, game: Game): + super().__init__() - print(selected_tab) - - self.tabCount = 0 - if flight: - self.general_settings_tab = QGeneralFlightSettingsTab(flight, game, planner) - self.general_settings_tab.on_flight_settings_changed.connect(lambda: self.on_planned_flight_changed.emit()) - self.payload_tab = QFlightPayloadTab(flight, game) - self.waypoint_tab = QFlightWaypointTab(game, flight) - self.waypoint_tab.on_flight_changed.connect(lambda: self.on_planned_flight_changed.emit()) - self.addTab(self.general_settings_tab, "General Flight settings") - self.addTab(self.payload_tab, "Payload") - self.addTab(self.waypoint_tab, "Waypoints") - self.tabCount = 3 - self.setCurrentIndex(selected_tab) - else: - tabError = QFrame() - l = QGridLayout() - l.addWidget(QLabel("No flight selected")) - tabError.setLayout(l) - self.addTab(tabError, "No flight") - self.tabCount = 1 - - def clearTabs(self): - for i in range(self.tabCount): - self.removeTab(i) + self.general_settings_tab = QGeneralFlightSettingsTab( + game, package, flight + ) + # noinspection PyUnresolvedReferences + self.general_settings_tab.on_flight_settings_changed.connect( + lambda: self.on_planned_flight_changed.emit()) + self.payload_tab = QFlightPayloadTab(flight, game) + self.waypoint_tab = QFlightWaypointTab(game, package, flight) + # noinspection PyUnresolvedReferences + self.waypoint_tab.on_flight_changed.connect( + lambda: self.on_planned_flight_changed.emit()) + self.addTab(self.general_settings_tab, "General Flight settings") + self.addTab(self.payload_tab, "Payload") + self.addTab(self.waypoint_tab, "Waypoints") + self.setCurrentIndex(0) diff --git a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py deleted file mode 100644 index 8a69d4cd..00000000 --- a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py +++ /dev/null @@ -1,43 +0,0 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QDialog, QPushButton - -from game import Game -from gen.flights.flight import Flight -from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox - - -class QAbstractMissionGenerator(QDialog): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list, title): - super(QAbstractMissionGenerator, self).__init__() - self.game = game - self.flight = flight - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setMinimumSize(400, 250) - self.setModal(True) - self.setWindowTitle(title) - self.setWindowIcon(EVENT_ICONS["strike"]) - self.flight_waypoint_list = flight_waypoint_list - self.planner = self.game.planners[self.flight.from_cp.id] - - self.selected_waypoints = [] - self.wpt_info = QFlightWaypointInfoBox() - - self.ok_button = QPushButton("Ok") - self.ok_button.clicked.connect(self.apply) - - def on_select_wpt_changed(self): - self.selected_waypoints = self.wpt_selection_box.get_selected_waypoints(False) - if self.selected_waypoints is None or len(self.selected_waypoints) <= 0: - self.ok_button.setDisabled(True) - else: - self.wpt_info.set_flight_waypoint(self.selected_waypoints[0]) - self.ok_button.setDisabled(False) - - def apply(self): - raise NotImplementedError() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py deleted file mode 100644 index c1f5591e..00000000 --- a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py +++ /dev/null @@ -1,52 +0,0 @@ -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout - -from game import Game -from gen.flights.flight import Flight, PredefinedWaypointCategory -from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QCAPMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCAPMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAP Generator") - - self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, True, True, False, False, True) - self.wpt_selection_box.setMinimumWidth(200) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.init_ui() - self.on_select_wpt_changed() - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAP mission on : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - - wpt = self.selected_waypoints[0] - if wpt.category == PredefinedWaypointCategory.FRONTLINE: - self.planner.generate_frontline_cap(self.flight, wpt.data[0], wpt.data[1]) - elif wpt.category == PredefinedWaypointCategory.ALLY_CP: - self.planner.generate_barcap(self.flight, wpt.data) - else: - return - - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py deleted file mode 100644 index cfae4e52..00000000 --- a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py +++ /dev/null @@ -1,65 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox -from dcs import Point - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QCASMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCASMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAS Generator") - - self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, False, True, False, False) - self.wpt_selection_box.setMinimumWidth(200) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.init_ui() - self.on_select_wpt_changed() - - def on_select_wpt_changed(self): - super(QCASMissionGenerator, self).on_select_wpt_changed() - wpts = self.wpt_selection_box.get_selected_waypoints() - - if len(wpts) > 0: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(Point(wpts[0].x, wpts[0].y)))) + " nm") - else: - self.distanceToTargetLabel.setText("??? nm") - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAS : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - self.planner.generate_cas(self.flight, self.selected_waypoints[0].data[0], self.selected_waypoints[0].data[1]) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py deleted file mode 100644 index 7221844c..00000000 --- a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py +++ /dev/null @@ -1,84 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QSEADTargetSelectionComboBox import QSEADTargetSelectionComboBox -from qt_ui.widgets.views.QSeadTargetInfoView import QSeadTargetInfoView -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QSEADMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSEADMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "SEAD/DEAD Generator") - - self.tgt_selection_box = QSEADTargetSelectionComboBox(self.game) - self.tgt_selection_box.setMinimumWidth(200) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.threatRangeLabel = QLabel("0 nm") - self.detectionRangeLabel = QLabel("0 nm") - self.seadTargetInfoView = QSeadTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - if target is not None: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.threatRangeLabel.setText(str(meter_to_nm(target.threat_range)) + " nm") - self.detectionRangeLabel.setText(str(meter_to_nm(target.detection_range)) + " nm") - self.seadTargetInfoView.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("SEAD/DEAD target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distThreatBox = QGroupBox("Infos :") - threatLayout = QVBoxLayout() - - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to site : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - - threatRangeLayout = QHBoxLayout() - threatRangeLayout.addWidget(QLabel("Site threat range : ")) - threatRangeLayout.addStretch() - threatRangeLayout.addWidget(self.threatRangeLabel, alignment=Qt.AlignRight) - - detectionRangeLayout = QHBoxLayout() - detectionRangeLayout.addWidget(QLabel("Site radar detection range: ")) - detectionRangeLayout.addStretch() - detectionRangeLayout.addWidget(self.detectionRangeLabel, alignment=Qt.AlignRight) - - threatLayout.addLayout(distToTarget) - threatLayout.addLayout(threatRangeLayout) - threatLayout.addLayout(detectionRangeLayout) - distThreatBox.setLayout(threatLayout) - - layout.addLayout(wpt_layout) - layout.addWidget(self.seadTargetInfoView) - layout.addWidget(distThreatBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - target = self.tgt_selection_box.get_selected_target() - self.planner.generate_sead(self.flight, target.location, target.radars) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py deleted file mode 100644 index 6da88e0b..00000000 --- a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py +++ /dev/null @@ -1,64 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import QStrikeTargetSelectionComboBox -from qt_ui.widgets.views.QStrikeTargetInfoView import QStrikeTargetInfoView -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QSTRIKEMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSTRIKEMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "Strike Generator") - - self.tgt_selection_box = QStrikeTargetSelectionComboBox(self.game) - self.tgt_selection_box.setMinimumWidth(200) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - - self.distanceToTargetLabel = QLabel("0 nm") - self.strike_infos = QStrikeTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.strike_infos.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("Target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.strike_infos) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - target = self.tgt_selection_box.get_selected_target() - self.planner.generate_strike(self.flight, target.location) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/payload/QPylonEditor.py b/qt_ui/windows/mission/flight/payload/QPylonEditor.py index 7652e2d6..87a7c49a 100644 --- a/qt_ui/windows/mission/flight/payload/QPylonEditor.py +++ b/qt_ui/windows/mission/flight/payload/QPylonEditor.py @@ -11,7 +11,7 @@ class QPylonEditor(QComboBox): self.pylon_number = pylon_number self.flight = flight - self.possible_loadout = [i for i in self.pylon.__dict__.keys() if i[:1] != '_'] + self.possible_loadout = [i for i in self.pylon.__dict__.keys() if i[:2] != '__'] if not str(self.pylon_number) in self.flight.loadout.keys(): self.flight.loadout[str(self.pylon_number)] = "" diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py new file mode 100644 index 00000000..6d789585 --- /dev/null +++ b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py @@ -0,0 +1,32 @@ +import datetime + +from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout + +from gen.ato import Package +from gen.flights.flight import Flight +from gen.flights.traveltime import TotEstimator + + +# TODO: Remove? +class QFlightDepartureDisplay(QGroupBox): + + def __init__(self, package: Package, flight: Flight): + super().__init__("Departure") + + layout = QVBoxLayout() + + departure_row = QHBoxLayout() + layout.addLayout(departure_row) + + estimator = TotEstimator(package) + delay = estimator.mission_start_time(flight) + + departure_row.addWidget(QLabel( + f"Departing from {flight.from_cp.name}" + )) + departure_row.addWidget(QLabel(f"At T+{delay}")) + + layout.addWidget(QLabel("Determined based on the package TOT. Edit the " + "package to adjust the TOT.")) + + self.setLayout(layout) diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py deleted file mode 100644 index 25e75e7c..00000000 --- a/qt_ui/windows/mission/flight/settings/QFlightDepartureEditor.py +++ /dev/null @@ -1,30 +0,0 @@ -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox - - -class QFlightDepartureEditor(QGroupBox): - - def __init__(self, flight): - super(QFlightDepartureEditor, self).__init__("Departure") - self.flight = flight - - layout = QHBoxLayout() - self.depart_from = QLabel("Departing from " + self.flight.from_cp.name + "") - self.depart_at_t = QLabel("At T +") - self.minutes = QLabel(" minutes") - - self.departure_delta = QSpinBox(self) - self.departure_delta.setMinimum(0) - self.departure_delta.setMaximum(120) - self.departure_delta.setValue(self.flight.scheduled_in) - self.departure_delta.valueChanged.connect(self.change_scheduled) - - layout.addWidget(self.depart_from) - layout.addWidget(self.depart_at_t) - layout.addWidget(self.departure_delta) - layout.addWidget(self.minutes) - self.setLayout(layout) - - self.changed = self.departure_delta.valueChanged - - def change_scheduled(self): - self.flight.scheduled_in = int(self.departure_delta.value()) diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index 36a72dc4..d6e745e0 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -1,3 +1,5 @@ +import logging + from PySide2.QtCore import Signal from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout @@ -6,32 +8,31 @@ class QFlightSlotEditor(QGroupBox): changed = Signal() - def __init__(self, flight, game, planner): + def __init__(self, flight, game): super(QFlightSlotEditor, self).__init__("Slots") self.flight = flight self.game = game - self.planner = planner - self.available = self.planner.get_available_aircraft() - if self.flight.unit_type not in self.available: - max = self.flight.count - else: - max = self.flight.count + self.available[self.flight.unit_type] - if max > 4: - max = 4 + self.inventory = self.game.aircraft_inventory.for_control_point( + flight.from_cp + ) + available = self.inventory.available(self.flight.unit_type) + max_count = self.flight.count + available + if max_count > 4: + max_count = 4 layout = QGridLayout() self.aircraft_count = QLabel("Aircraft count :") self.aircraft_count_spinner = QSpinBox() self.aircraft_count_spinner.setMinimum(1) - self.aircraft_count_spinner.setMaximum(max) + self.aircraft_count_spinner.setMaximum(max_count) self.aircraft_count_spinner.setValue(flight.count) self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count) self.client_count = QLabel("Client slots count :") self.client_count_spinner = QSpinBox() self.client_count_spinner.setMinimum(0) - self.client_count_spinner.setMaximum(max) + self.client_count_spinner.setMaximum(max_count) self.client_count_spinner.setValue(flight.client_count) self.client_count_spinner.valueChanged.connect(self._changed_client_count) @@ -48,9 +49,23 @@ class QFlightSlotEditor(QGroupBox): self.setLayout(layout) def _changed_aircraft_count(self): + self.game.aircraft_inventory.return_from_flight(self.flight) + old_count = self.flight.count self.flight.count = int(self.aircraft_count_spinner.value()) + try: + self.game.aircraft_inventory.claim_for_flight(self.flight) + except ValueError: + # The UI should have prevented this, but if we ran out of aircraft + # then roll back the inventory change. + difference = self.flight.count - old_count + available = self.inventory.available(self.flight.unit_type) + logging.error( + f"Could not add {difference} additional aircraft to " + f"{self.flight} because {self.flight.from_cp} has only " + f"{available} {self.flight.unit_type} remaining") + self.flight.count = old_count + self.game.aircraft_inventory.claim_for_flight(self.flight) self.changed.emit() - # TODO check if enough aircraft are available def _changed_client_count(self): self.flight.client_count = int(self.client_count_spinner.value()) diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index cabd99cf..f1419669 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -2,29 +2,29 @@ from PySide2.QtCore import Signal from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout from game import Game +from gen.ato import Package from gen.flights.flight import Flight -from qt_ui.windows.mission.flight.settings.QFlightDepartureEditor import QFlightDepartureEditor -from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor -from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType -from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTypeTaskInfo +from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \ + QFlightDepartureDisplay +from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \ + QFlightSlotEditor +from qt_ui.windows.mission.flight.settings.QFlightStartType import \ + QFlightStartType +from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \ + QFlightTypeTaskInfo class QGeneralFlightSettingsTab(QFrame): on_flight_settings_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner): - super(QGeneralFlightSettingsTab, self).__init__() - self.flight = flight - self.game = game - self.planner = planner - self.init_ui() + def __init__(self, game: Game, package: Package, flight: Flight): + super().__init__() - def init_ui(self): layout = QGridLayout() - flight_info = QFlightTypeTaskInfo(self.flight) - flight_departure = QFlightDepartureEditor(self.flight) - flight_slots = QFlightSlotEditor(self.flight, self.game, self.planner) - flight_start_type = QFlightStartType(self.flight) + flight_info = QFlightTypeTaskInfo(flight) + flight_departure = QFlightDepartureDisplay(package, flight) + flight_slots = QFlightSlotEditor(flight, game) + flight_start_type = QFlightStartType(flight) layout.addWidget(flight_info, 0, 0) layout.addWidget(flight_departure, 1, 0) layout.addWidget(flight_slots, 2, 0) @@ -34,6 +34,6 @@ class QGeneralFlightSettingsTab(QFrame): layout.addLayout(vstretch, 3, 0) self.setLayout(layout) - flight_start_type.setEnabled(self.flight.client_count > 0) - flight_slots.changed.connect(lambda: flight_start_type.setEnabled(self.flight.client_count > 0)) - flight_departure.changed.connect(lambda: self.on_flight_settings_changed.emit()) + flight_start_type.setEnabled(flight.client_count > 0) + flight_slots.changed.connect( + lambda: flight_start_type.setEnabled(flight.client_count > 0)) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py index 8deefbc7..381d8e39 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py @@ -1,26 +1,32 @@ +import itertools +from datetime import timedelta + from PySide2.QtCore import QItemSelectionModel, QPoint -from PySide2.QtGui import QStandardItemModel, QStandardItem -from PySide2.QtWidgets import QTableView, QHeaderView +from PySide2.QtGui import QStandardItem, QStandardItemModel +from PySide2.QtWidgets import QHeaderView, QTableView from game.utils import meter_to_feet +from gen.ato import Package from gen.flights.flight import Flight, FlightWaypoint -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \ + QWaypointItem class QFlightWaypointList(QTableView): - def __init__(self, flight: Flight): - super(QFlightWaypointList, self).__init__() + def __init__(self, package: Package, flight: Flight): + super().__init__() + self.package = package + self.flight = flight + self.model = QStandardItemModel(self) self.setModel(self.model) self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.model.setHorizontalHeaderLabels(["Name", "Alt"]) + self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"]) header = self.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeToContents) - self.flight = flight - if len(self.flight.points) > 0: self.selectedPoint = self.flight.points[0] self.update_list() @@ -33,18 +39,48 @@ class QFlightWaypointList(QTableView): def update_list(self): self.model.clear() - self.model.setHorizontalHeaderLabels(["Name", "Alt"]) - takeoff = FlightWaypoint(self.flight.from_cp.position.x, self.flight.from_cp.position.y, 0) + + self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"]) + + # The first waypoint is set up by pydcs at mission generation time, so + # we need to add that waypoint manually. + takeoff = FlightWaypoint(self.flight.from_cp.position.x, + self.flight.from_cp.position.y, 0) takeoff.description = "Take Off" takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name - self.model.appendRow(QWaypointItem(takeoff, 0)) - item = QStandardItem("0 feet AGL") - item.setEditable(False) - self.model.setItem(0, 1, item) - for i, point in enumerate(self.flight.points): - self.model.insertRow(self.model.rowCount()) - self.model.setItem(self.model.rowCount()-1, 0, QWaypointItem(point, i + 1)) - item = QStandardItem(str(meter_to_feet(point.alt)) + " ft " + str(["AGL" if point.alt_type == "RADIO" else "MSL"][0])) - item.setEditable(False) - self.model.setItem(self.model.rowCount()-1, 1, item) - self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select) \ No newline at end of file + takeoff.alt_type = "RADIO" + + waypoints = itertools.chain([takeoff], self.flight.points) + for row, waypoint in enumerate(waypoints): + self.add_waypoint_row(row, self.flight, waypoint) + self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), + QItemSelectionModel.Select) + + def add_waypoint_row(self, row: int, flight: Flight, + waypoint: FlightWaypoint) -> None: + self.model.insertRow(self.model.rowCount()) + + self.model.setItem(row, 0, QWaypointItem(waypoint, row)) + + altitude = meter_to_feet(waypoint.alt) + altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" + altitude_item = QStandardItem(f"{altitude} ft {altitude_type}") + altitude_item.setEditable(False) + self.model.setItem(row, 1, altitude_item) + + tot = self.tot_text(flight, waypoint) + tot_item = QStandardItem(tot) + tot_item.setEditable(False) + self.model.setItem(row, 2, tot_item) + + @staticmethod + def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str: + prefix = "" + time = flight.flight_plan.tot_for_waypoint(waypoint) + if time is None: + prefix = "Depart " + time = flight.flight_plan.depart_time_for_waypoint(waypoint) + if time is None: + return "" + time = timedelta(seconds=int(time.total_seconds())) + return f"{prefix}T+{time}" diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 69870d1c..43198d28 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -1,75 +1,103 @@ +from typing import List, Optional + from PySide2.QtCore import Signal -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLayout +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, +) from game import Game -from gen.flights.flight import Flight -from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator -from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator -from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator -from qt_ui.windows.mission.flight.generator.QSTRIKEMissionGenerator import QSTRIKEMissionGenerator -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import QFlightWaypointList -from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import QPredefinedWaypointSelectionWindow +from gen.ato import Package +from gen.flights.flight import Flight, FlightType +from gen.flights.flightplan import FlightPlanBuilder +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \ + QFlightWaypointList +from qt_ui.windows.mission.flight.waypoints\ + .QPredefinedWaypointSelectionWindow import \ + QPredefinedWaypointSelectionWindow +from theater import FrontLine class QFlightWaypointTab(QFrame): on_flight_changed = Signal() - def __init__(self, game: Game, flight: Flight): + def __init__(self, game: Game, package: Package, flight: Flight): super(QFlightWaypointTab, self).__init__() - self.flight = flight self.game = game - self.planner = self.game.planners[self.flight.from_cp.id] + self.package = package + self.flight = flight + self.planner = FlightPlanBuilder(self.game, package, is_player=True) + + self.flight_waypoint_list: Optional[QFlightWaypointList] = None + self.ascend_waypoint: Optional[QPushButton] = None + self.descend_waypoint: Optional[QPushButton] = None + self.rtb_waypoint: Optional[QPushButton] = None + self.delete_selected: Optional[QPushButton] = None + self.open_fast_waypoint_button: Optional[QPushButton] = None + self.recreate_buttons: List[QPushButton] = [] self.init_ui() def init_ui(self): layout = QGridLayout() - rlayout = QVBoxLayout() - self.flight_waypoint_list = QFlightWaypointList(self.flight) - self.open_fast_waypoint_button = QPushButton("Add Waypoint") - self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) - - self.cas_generator = QPushButton("Gen. CAS") - self.cas_generator.clicked.connect(self.on_cas_generator) - - self.cap_generator = QPushButton("Gen. CAP") - self.cap_generator.clicked.connect(self.on_cap_generator) - - self.sead_generator = QPushButton("Gen. SEAD/DEAD") - self.sead_generator.clicked.connect(self.on_sead_generator) - - self.strike_generator = QPushButton("Gen. STRIKE") - self.strike_generator.clicked.connect(self.on_strike_generator) - - self.rtb_waypoint = QPushButton("Add RTB Waypoint") - self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) - - self.ascend_waypoint = QPushButton("Add Ascend Waypoint") - self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) - - self.descend_waypoint = QPushButton("Add Descend Waypoint") - self.descend_waypoint.clicked.connect(self.on_descend_waypoint) - - self.delete_selected = QPushButton("Delete Selected") - self.delete_selected.clicked.connect(self.on_delete_waypoint) + self.flight_waypoint_list = QFlightWaypointList(self.package, + self.flight) layout.addWidget(self.flight_waypoint_list, 0, 0) + rlayout = QVBoxLayout() + layout.addLayout(rlayout, 0, 1) + rlayout.addWidget(QLabel("Generator :")) rlayout.addWidget(QLabel("AI compatible")) - rlayout.addWidget(self.cas_generator) - rlayout.addWidget(self.cap_generator) - rlayout.addWidget(self.sead_generator) - rlayout.addWidget(self.strike_generator) + + # TODO: Filter by objective type. + self.recreate_buttons.clear() + recreate_types = [ + FlightType.CAS, + FlightType.CAP, + FlightType.DEAD, + FlightType.ESCORT, + FlightType.SEAD, + FlightType.STRIKE + ] + for task in recreate_types: + def make_closure(arg): + def closure(): + return self.confirm_recreate(arg) + return closure + button = QPushButton(f"Recreate as {task.name}") + button.clicked.connect(make_closure(task)) + rlayout.addWidget(button) + self.recreate_buttons.append(button) + rlayout.addWidget(QLabel("Advanced : ")) rlayout.addWidget(QLabel("Do not use for AI flights")) + + self.ascend_waypoint = QPushButton("Add Ascend Waypoint") + self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) rlayout.addWidget(self.ascend_waypoint) + + self.descend_waypoint = QPushButton("Add Descend Waypoint") + self.descend_waypoint.clicked.connect(self.on_descend_waypoint) rlayout.addWidget(self.descend_waypoint) + + self.rtb_waypoint = QPushButton("Add RTB Waypoint") + self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) rlayout.addWidget(self.rtb_waypoint) - rlayout.addWidget(self.open_fast_waypoint_button) + + self.delete_selected = QPushButton("Delete Selected") + self.delete_selected.clicked.connect(self.on_delete_waypoint) rlayout.addWidget(self.delete_selected) + + self.open_fast_waypoint_button = QPushButton("Add Waypoint") + self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) + rlayout.addWidget(self.open_fast_waypoint_button) rlayout.addStretch() - layout.addLayout(rlayout, 0, 1) self.setLayout(layout) def on_delete_waypoint(self): @@ -85,42 +113,50 @@ class QFlightWaypointTab(QFrame): self.subwindow.show() def on_ascend_waypoint(self): - ascend = self.planner.generate_ascend_point(self.flight.from_cp) + ascend = self.planner.generate_ascend_point(self.flight, + self.flight.from_cp) self.flight.points.append(ascend) self.flight_waypoint_list.update_list() self.on_change() def on_rtb_waypoint(self): - rtb = self.planner.generate_rtb_waypoint(self.flight.from_cp) + rtb = self.planner.generate_rtb_waypoint(self.flight, + self.flight.from_cp) self.flight.points.append(rtb) self.flight_waypoint_list.update_list() self.on_change() def on_descend_waypoint(self): - descend = self.planner.generate_descend_point(self.flight.from_cp) + descend = self.planner.generate_descend_point(self.flight, + self.flight.from_cp) self.flight.points.append(descend) self.flight_waypoint_list.update_list() self.on_change() - def on_cas_generator(self): - self.subwindow = QCASMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_cap_generator(self): - self.subwindow = QCAPMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_sead_generator(self): - self.subwindow = QSEADMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_strike_generator(self): - self.subwindow = QSTRIKEMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() + def confirm_recreate(self, task: FlightType) -> None: + result = QMessageBox.question( + self, + "Regenerate flight?", + ("Changing the flight type will reset its flight plan. Do you want " + "to continue?"), + QMessageBox.No, + QMessageBox.Yes + ) + if result == QMessageBox.Yes: + # TODO: Should be buttons for both BARCAP and TARCAP. + # BARCAP and TARCAP behave differently. TARCAP arrives a few minutes + # ahead of the rest of the package and stays until the package + # departs, whereas BARCAP usually isn't part of a strike package and + # has a fixed mission time. + if task == FlightType.CAP: + if isinstance(self.package.target, FrontLine): + task = FlightType.TARCAP + else: + task = FlightType.BARCAP + self.flight.flight_type = task + self.planner.populate_flight_plan(self.flight) + self.flight_waypoint_list.update_list() + self.on_change() def on_change(self): self.flight_waypoint_list.update_list() diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 06c42d9f..617869bc 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -1,48 +1,64 @@ +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import List + from PySide2 import QtGui -from PySide2.QtCore import QSize, QItemSelectionModel -from PySide2.QtGui import QStandardItemModel, QStandardItem -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtCore import QItemSelectionModel +from PySide2.QtGui import QStandardItem, QStandardItemModel +from PySide2.QtWidgets import QAbstractItemView, QListView -from theater import caucasus, nevada, persiangulf, normandy, thechannel, syria import qt_ui.uiconstants as CONST +from theater import ConflictTheater -CAMPAIGNS = [ - ("Caucasus - Western Georgia", caucasus.WesternGeorgia, "Terrain_Caucasus"), - ("Caucasus - Russia Small", caucasus.RussiaSmall, "Terrain_Caucasus"), - ("Caucasus - North Caucasus", caucasus.NorthCaucasus, "Terrain_Caucasus"), - ("Caucasus - Full Map", caucasus.CaucasusTheater, "Terrain_Caucasus"), - ("Nevada - North Nevada", nevada.NevadaTheater, "Terrain_Nevada"), - ("Persian Gulf - Invasion of Iran", persiangulf.IranianCampaign, "Terrain_Persian_Gulf"), - ("Persian Gulf - Invasion of Iran [Lite]", persiangulf.IranInvasionLite, "Terrain_Persian_Gulf"), - ("Persian Gulf - Emirates", persiangulf.Emirates, "Terrain_Persian_Gulf"), - ("Persian Gulf - Desert War", persiangulf.DesertWar, "Terrain_Persian_Gulf"), - ("Persian Gulf - Full Map", persiangulf.PersianGulfTheater, "Terrain_Persian_Gulf"), - ("Syria - Golan heights battle", syria.GolanHeights, "Terrain_Syria"), - ("Syria - Invasion from Turkey", syria.TurkishInvasion, "Terrain_Syria"), - ("Syria - Syrian Civil War", syria.SyrianCivilWar, "Terrain_Syria"), - ("Syria - Inherent Resolve", syria.InherentResolve, "Terrain_Syria"), - ("Syria - Full Map", syria.SyriaFullMap, "Terrain_Syria"), +@dataclass(frozen=True) +class Campaign: + name: str + icon_name: str + authors: str + description: str + theater: ConflictTheater - ("Normandy - Normandy", normandy.NormandyTheater, "Terrain_Normandy"), - ("Normandy - Normandy Small", normandy.NormandySmall, "Terrain_Normandy"), - ("The Channel - Battle of Britain", thechannel.BattleOfBritain, "Terrain_Channel"), - ("The Channel - Dunkirk", thechannel.Dunkirk, "Terrain_Channel"), -] + @classmethod + def from_json(cls, path: Path) -> Campaign: + with path.open() as campaign_file: + data = json.load(campaign_file) + + sanitized_theater = data["theater"].replace(" ", "") + return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"), + data.get("description", ""), ConflictTheater.from_json(data)) + + +def load_campaigns() -> List[Campaign]: + campaign_dir = Path("resources\\campaigns") + campaigns = [] + for path in campaign_dir.iterdir(): + try: + logging.debug(f"Loading campaign from {path}...") + campaign = Campaign.from_json(path) + campaigns.append(campaign) + except RuntimeError: + logging.exception(f"Unable to load campaign from {path}") + + return sorted(campaigns, key=lambda x: x.name) class QCampaignItem(QStandardItem): - def __init__(self, text, theater, icon): + def __init__(self, campaign: Campaign) -> None: super(QCampaignItem, self).__init__() - self.theater = theater - self.setIcon(QtGui.QIcon(CONST.ICONS[icon])) + self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name])) self.setEditable(False) - self.setText(text) + self.setText(campaign.name) + class QCampaignList(QListView): - def __init__(self): + def __init__(self, campaigns: List[Campaign]) -> None: super(QCampaignList, self).__init__() self.model = QStandardItemModel(self) self.setModel(self.model) @@ -50,12 +66,12 @@ class QCampaignList(QListView): self.setMinimumHeight(350) self.campaigns = [] self.setSelectionBehavior(QAbstractItemView.SelectItems) - self.setup_content() + self.setup_content(campaigns) - def setup_content(self): - for i, campaign in enumerate(CAMPAIGNS): + def setup_content(self, campaigns: List[Campaign]) -> None: + for campaign in campaigns: self.campaigns.append(campaign) - item = QCampaignItem(*campaign) + item = QCampaignItem(campaign) self.model.appendRow(item) self.setSelectedCampaign(0) self.repaint() diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index cba58371..e82357dc 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -1,28 +1,42 @@ from __future__ import unicode_literals -import datetime import logging +from typing import List, Optional from PySide2 import QtGui, QtWidgets -from PySide2.QtCore import QPoint, QItemSelectionModel -from PySide2.QtWidgets import QHBoxLayout, QVBoxLayout -from dcs.task import CAP, CAS +from PySide2.QtCore import QItemSelectionModel, QPoint, Qt +from PySide2.QtWidgets import QVBoxLayout, QTextEdit +from jinja2 import Environment, FileSystemLoader, select_autoescape -import qt_ui.uiconstants as CONST -from game import db, Game +from game import db from game.settings import Settings -from gen import namegen -from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS -from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel +from qt_ui.windows.newgame.QCampaignList import ( + Campaign, + QCampaignList, + load_campaigns, +) +from theater.start_generator import GameGenerator +jinja_env = Environment( + loader=FileSystemLoader("resources/ui/templates"), + autoescape=select_autoescape( + disabled_extensions=("",), + default_for_string=True, + default=True, + ), + trim_blocks=True, + lstrip_blocks=True, +) class NewGameWizard(QtWidgets.QWizard): def __init__(self, parent=None): super(NewGameWizard, self).__init__(parent) + self.campaigns = load_campaigns() + self.addPage(IntroPage()) self.addPage(FactionSelection()) - self.addPage(TheaterConfiguration()) + self.addPage(TheaterConfiguration(self.campaigns)) self.addPage(MiscOptions()) self.addPage(ConclusionPage()) @@ -34,7 +48,6 @@ class NewGameWizard(QtWidgets.QWizard): self.generatedGame = None def accept(self): - logging.info("New Game Wizard accept") logging.info("======================") @@ -43,8 +56,9 @@ class NewGameWizard(QtWidgets.QWizard): selectedCampaign = self.field("selectedCampaign") if selectedCampaign is None: - selectedCampaign = CAMPAIGNS[0] - conflictTheater = selectedCampaign[1]() + selectedCampaign = self.campaigns[0] + + conflictTheater = selectedCampaign.theater timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] midGame = self.field("midGame") @@ -55,6 +69,7 @@ class NewGameWizard(QtWidgets.QWizard): no_player_navy = self.field("no_player_navy") no_enemy_navy = self.field("no_enemy_navy") invertMap = self.field("invertMap") + starting_money = int(self.field("starting_money")) player_name = blueFaction enemy_name = redFaction @@ -67,43 +82,13 @@ class NewGameWizard(QtWidgets.QWizard): settings.do_not_generate_player_navy = no_player_navy settings.do_not_generate_enemy_navy = no_enemy_navy - self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier, - timePeriod, settings) + generator = GameGenerator(player_name, enemy_name, conflictTheater, + settings, timePeriod, starting_money, + multiplier, midGame) + self.generatedGame = generator.generate() super(NewGameWizard, self).accept() - def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater, - midgame: bool, multiplier: float, period: datetime, settings:Settings): - - # Reset name generator - namegen.reset() - start_generator.prepare_theater(conflictTheater, settings, midgame) - - print("-- Starting New Game Generator") - print("Enemy name : " + enemy_name) - print("Player name : " + player_name) - print("Midgame : " + str(midgame)) - start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier) - - print("-- Initial units generated") - game = Game(player_name=player_name, - enemy_name=enemy_name, - theater=conflictTheater, - start_date=period, - settings=settings) - - print("-- Game Object generated") - start_generator.generate_groundobjects(conflictTheater, game) - game.budget = int(game.budget * multiplier) - game.settings.multiplier = multiplier - game.settings.sams = True - game.settings.version = CONST.VERSION_STRING - - if midgame: - game.budget = game.budget * 4 * len(list(conflictTheater.conflicts())) - - return game - class IntroPage(QtWidgets.QWizardPage): def __init__(self, parent=None): @@ -135,7 +120,9 @@ class FactionSelection(QtWidgets.QWizardPage): # Factions selection self.factionsGroup = QtWidgets.QGroupBox("Factions") - self.factionsGroupLayout = QtWidgets.QGridLayout() + self.factionsGroupLayout = QtWidgets.QHBoxLayout() + self.blueGroupLayout = QtWidgets.QGridLayout() + self.redGroupLayout = QtWidgets.QGridLayout() blueFaction = QtWidgets.QLabel("Player Faction :") self.blueFactionSelect = QtWidgets.QComboBox() @@ -145,26 +132,33 @@ class FactionSelection(QtWidgets.QWizardPage): redFaction = QtWidgets.QLabel("Enemy Faction :") self.redFactionSelect = QtWidgets.QComboBox() - for i, r in enumerate(db.FACTIONS): - self.redFactionSelect.addItem(r) - if r == "Russia 1990": # Default ennemy - self.redFactionSelect.setCurrentIndex(i) redFaction.setBuddy(self.redFactionSelect) - self.blueSideRecap = QtWidgets.QLabel("") - self.blueSideRecap.setFont(CONST.FONT_PRIMARY_I) - self.blueSideRecap.setWordWrap(True) + # Faction description + self.blueFactionDescription = QTextEdit("") + self.blueFactionDescription.setReadOnly(True) - self.redSideRecap = QtWidgets.QLabel("") - self.redSideRecap.setFont(CONST.FONT_PRIMARY_I) - self.redSideRecap.setWordWrap(True) + self.redFactionDescription = QTextEdit("") + self.redFactionDescription.setReadOnly(True) - self.factionsGroupLayout.addWidget(blueFaction, 0, 0) - self.factionsGroupLayout.addWidget(self.blueFactionSelect, 0, 1) - self.factionsGroupLayout.addWidget(self.blueSideRecap, 1, 0, 1, 2) - self.factionsGroupLayout.addWidget(redFaction, 2, 0) - self.factionsGroupLayout.addWidget(self.redFactionSelect, 2, 1) - self.factionsGroupLayout.addWidget(self.redSideRecap, 3, 0, 1, 2) + # Setup default selected factions + for i, r in enumerate(db.FACTIONS): + self.redFactionSelect.addItem(r) + if r == "Russia 1990": + self.redFactionSelect.setCurrentIndex(i) + if r == "USA 2005": + self.blueFactionSelect.setCurrentIndex(i) + + self.blueGroupLayout.addWidget(blueFaction, 0, 0) + self.blueGroupLayout.addWidget(self.blueFactionSelect, 0, 1) + self.blueGroupLayout.addWidget(self.blueFactionDescription, 1, 0, 1, 2) + + self.redGroupLayout.addWidget(redFaction, 0, 0) + self.redGroupLayout.addWidget(self.redFactionSelect, 0, 1) + self.redGroupLayout.addWidget(self.redFactionDescription, 1, 0, 1, 2) + + self.factionsGroupLayout.addLayout(self.blueGroupLayout) + self.factionsGroupLayout.addLayout(self.redGroupLayout) self.factionsGroup.setLayout(self.factionsGroupLayout) # Create required mod layout @@ -190,39 +184,34 @@ class FactionSelection(QtWidgets.QWizardPage): def updateUnitRecap(self): - self.requiredMods.setText("
Experience the Battle of Britain on the Channel map !
Note: It is not possible to cross the channel to capture enemy bases yet, but you can consider you won if you manage to destroy all the ennemy targets
", + "player_points": [ + { + "type": "airbase", + "id": "Hawkinge", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lympne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Manston", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "High Halden", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Dunkirk Mardyck", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Saint Omer Longuenesse", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Merville Calonne", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Abbeville Drucat", + "size": 600, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Hawkinge", + "Lympne" + ], + [ + "Hawkinge", + "Manston" + ], + [ + "High Halden", + "Lympne" + ], + [ + "Dunkirk Mardyck", + "Saint Omer Longuenesse" + ], + [ + "Merville Calonne", + "Saint Omer Longuenesse" + ], + [ + "Abbeville Drucat", + "Saint Omer Longuenesse" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json new file mode 100644 index 00000000..b8a1e150 --- /dev/null +++ b/resources/campaigns/desert_war.json @@ -0,0 +1,63 @@ +{ + "name": "Persian Gulf - Desert War", + "theater": "Persian Gulf", + "authors": "Khopa", + "description": "This is a simple scenario in the Desert near Dubai and Abu-Dhabi. Progress from Liwa airbase to Al-Minhad.
This scenario shouldn't require too much performance.
", + "player_points": [ + { + "type": "airbase", + "id": "Liwa Airbase", + "size": 2000, + "importance": 1.2 + }, + { + "type": "lha", + "id": 1002, + "x": -164000, + "y": -257000, + "captured_invert": true + }, + { + "type": "carrier", + "id": 1001, + "x": -124000, + "y": -303000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Al Ain International Airport", + "size": 2000, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Al Maktoum Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Minhad AB", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Al Ain International Airport", + "Liwa Airbase" + ], + [ + "Al Ain International Airport", + "Al Maktoum Intl" + ], + [ + "Al Maktoum Intl", + "Al Minhad AB" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json new file mode 100644 index 00000000..09a70d2d --- /dev/null +++ b/resources/campaigns/dunkirk.json @@ -0,0 +1,79 @@ +{ + "name": "The Channel - Dunkirk", + "theater": "The Channel", + "authors": "Khopa", + "description": "In this scenario, your forces starts in Dunkirk and can be supported by the airfields on the other side of the Channel.
If you select the inverted configuration, you can play a German invasion of England.
Note: B-17 should be operated from Manston airfield
", + "player_points": [ + { + "type": "airbase", + "id": "Hawkinge", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lympne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Manston", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Dunkirk Mardyck", + "size": 600, + "importance": 1, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Saint Omer Longuenesse", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Merville Calonne", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Abbeville Drucat", + "size": 600, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Hawkinge", + "Lympne" + ], + [ + "Hawkinge", + "Manston" + ], + [ + "Dunkirk Mardyck", + "Saint Omer Longuenesse" + ], + [ + "Merville Calonne", + "Saint Omer Longuenesse" + ], + [ + "Abbeville Drucat", + "Saint Omer Longuenesse" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/emirates.json b/resources/campaigns/emirates.json new file mode 100644 index 00000000..147295e3 --- /dev/null +++ b/resources/campaigns/emirates.json @@ -0,0 +1,108 @@ +{ + "name": "Persian Gulf - Emirates", + "theater": "Persian Gulf", + "authors": "Khopa", + "description": "In this scenario, you can play an invasion of the Emirates and Oman, where your forces starts in Fujairah.
Note: Fujairah airfield has very few slots for aircrafts, so it recommended to operate from carriers at the start of the campaign. Thus, a carrier-capable faction is recommended.
", + "player_points": [ + { + "type": "airbase", + "id": "Fujairah Intl", + "radials": [ + 180, + 225, + 270, + 315, + 0 + ], + "size": 1000, + "importance": 1, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -79770, + "y": 49430, + "captured_invert": true + }, + { + "type": "carrier", + "id": 1001, + "x": -61770, + "y": 69039, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Al Dhafra AB", + "size": 2000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Ain International Airport", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Maktoum Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Minhad AB", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sharjah Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Ras Al Khaimah", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Al Ain International Airport", + "Al Dhafra AB" + ], + [ + "Al Dhafra AB", + "Al Maktoum Intl" + ], + [ + "Al Ain International Airport", + "Fujairah Intl" + ], + [ + "Al Ain International Airport", + "Al Maktoum Intl" + ], + [ + "Al Maktoum Intl", + "Al Minhad AB" + ], + [ + "Al Minhad AB", + "Sharjah Intl" + ], + [ + "Ras Al Khaimah", + "Sharjah Intl" + ], + [ + "Fujairah Intl", + "Sharjah Intl" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/full_caucasus.json b/resources/campaigns/full_caucasus.json new file mode 100644 index 00000000..ce4d3858 --- /dev/null +++ b/resources/campaigns/full_caucasus.json @@ -0,0 +1,168 @@ +{ + "name": "Caucasus - Full Map", + "theater": "Caucasus", + "authors": "george", + "description": "Full map of the Caucasus
Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.
", + "player_points": [ + { + "type": "airbase", + "id": "Kobuleti", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Senaki-Kolkhi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": -304708, + "y": 552839, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Beslan", + "size": 1000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Nalchik", + "size": 1000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mozdok", + "size": 2000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mineralnye Vody", + "size": 2000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Sukhumi-Babushara", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Gudauta", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Sochi-Adler", + "size": 2000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Gelendzhik", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Vaziani", + "size": 2000, + "importance": 1.3 + } + ], + "links": [ + [ + "Kutaisi", + "Vaziani" + ], + [ + "Beslan", + "Vaziani" + ], + [ + "Beslan", + "Mozdok" + ], + [ + "Beslan", + "Nalchik" + ], + [ + "Mozdok", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ], + [ + "Maykop-Khanskaya", + "Gelendzhik" + ], + [ + "Gelendzhik", + "Sochi-Adler" + ], + [ + "Gudauta", + "Sochi-Adler" + ], + [ + "Gudauta", + "Sukhumi-Babushara" + ], + [ + "Senaki-Kolkhi", + "Sukhumi-Babushara" + ], + [ + "Kutaisi", + "Senaki-Kolkhi" + ], + [ + "Senaki-Kolkhi", + "Kobuleti" + ], + [ + "Kobuleti", + "Kutaisi" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/golan_heights_battle.json b/resources/campaigns/golan_heights_battle.json new file mode 100644 index 00000000..6afd303f --- /dev/null +++ b/resources/campaigns/golan_heights_battle.json @@ -0,0 +1,83 @@ +{ + "name": "Syria - Golan heights battle", + "theater": "Syria", + "authors": "Khopa", + "description": "In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.
", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -280000, + "y": -238000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -237000, + "y": -89800, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Mezzeh", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2, + "captured_invert": true + } + ], + "links": [ + [ + "Khalkhalah", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Khalkhalah", + "Marj Ruhayyil" + ], + [ + "Marj Ruhayyil", + "Mezzeh" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json new file mode 100644 index 00000000..fc5969a5 --- /dev/null +++ b/resources/campaigns/inherent_resolve.json @@ -0,0 +1,84 @@ +{ + "name": "Syria - Inherent Resolve", + "theater": "Syria", + "authors": "Khopa", + "description": "In this scenario, you start from Jordan, and have to fight your way through eastern Syria.
", + "player_points": [ + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4, + "captured_invert": true + }, + { + "type": "carrier", + "id": 1001, + "x": -210000, + "y": -200000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -131000, + "y": -161000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Incirlik", + "Incirlik" + ], + [ + "Khalkhalah", + "Palmyra" + ], + [ + "Palmyra", + "Tabqa" + ], + [ + "Jirah", + "Tabqa" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json new file mode 100644 index 00000000..41cd39a2 --- /dev/null +++ b/resources/campaigns/invasion_from_turkey.json @@ -0,0 +1,87 @@ +{ + "name": "Syria - Invasion from Turkey", + "theater": "Syria", + "authors": "Khopa", + "description": "In this scenario, you start from Turkey and have to invade territories in northern Syria.
", + "player_points": [ + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Hatay", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 133000, + "y": -54000 + }, + { + "type": "lha", + "id": 1002, + "x": 155000, + "y": -19000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Minakh", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Kuweires", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Hatay", + "Minakh" + ], + [ + "Aleppo", + "Minakh" + ], + [ + "Aleppo", + "Kuweires" + ], + [ + "Jirah", + "Kuweires" + ], + [ + "Jirah", + "Tabqa" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json new file mode 100644 index 00000000..248356fb --- /dev/null +++ b/resources/campaigns/invasion_of_iran.json @@ -0,0 +1,143 @@ +{ + "name": "Persian Gulf - Invasion of Iran", + "theater": "Persian Gulf", + "authors": "Khopa", + "description": "In this scenario, you start in Bandar Abbas, and must invade Iran.
", + "player_points": [ + { + "type": "airbase", + "id": "Ras Al Khaimah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Khasab", + "size": 600, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Qeshm Island", + "radials": [ + 270, + 315, + 0, + 45, + 90, + 135, + 180 + ], + "size": 600, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Havadarya", + "radials": [ + 225, + 270, + 315, + 0, + 45 + ], + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Bandar Abbas Intl", + "size": 2000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 59514.324335475, + "y": 28165.517980635 + }, + { + "type": "lha", + "id": 1002, + "x": -27500.813952358, + "y": -147000.65947136 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Bandar Lengeh", + "radials": [ + 270, + 315, + 0, + 45 + ], + "size": 600, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Shiraz International Airport", + "size": 2000, + "importance": 1.4, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Jiroft Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Kerman Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Lar Airbase", + "size": 1000, + "importance": 1.4 + } + ], + "links": [ + [ + "Khasab", + "Ras Al Khaimah" + ], + [ + "Bandar Lengeh", + "Lar Airbase" + ], + [ + "Havadarya", + "Lar Airbase" + ], + [ + "Bandar Abbas Intl", + "Havadarya" + ], + [ + "Bandar Abbas Intl", + "Jiroft Airport" + ], + [ + "Lar Airbase", + "Shiraz International Airport" + ], + [ + "Kerman Airport", + "Shiraz International Airport" + ], + [ + "Jiroft Airport", + "Kerman Airport" + ], + [ + "Kerman Airport", + "Lar Airbase" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json new file mode 100644 index 00000000..840fb55c --- /dev/null +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -0,0 +1,77 @@ +{ + "name": "Persian Gulf - Invasion of Iran [Lite]", + "theater": "Persian Gulf", + "authors": "Khopa", + "description": "This is lighter version of the invasion of Iran scenario.
", + "player_points": [ + { + "type": "airbase", + "id": "Bandar Lengeh", + "radials": [ + 270, + 315, + 0, + 45 + ], + "size": 600, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 72000.324335475, + "y": -376000 + }, + { + "type": "lha", + "id": 1002, + "x": -27500.813952358, + "y": -147000.65947136 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Shiraz International Airport", + "size": 2000, + "importance": 1.4, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Jiroft Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Kerman Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Lar Airbase", + "size": 1000, + "importance": 1.4 + } + ], + "links": [ + [ + "Bandar Lengeh", + "Lar Airbase" + ], + [ + "Lar Airbase", + "Shiraz International Airport" + ], + [ + "Kerman Airport", + "Shiraz International Airport" + ], + [ + "Jiroft Airport", + "Kerman Airport" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json new file mode 100644 index 00000000..b656709b --- /dev/null +++ b/resources/campaigns/normandy.json @@ -0,0 +1,85 @@ +{ + "name": "Normandy - Normandy", + "theater": "Normandy", + "authors": "Khopa", + "description": "Normandy 1944 D-Day scenario.
", + "player_points": [ + { + "type": "airbase", + "id": "Chailey", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Needs Oar Point", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Deux Jumeaux", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Lignerolles", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lessay", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Carpiquet", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Maupertus", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Evreux", + "size": 600, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Chailey", + "Needs Oar Point" + ], + [ + "Deux Jumeaux", + "Lignerolles" + ], + [ + "Lessay", + "Lignerolles" + ], + [ + "Carpiquet", + "Lignerolles" + ], + [ + "Lessay", + "Maupertus" + ], + [ + "Carpiquet", + "Evreux" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json new file mode 100644 index 00000000..3bbee783 --- /dev/null +++ b/resources/campaigns/normandy_small.json @@ -0,0 +1,55 @@ +{ + "name": "Normandy - Normandy Small", + "theater": "Normandy", + "authors": "Khopa", + "description": "A lighter version of the Normandy 1944 D-Day scenario.
", + "player_points": [ + { + "type": "airbase", + "id": "Needs Oar Point", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Deux Jumeaux", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Lignerolles", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Carpiquet", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Evreux", + "size": 600, + "importance": 1, + "captured_invert": true + } + ], + "links": [ + [ + "Deux Jumeaux", + "Lignerolles" + ], + [ + "Carpiquet", + "Lignerolles" + ], + [ + "Carpiquet", + "Evreux" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_caucasus.json b/resources/campaigns/north_caucasus.json new file mode 100644 index 00000000..08f60007 --- /dev/null +++ b/resources/campaigns/north_caucasus.json @@ -0,0 +1,101 @@ +{ + "name": "Caucasus - North Caucasus", + "theater": "Caucasus", + "authors": "Khopa", + "description": "In this scenario you will have to fight in the moutain of Caucasus
Note: Running CAS in the moutains can be a bit difficult.
", + "player_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Vaziani", + "size": 600, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Beslan", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Nalchik", + "size": 1000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mozdok", + "size": 2000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mineralnye Vody", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "Kutaisi", + "Vaziani" + ], + [ + "Beslan", + "Vaziani" + ], + [ + "Beslan", + "Mozdok" + ], + [ + "Beslan", + "Nalchik" + ], + [ + "Mozdok", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_nevada.json b/resources/campaigns/north_nevada.json new file mode 100644 index 00000000..86521fc2 --- /dev/null +++ b/resources/campaigns/north_nevada.json @@ -0,0 +1,73 @@ +{ + "name": "Nevada - North Nevada", + "theater": "Nevada", + "authors": "Khopa", + "description": "A scenario taking place in the NTTR. Here you can simulate a red flag exercise.
", + "player_points": [ + { + "type": "airbase", + "id": "Nellis AFB", + "size": 2000, + "importance": 1.4 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Tonopah Test Range Airfield", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Lincoln County", + "size": 600, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Groom Lake AFB", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Creech AFB", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Mesquite", + "size": 1000, + "importance": 1.3 + } + ], + "links": [ + [ + "Lincoln County", + "Tonopah Test Range Airfield" + ], + [ + "Groom Lake AFB", + "Tonopah Test Range Airfield" + ], + [ + "Lincoln County", + "Mesquite" + ], + [ + "Groom Lake AFB", + "Mesquite" + ], + [ + "Creech AFB", + "Groom Lake AFB" + ], + [ + "Creech AFB", + "Nellis AFB" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json new file mode 100644 index 00000000..d79c4d92 --- /dev/null +++ b/resources/campaigns/russia_small.json @@ -0,0 +1,39 @@ +{ + "name": "Caucasus - Russia Small", + "theater": "Caucasus", + "authors": "Khopa", + "description": "A small theater in Russia, progress from Mozdok to Maykop.
This scenario is pretty simple, it is ideal if you want to run a short campaign. If your PC is not powerful, this is also the less performance heavy scenario.
", + "player_points": [ + { + "type": "airbase", + "id": "Mozdok", + "size": 2000, + "importance": 1.1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Mineralnye Vody", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/syria_full_map.json b/resources/campaigns/syria_full_map.json new file mode 100644 index 00000000..aa45dae0 --- /dev/null +++ b/resources/campaigns/syria_full_map.json @@ -0,0 +1,185 @@ +{ + "name": "Syria - Full Map", + "theater": "Syria", + "authors": "Khopa", + "description": "Full map of Syria
Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.
", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -151000, + "y": -106000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -131000, + "y": -161000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Rene Mouawad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Minakh", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Hatay", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "King Hussein Air College", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Al-Dumayr", + "Khalkhalah" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Palmyra" + ], + [ + "Al Qusayr", + "Rene Mouawad" + ], + [ + "Bassel Al-Assad", + "Rene Mouawad" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hatay" + ], + [ + "Palmyra", + "Tabqa" + ], + [ + "Jirah", + "Tabqa" + ], + [ + "Aleppo", + "Jirah" + ], + [ + "Aleppo", + "Minakh" + ], + [ + "Hatay", + "Minakh" + ], + [ + "Incirlik", + "Minakh" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/syrian_civil_war.json b/resources/campaigns/syrian_civil_war.json new file mode 100644 index 00000000..0ca59b44 --- /dev/null +++ b/resources/campaigns/syrian_civil_war.json @@ -0,0 +1,93 @@ +{ + "name": "Syria - Syrian Civil War", + "theater": "Syria", + "authors": "Khopa", + "description": "This scenario can be used to simulate parts of the Syrian Civil War.
", + "player_points": [ + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": 18537, + "y": -52000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": 116000, + "y": -30000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + } + ], + "links": [ + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Palmyra" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/western_georgia.json b/resources/campaigns/western_georgia.json new file mode 100644 index 00000000..df1c755d --- /dev/null +++ b/resources/campaigns/western_georgia.json @@ -0,0 +1,113 @@ +{ + "name": "Caucasus - Western Georgia", + "theater": "Caucasus", + "authors": "Khopa", + "description": "A medium sized theater in the south west of the Caucasus map. Shouldn't be too hard on performance.
", + "player_points": [ + { + "type": "airbase", + "id": "Kobuleti", + "radials": [ + 0, + 45, + 90, + 135, + 180, + 225, + 315 + ], + "size": 600, + "importance": 1.1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Senaki-Kolkhi", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sukhumi-Babushara", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Gudauta", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Sochi-Adler", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 2000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "Kutaisi", + "Senaki-Kolkhi" + ], + [ + "Kobuleti", + "Senaki-Kolkhi" + ], + [ + "Senaki-Kolkhi", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sochi-Adler" + ] + ] +} \ No newline at end of file diff --git a/resources/caulandmap.p b/resources/caulandmap.p index 68f6806f..5736275a 100644 Binary files a/resources/caulandmap.p and b/resources/caulandmap.p differ diff --git a/resources/channellandmap.p b/resources/channellandmap.p index c0fe728e..649ad174 100644 Binary files a/resources/channellandmap.p and b/resources/channellandmap.p differ diff --git a/resources/customized_payloads/B-1B.lua b/resources/customized_payloads/B-1B.lua new file mode 100644 index 00000000..a637ddb4 --- /dev/null +++ b/resources/customized_payloads/B-1B.lua @@ -0,0 +1,96 @@ +local unitPayloads = { + ["name"] = "B-1B", + ["payloads"] = { + [1] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [2] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "B-1B_Mk-84*8", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "B-1B_Mk-84*8", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "B-1B_Mk-84*8", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 34, + [2] = 32, + }, + }, + [3] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{AABA1A14-78A1-4E85-94DD-463CF75BD9E4}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [4] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 33, + }, + }, + [5] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "CBU97*10", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "CBU97*10", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "CBU97*10", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + }, + ["unitType"] = "B-1B", +} +return unitPayloads diff --git a/resources/customized_payloads/B-52H.lua b/resources/customized_payloads/B-52H.lua new file mode 100644 index 00000000..20c9958f --- /dev/null +++ b/resources/customized_payloads/B-52H.lua @@ -0,0 +1,82 @@ +local unitPayloads = { + ["name"] = "B-52H", + ["payloads"] = { + [1] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{45447F82-01B5-4029-A572-9AAD28AF0275}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{8DCAF3A3-7FCF-41B8-BB88-58DEDA878EDE}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{45447F82-01B5-4029-A572-9AAD28AF0275}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [2] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{46ACDCF8-5451-4E26-BDDB-E78D5830E93C}", + ["num"] = 2, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{696CFFC4-0BDE-42A8-BE4B-0BE3D9DD723C}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{6C47D097-83FF-4FB2-9496-EAB36DDF0B05}", + ["num"] = 2, + }, + [3] = { + ["CLSID"] = "{696CFFC4-0BDE-42A8-BE4B-0BE3D9DD723C}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 32, + [2] = 34, + }, + }, + [4] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + }, + }, + [5] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{4CD2BB0F-5493-44EF-A927-9760350F7BA1}", + ["num"] = 3, + }, + [2] = { + ["CLSID"] = "{4CD2BB0F-5493-44EF-A927-9760350F7BA1}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["unitType"] = "B-52H", +} +return unitPayloads diff --git a/resources/customized_payloads/F-117A.lua b/resources/customized_payloads/F-117A.lua new file mode 100644 index 00000000..503ed662 --- /dev/null +++ b/resources/customized_payloads/F-117A.lua @@ -0,0 +1,79 @@ +local unitPayloads = { + ["name"] = "F-117A", + ["payloads"] = { + [1] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 2, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [2] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 2, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [3] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 2, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 1, + }, + [2] = { + ["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}", + ["num"] = 2, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [5] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 33, + }, + }, + }, + ["unitType"] = "F-117A", +} +return unitPayloads diff --git a/resources/customized_payloads/Su-57.lua b/resources/customized_payloads/Su-57.lua new file mode 100644 index 00000000..fd4f7479 --- /dev/null +++ b/resources/customized_payloads/Su-57.lua @@ -0,0 +1,289 @@ +local unitPayloads = { + ["name"] = "Su-57", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 12, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 11, + }, + [4] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 2, + }, + [5] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 6, + }, + [7] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 8, + }, + [8] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 5, + }, + [9] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 3, + }, + [11] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 9, + }, + [12] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 12, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 3, + }, + [5] = { + ["CLSID"] = "{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}", + ["num"] = 11, + }, + [6] = { + ["CLSID"] = "{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{40AB87E8-BEFB-4D85-90D9-B2753ACF9514}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 8, + }, + [9] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 5, + }, + [10] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 6, + }, + [11] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 7, + }, + [12] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 12, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 3, + }, + [5] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 11, + }, + [6] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 8, + }, + [9] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 5, + }, + [10] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 6, + }, + [11] = { + ["CLSID"] = "{KH_59MK2}", + ["num"] = 7, + }, + [12] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 12, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{B4FC81C9-B861-4E87-BBDC-A1158E648EBF}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 3, + }, + [5] = { + ["CLSID"] = "{B4FC81C9-B861-4E87-BBDC-A1158E648EBF}", + ["num"] = 11, + }, + [6] = { + ["CLSID"] = "{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{F72F47E5-C83A-4B85-96ED-D3E46671EE9A}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 8, + }, + [9] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 5, + }, + [10] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 6, + }, + [11] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 7, + }, + [12] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 12, + }, + [2] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{3C612111-C7AD-476E-8A8E-2485812F4E5C}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 3, + }, + [5] = { + ["CLSID"] = "{3C612111-C7AD-476E-8A8E-2485812F4E5C}", + ["num"] = 11, + }, + [6] = { + ["CLSID"] = "{53BE25A4-C86C-4571-9BC0-47D668349595}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{53BE25A4-C86C-4571-9BC0-47D668349595}", + ["num"] = 4, + }, + [8] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 8, + }, + [9] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 5, + }, + [10] = { + ["CLSID"] = "{FBC29BFE-3D24-4C64-B81D-941239D12249}", + ["num"] = 10, + }, + [11] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 7, + }, + [12] = { + ["CLSID"] = "{RVV-AE}", + ["num"] = 6, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "Su-57", +} +return unitPayloads diff --git a/resources/customized_payloads/Tu-142.lua b/resources/customized_payloads/Tu-142.lua new file mode 100644 index 00000000..2570bf01 --- /dev/null +++ b/resources/customized_payloads/Tu-142.lua @@ -0,0 +1,67 @@ +local unitPayloads = { + ["name"] = "Tu-142", + ["payloads"] = { + [1] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{C42EE4C3-355C-4B83-8B22-B39430B8F4AE}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [2] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{C42EE4C3-355C-4B83-8B22-B39430B8F4AE}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{C42EE4C3-355C-4B83-8B22-B39430B8F4AE}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [4] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{C42EE4C3-355C-4B83-8B22-B39430B8F4AE}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{C42EE4C3-355C-4B83-8B22-B39430B8F4AE}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + }, + ["unitType"] = "Tu-142", +} +return unitPayloads diff --git a/resources/customized_payloads/Tu-160.lua b/resources/customized_payloads/Tu-160.lua new file mode 100644 index 00000000..e0506bfc --- /dev/null +++ b/resources/customized_payloads/Tu-160.lua @@ -0,0 +1,82 @@ +local unitPayloads = { + ["name"] = "Tu-160", + ["payloads"] = { + [1] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + [2] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + [3] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + [5] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 2, + }, + [2] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + }, + }, + }, + ["unitType"] = "Tu-160", +} +return unitPayloads diff --git a/resources/customized_payloads/Tu-22M3.lua b/resources/customized_payloads/Tu-22M3.lua new file mode 100644 index 00000000..d64f0862 --- /dev/null +++ b/resources/customized_payloads/Tu-22M3.lua @@ -0,0 +1,123 @@ +local unitPayloads = { + ["name"] = "Tu-22M3", + ["payloads"] = { + [1] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 2, + }, + [5] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [2] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "{AD5E5863-08FC-4283-B92C-162E2B2BD3FF}", + ["num"] = 3, + }, + [4] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 2, + }, + [5] = { + ["CLSID"] = "{E1AAE713-5FC3-4CAA-9FF5-3FDCFB899E33}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + [5] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 3, + }, + [3] = { + ["CLSID"] = "{12429ECF-03F0-4DF6-BCBD-5D38B6343DE1}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 30, + }, + }, + }, + ["unitType"] = "Tu-22M3", +} +return unitPayloads diff --git a/resources/customized_payloads/Tu-95MS.lua b/resources/customized_payloads/Tu-95MS.lua new file mode 100644 index 00000000..43341378 --- /dev/null +++ b/resources/customized_payloads/Tu-95MS.lua @@ -0,0 +1,67 @@ +local unitPayloads = { + ["name"] = "Tu-95MS", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [3] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + [5] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{0290F5DE-014A-4BB1-9843-D717749B1DED}", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 33, + }, + }, + }, + ["unitType"] = "Tu-95MS", +} +return unitPayloads diff --git a/resources/dcs/beacons/thechannel.json b/resources/dcs/beacons/thechannel.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/resources/dcs/beacons/thechannel.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/resources/factions/allies_1944.json b/resources/factions/allies_1944.json new file mode 100644 index 00000000..77652519 --- /dev/null +++ b/resources/factions/allies_1944.json @@ -0,0 +1,70 @@ +{ + "country": "USA", + "name": "Allies 1944", + "authors": "Khopa", + "description": "A generic WW2 ally factions, with all their WW2 units.
", + "aircrafts": [ + "P_51D", + "P_51D_30_NA", + "P_47D_30", + "P_47D_30bl1", + "P_47D_40", + "SpitfireLFMkIX", + "SpitfireLFMkIXCW", + "A_20G", + "B_17G" + ], + "frontline_units": [ + "MT_M4A4_Sherman_Firefly", + "MT_M4_Sherman", + "APC_M2A1", + "CT_Cromwell_IV", + "CT_Centaur_IV", + "HIT_Churchill_VII", + "M30_Cargo_Carrier", + "LAC_M8_Greyhound", + "TD_M10_GMC", + "Daimler_Armoured_Car", + "LT_Mk_VII_Tetrarch" + ], + "artillery_units": [ + "M12_GMC" + ], + "logistics_units": [ + "Bedford_MWD", + "CCKW_353" + ], + "infantry_units": [ + "Infantry_SMLE_No_4_Mk_1", + "Infantry_M1_Garand" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "AllyWW2FlakGenerator", + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "WW2LSTGroupGenerator" + ], + "navy_group_count": 1, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2ally" +} diff --git a/resources/factions/allies_1944_free.json b/resources/factions/allies_1944_free.json new file mode 100644 index 00000000..56b1a781 --- /dev/null +++ b/resources/factions/allies_1944_free.json @@ -0,0 +1,54 @@ +{ + "country": "USA", + "name": "Allies 1944 (Free)", + "authors": "Khopa", + "description": "A generic WW2 ally faction that does not requires the paid WW2 asset pack.
", + "aircrafts": [ + "P_51D", + "P_51D_30_NA", + "P_47D_30", + "P_47D_30bl1", + "P_47D_40", + "SpitfireLFMkIX", + "SpitfireLFMkIXCW", + "A_20G" + ], + "frontline_units": [ + "MT_M4_Sherman", + "APC_M2A1" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Bedford_MWD" + ], + "infantry_units": [ + "Paratrooper_AKS" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "navy_group_count": 0, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2free" +} diff --git a/resources/factions/australia_2005.json b/resources/factions/australia_2005.json new file mode 100644 index 00000000..58f88930 --- /dev/null +++ b/resources/factions/australia_2005.json @@ -0,0 +1,66 @@ +{ + "country": "Australia", + "name": "Australia 2005", + "authors": "Khopa", + "description": "The Australian army in 2005.
Some units might not be accurate, but were picked to represent at best this army.
", + "aircrafts": [ + "FA_18C_hornet", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "MBT_Leopard_1A3", + "APC_M113", + "IFV_LAV_25", + "IFV_MCV_80" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "RapierGenerator" + ], + "sams": [ + "HawkGenerator", + "RapierGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "HMAS Canberra", + "HMAS Adelaide" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/bluefor_coldwar.json b/resources/factions/bluefor_coldwar.json new file mode 100644 index 00000000..dd711747 --- /dev/null +++ b/resources/factions/bluefor_coldwar.json @@ -0,0 +1,82 @@ +{ + "country": "Combined Joint Task Forces Blue", + "name": "Bluefor Coldwar", + "authors": "Khopa", + "description": "A generic bluefor coldwar faction.
", + "aircrafts": [ + "F_14B", + "F_4E", + "F_5E_3", + "A_10A", + "AJS37", + "UH_1H", + "SA342M", + "SA342L", + "B_52H" + ], + "awacs": [ + "C_130", + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "VulcanGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper", + "doctrine": "coldwar" +} diff --git a/resources/factions/bluefor_coldwar_a4.json b/resources/factions/bluefor_coldwar_a4.json new file mode 100644 index 00000000..12fe48bf --- /dev/null +++ b/resources/factions/bluefor_coldwar_a4.json @@ -0,0 +1,85 @@ +{ + "country": "Combined Joint Task Forces Blue", + "name": "Bluefor Coldwar (With A4)", + "authors": "Khopa", + "description": "A generic bluefor coldwar faction. (With the A-4E-C mod)
", + "aircrafts": [ + "F_14B", + "F_4E", + "F_5E_3", + "A_10A", + "AJS37", + "UH_1H", + "SA342M", + "SA342L", + "A_4E_C", + "B_52H" + ], + "awacs": [ + "C_130", + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "VulcanGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + "Community A-4E": "https://heclak.github.io/community-a4e-c/" + }, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper", + "doctrine": "coldwar" +} diff --git a/resources/factions/bluefor_coldwar_a4_mb339.json b/resources/factions/bluefor_coldwar_a4_mb339.json new file mode 100644 index 00000000..d106c693 --- /dev/null +++ b/resources/factions/bluefor_coldwar_a4_mb339.json @@ -0,0 +1,87 @@ +{ + "country": "Combined Joint Task Forces Blue", + "name": "Bluefor Coldwar (With A4 & MB339)", + "authors": "Khopa", + "description": "A generic bluefor coldwar faction. (With the A-4E-C and the MB-339 mods)
", + "aircrafts": [ + "F_14B", + "F_4E", + "F_5E_3", + "A_10A", + "AJS37", + "UH_1H", + "SA342M", + "SA342L", + "A_4E_C", + "MB_339PAN", + "B_52H" + ], + "awacs": [ + "C_130", + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "VulcanGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + "MB-339A/PAN by Frecce Tricolori Virtuali": "http://www.freccetricolorivirtuali.net/", + "Community A-4E": "https://heclak.github.io/community-a4e-c/" + }, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper", + "doctrine": "coldwar" +} diff --git a/resources/factions/bluefor_modern.json b/resources/factions/bluefor_modern.json new file mode 100644 index 00000000..49e2e1cf --- /dev/null +++ b/resources/factions/bluefor_modern.json @@ -0,0 +1,100 @@ +{ + "country": "Combined Joint Task Forces Blue", + "name": "Bluefor Modern", + "authors": "Khopa", + "description": "A generic bluefor modern faction. This also includes many redfor units and is meant to be a faction that has access to most modern flyable modules.
", + "aircrafts": [ + "F_14B", + "F_15C", + "F_15E", + "F_16C_50", + "FA_18C_hornet", + "JF_17", + "M_2000C", + "F_5E_3", + "Su_27", + "Su_25T", + "A_10A", + "A_10C", + "A_10C_2", + "AV8BNA", + "AJS37", + "UH_1H", + "AH_64D", + "Ka_50", + "SA342M", + "SA342L", + "B_52H", + "B_1B" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "MBT_Leopard_2", + "MBT_Merkava_Mk__4", + "ATGM_M1134_Stryker", + "IFV_M2A2_Bradley", + "IFV_Marder", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/canada_2005.json b/resources/factions/canada_2005.json new file mode 100644 index 00000000..32883ce3 --- /dev/null +++ b/resources/factions/canada_2005.json @@ -0,0 +1,64 @@ +{ + "country": "Canada", + "name": "Canada 2005", + "authors": "Khopa", + "description": "Canada in the 2000s, an F/A-18C Hornet focused faction.
", + "aircrafts": [ + "FA_18C_hornet", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leopard_1A3", + "MBT_Leopard_2", + "IFV_LAV_25", + "APC_M113", + "IFV_MCV_80" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "AvengerGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/china_2010.json b/resources/factions/china_2010.json new file mode 100644 index 00000000..28f5253d --- /dev/null +++ b/resources/factions/china_2010.json @@ -0,0 +1,87 @@ +{ + "country": "China", + "name": "China 2010", + "authors": "Khopa", + "description": "China in the late 2000s, early 2010s.
", + "aircrafts": [ + "MiG_21Bis", + "Su_30", + "Su_33", + "J_11A", + "JF_17", + "Mi_8MT", + "Mi_28N" + ], + "awacs": [ + "KJ_2000" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ZTZ_96B", + "MBT_T_55", + "ZBD_04A", + "IFV_BMP_1" + ], + "artillery_units": [ + "MLRS_9A52_Smerch", + "SPH_2S9_Nona" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA9Generator", + "SA13Generator", + "ZSU23Generator", + "ZU23Generator" + ], + "sams": [ + "HQ7Generator", + "SA10Generator", + "SA6Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "carrier_names": [ + "001 Liaoning", + "002 Shandong" + ], + "helicopter_carrier": [ + "Type_071_Amphibious_Transport_Dock" + ], + "helicopter_carrier_names": [ + "Kunlun Shan", + "Jinggang Shan", + "Changbai Shan", + "Yimeng Shan", + "Longhu Shan", + "Wuzhi Shan", + "Wudang Shan" + ], + "destroyers": [ + "Type_052B_Destroyer", + "Type_052C_Destroyer" + ], + "cruiser": [ + "Type_054A_Frigate" + ], + "requirements": {}, + "navy_generators": [ + "Type54GroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "WingLoong_I" +} diff --git a/resources/factions/france_1995.json b/resources/factions/france_1995.json new file mode 100644 index 00000000..b548eebb --- /dev/null +++ b/resources/factions/france_1995.json @@ -0,0 +1,75 @@ +{ + "country": "France", + "name": "France 1995", + "authors": "Khopa", + "description": "France in the late 90s before Rafale introduction. A Mirage-2000 centric faction choice.
", + "aircrafts": [ + "M_2000C", + "Mirage_2000_5", + "SA342M", + "SA342L", + "SA342Mistral" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leclerc", + "TPz_Fuchs", + "APC_Cobra", + "ATGM_M1134_Stryker", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "HQ7Generator", + "RolandGenerator" + ], + "sams": [ + "RolandGenerator", + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa", + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "Jeanne d'Arc" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator", + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/france_2005_modded.json b/resources/factions/france_2005_modded.json new file mode 100644 index 00000000..87624cc8 --- /dev/null +++ b/resources/factions/france_2005_modded.json @@ -0,0 +1,89 @@ +{ + "country": "France", + "name": "France 2005 (Modded)", + "authors": "Khopa", + "description": "France in the mid, late 2000s, using the Rafale mod, and Frenchpack's units.
", + "aircrafts": [ + "M_2000C", + "Mirage_2000_5", + "Rafale_M", + "Rafale_A_S", + "SA342M", + "SA342L", + "SA342Mistral" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "AMX_10RCR", + "AMX_10RCR_SEPAR", + "ERC_90", + "TRM_2000_PAMELA", + "VAB__50", + "VAB_MEPHISTO", + "VAB_T20_13", + "VAB_T20_13", + "VBL__50", + "VBL_AANF1", + "VBAE_CRAB", + "VBAE_CRAB_MMP", + "AMX_30B2", + "Leclerc_Serie_XXI" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "HQ7Generator", + "RolandGenerator" + ], + "sams": [ + "RolandGenerator", + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974", + "RAFALE 2.5.5": "https://www.digitalcombatsimulator.com/fr/files/3307478/" + }, + "carrier_names": [ + "L9013 Mistral", + "L9014 Tonerre", + "L9015 Dixmude" + ], + "helicopter_carrier_names": [ + "Jeanne d'Arc" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/germany_1942.json b/resources/factions/germany_1942.json new file mode 100644 index 00000000..aaaeec17 --- /dev/null +++ b/resources/factions/germany_1942.json @@ -0,0 +1,60 @@ +{ + "country": "Third Reich", + "name": "Germany 1942", + "authors": "Khopa", + "description": "Germany 1942, is a faction that does not use the late war german units such as the Tiger tank, so it's a bit easier to perform CAS against them.
", + "aircrafts": [ + "FW_190A8", + "FW_190D9", + "Bf_109K_4", + "Ju_88A4" + ], + "frontline_units": [ + "MT_Pz_Kpfw_IV_Ausf_H", + "APC_Sd_Kfz_251", + "AC_Sd_Kfz_234_2_Puma", + "TD_Jagdpanzer_IV" + ], + "artillery_units": [ + "Sturmpanzer_IV_Brummbär" + ], + "logistics_units": [ + "Blitz_3_6_6700A", + "Kübelwagen_82", + "Sd_Kfz_7", + "Sd_Kfz_2" + ], + "infantry_units": [ + "Infantry_Mauser_98" + ], + "shorads": [ + "FlakGenerator" + ], + "sams": [ + "FreyaGenerator", + "FlakGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "UBoatGroupGenerator", + "SchnellbootGroupGenerator" + ], + "navy_group_count": 2, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2germany" +} diff --git a/resources/factions/germany_1944.json b/resources/factions/germany_1944.json new file mode 100644 index 00000000..7d4dc7d6 --- /dev/null +++ b/resources/factions/germany_1944.json @@ -0,0 +1,69 @@ +{ + "country": "Third Reich", + "name": "Germany 1944", + "authors": "Khopa", + "description": "Late war Germany with access to all the late-war ground units such as the Tiger and Tiger II tanks.
", + "aircrafts": [ + "FW_190A8", + "FW_190D9", + "Bf_109K_4", + "Ju_88A4" + ], + "frontline_units": [ + "MT_Pz_Kpfw_V_Panther_Ausf_G", + "MT_Pz_Kpfw_IV_Ausf_H", + "HT_Pz_Kpfw_VI_Tiger_I", + "HT_Pz_Kpfw_VI_Ausf__B_Tiger_II", + "APC_Sd_Kfz_251", + "AC_Sd_Kfz_234_2_Puma", + "Sd_Kfz_184_Elefant", + "TD_Jagdpanther_G1", + "TD_Jagdpanzer_IV" + ], + "artillery_units": [ + "Sturmpanzer_IV_Brummbär" + ], + "logistics_units": [ + "Blitz_3_6_6700A", + "Kübelwagen_82", + "Sd_Kfz_7", + "Sd_Kfz_2" + ], + "infantry_units": [ + "Infantry_Mauser_98" + ], + "shorads": [ + "FlakGenerator" + ], + "sams": [ + "FlakGenerator", + "FreyaGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "UBoatGroupGenerator", + "SchnellbootGroupGenerator" + ], + "navy_group_count": 2, + "missiles": [ + "V1GroupGenerator" + ], + "missiles_group_count": 1, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2germany" +} diff --git a/resources/factions/germany_1944_free.json b/resources/factions/germany_1944_free.json new file mode 100644 index 00000000..30eb83f3 --- /dev/null +++ b/resources/factions/germany_1944_free.json @@ -0,0 +1,52 @@ +{ + "country": "Third Reich", + "name": "Germany 1944 (Free)", + "authors": "Khopa", + "description": "WW2 germany faction that does not require the WW2 asset pack.
", + "aircrafts": [ + "FW_190A8", + "FW_190D9", + "Bf_109K_4" + ], + "frontline_units": [ + "MT_Pz_Kpfw_IV_Ausf_H", + "APC_Sd_Kfz_251" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Blitz_3_6_6700A" + ], + "infantry_units": [ + "Infantry_Soldier_Rus" + ], + "shorads": [ + "Flak18Generator" + ], + "sams": [ + "Flak18Generator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "navy_group_count": 0, + "missiles": [ + ], + "missiles_group_count": 0, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2free" +} diff --git a/resources/factions/germany_1990.json b/resources/factions/germany_1990.json new file mode 100644 index 00000000..9d13533b --- /dev/null +++ b/resources/factions/germany_1990.json @@ -0,0 +1,66 @@ +{ + "country": "Germany", + "name": "Germany 1990", + "authors": "Khopa", + "description": "1990s reunited Germany.
", + "aircrafts": [ + "MiG_29G", + "Tornado_IDS", + "F_4E", + "UH_1H", + "SA342M", + "SA342L" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "TPz_Fuchs", + "MBT_Leopard_1A3", + "MBT_Leopard_2", + "IFV_Marder" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "RolandGenerator" + ], + "sams": [ + "HawkGenerator", + "RolandGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/india_2010.json b/resources/factions/india_2010.json new file mode 100644 index 00000000..0735d9a3 --- /dev/null +++ b/resources/factions/india_2010.json @@ -0,0 +1,75 @@ +{ + "country": "India", + "name": "India 2010", + "authors": "Khopa", + "description": "Indian faction in the late 2000s.
", + "aircrafts": [ + "Mirage_2000_5", + "M_2000C", + "MiG_27K", + "MiG_21Bis", + "MiG_29S", + "Su_30", + "AH_64A", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "MBT_T_90", + "MBT_T_72B", + "IFV_BMP_2" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_M4", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA8Generator", + "SA13Generator", + "SA19Generator", + "ZSU23Generator" + ], + "sams": [ + "SA6Generator", + "SA3Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + "INS Vikramaditya" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator", + "MolniyaGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/insurgents.json b/resources/factions/insurgents.json new file mode 100644 index 00000000..a124f93f --- /dev/null +++ b/resources/factions/insurgents.json @@ -0,0 +1,35 @@ +{ + "country": "Insurgents", + "name": "Insurgents", + "authors": "Khopa", + "description": "Insurgents faction.
", + "aircrafts": [ + ], + "frontline_units": [ + "APC_Cobra", + "APC_MTLB", + "ARV_BRDM_2", + "AAA_ZU_23_Insurgent_on_Ural_375" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Insurgents", + "Soldier_RPG" + ], + "shorads": [ + "SA9Generator", + "ZU23Generator", + "ZSU23Generator" + ], + "sams": [ + "ZU23Generator", + "ZSU23Generator" + ] +} diff --git a/resources/factions/insurgents_modded.json b/resources/factions/insurgents_modded.json new file mode 100644 index 00000000..1a199e53 --- /dev/null +++ b/resources/factions/insurgents_modded.json @@ -0,0 +1,39 @@ +{ + "country": "Insurgents", + "name": "Insurgents (Modded)", + "authors": "Khopa", + "description": "Insurgents faction using the modded insurgents units from the frenchpack mods.
", + "aircrafts": [ + ], + "frontline_units": [ + "DIM__TOYOTA_BLUE", + "DIM__TOYOTA_DESERT", + "DIM__TOYOTA_GREEN", + "DIM__KAMIKAZE", + "AAA_ZU_23_Insurgent_on_Ural_375" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Insurgents", + "Soldier_RPG" + ], + "shorads": [ + "SA9Generator", + "ZU23Generator", + "ZSU23Generator" + ], + "sams": [ + "ZU23Generator", + "ZSU23Generator" + ], + "requirements": { + "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974" + } +} diff --git a/resources/factions/iran_2015.json b/resources/factions/iran_2015.json new file mode 100644 index 00000000..6b028767 --- /dev/null +++ b/resources/factions/iran_2015.json @@ -0,0 +1,85 @@ +{ + "country": "Iran", + "name": "Iran 2015", + "authors": "Khopa", + "description": "Iranian 2010s faction
", + "aircrafts": [ + "MiG_21Bis", + "MiG_29A", + "F_4E", + "F_5E_3", + "F_14B", + "Su_17M4", + "Su_24M", + "Su_25", + "Su_25T", + "Mi_28N", + "Mi_24V" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_M113", + "APC_BTR_80", + "MBT_M60A3_Patton", + "IFV_BMP_1", + "MBT_T_72B" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Insurgents", + "Paratrooper_RPG_16" + ], + "shorads": [ + "HQ7Generator", + "ZSU23Generator" + ], + "sams": [ + "SA2Generator", + "SA6Generator", + "SA11Generator", + "HawkGenerator", + "HQ7Generator" + ], + "ewrs": [ + "TallRackGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": {}, + "carrier_names": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "navy_generators": [ + "GrishaGroupGenerator", + "MolniyaGroupGenerator", + "KiloSubGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/israel_1948.json b/resources/factions/israel_1948.json new file mode 100644 index 00000000..99685c84 --- /dev/null +++ b/resources/factions/israel_1948.json @@ -0,0 +1,54 @@ +{ + "country": "Israel", + "name": "Israel 1948", + "authors": "Khopa", + "description": "Israel during the 1948 Arab-Israeli war.
", + "aircrafts": [ + "SpitfireLFMkIXCW", + "SpitfireLFMkIX", + "P_51D", + "P_51D_30_NA", + "Bf_109K_4", + "B_17G" + ], + "frontline_units": [ + "MT_M4A4_Sherman_Firefly", + "APC_M2A1", + "MT_M4_Sherman", + "LAC_M8_Greyhound" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_SMLE_No_4_Mk_1" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "EarlyColdWarFlakGenerator", + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": false, + "doctrine": "ww2" +} diff --git a/resources/factions/israel_1973.json b/resources/factions/israel_1973.json new file mode 100644 index 00000000..1ce2067d --- /dev/null +++ b/resources/factions/israel_1973.json @@ -0,0 +1,62 @@ +{ + "country": "Israel", + "name": "Israel 1973", + "authors": "Khopa", + "description": "Israel during the 1973 Yom Kippur War.
", + "aircrafts": [ + "F_4E", + "A_4E_C", + "UH_1H" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MT_M4_Sherman", + "APC_M2A1", + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "Community A-4E": "https://heclak.github.io/community-a4e-c/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": false, + "doctrine": "coldwar" +} diff --git a/resources/factions/israel_1982.json b/resources/factions/israel_1982.json new file mode 100644 index 00000000..d81462e3 --- /dev/null +++ b/resources/factions/israel_1982.json @@ -0,0 +1,65 @@ +{ + "country": "Israel", + "name": "Israel 1982", + "authors": "Khopa", + "description": "Israel during the 1982 war with Lebanon.
", + "aircrafts": [ + "F_4E", + "A_4E_C", + "F_15C", + "F_16A", + "F_16C_50", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "APC_M113", + "MBT_M60A3_Patton", + "MBT_Merkava_Mk__4" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "ChaparralGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "Community A-4E": "https://heclak.github.io/community-a4e-c/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/israel_2000.json b/resources/factions/israel_2000.json new file mode 100644 index 00000000..9dd42d7d --- /dev/null +++ b/resources/factions/israel_2000.json @@ -0,0 +1,68 @@ +{ + "country": "Israel", + "name": "Israel 2000", + "authors": "Khopa", + "description": "Modern Israeli faction.
", + "aircrafts": [ + "F_4E", + "F_15C", + "F_15E", + "F_16C_50", + "UH_1H", + "AH_1W", + "AH_64D" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "APC_M113", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW", + "MBT_Merkava_Mk__4" + ], + "artillery_units": [ + "SPH_M109_Paladin", + "MLRS_M270" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "ChaparralGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/italy_1990.json b/resources/factions/italy_1990.json new file mode 100644 index 00000000..30a05926 --- /dev/null +++ b/resources/factions/italy_1990.json @@ -0,0 +1,67 @@ +{ + "country": "Italy", + "name": "Italy 1990", + "authors": "Khopa", + "description": "Italy in the 90s.
", + "aircrafts": [ + "Tornado_IDS", + "AV8BNA", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leopard_1A3", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "AvengerGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "Giuseppe Garibaldi", + "Cavour" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/italy_1990_mb339.json b/resources/factions/italy_1990_mb339.json new file mode 100644 index 00000000..bad67565 --- /dev/null +++ b/resources/factions/italy_1990_mb339.json @@ -0,0 +1,69 @@ +{ + "country": "Italy", + "name": "Italy 1990 (With MB339)", + "authors": "Khopa", + "description": "Italy in the 90s, with the MB339 mod.
", + "aircrafts": [ + "Tornado_IDS", + "AV8BNA", + "MB_339PAN", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leopard_1A3", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "AvengerGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + "MB-339A/PAN by Frecce Tricolori Virtuali": "http://www.freccetricolorivirtuali.net/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "Giuseppe Garibaldi", + "Cavour" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/japan_2005.json b/resources/factions/japan_2005.json new file mode 100644 index 00000000..f3c6fcb4 --- /dev/null +++ b/resources/factions/japan_2005.json @@ -0,0 +1,73 @@ +{ + "country": "Japan", + "name": "Japan 2005", + "authors": "Khopa", + "description": "Japanese self defense force, F-15C standing as F-15J, and F-16 as Mitsubishi F-2.
Ground units were also chosen to fit the existing vehicles of the japanese forces
", + "aircrafts": [ + "F_15C", + "F_16C_50", + "F_4E", + "AH_1W", + "AH_64D" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Merkava_Mk__4", + "MBT_M1A2_Abrams", + "IFV_Marder", + "TPz_Fuchs", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament" + ], + "artillery_units": [ + "SPH_M109_Paladin", + "MLRS_M270" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "GepardGenerator" + ], + "sams": [ + "HawkGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": { + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "Hyuga", + "Ise" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/libya_2011.json b/resources/factions/libya_2011.json new file mode 100644 index 00000000..2bc06c93 --- /dev/null +++ b/resources/factions/libya_2011.json @@ -0,0 +1,72 @@ +{ + "country": "Libya", + "name": "Libya 2011", + "authors": "Khopa", + "description": "Gaddafi's Lybian forces during the 2011 international intervention
", + "aircrafts": [ + "MiG_21Bis", + "MiG_23MLD", + "Su_17M4", + "Su_24M", + "Mi_24V" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "ARV_BRDM_2", + "MBT_T_72B", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Insurgents", + "Paratrooper_RPG_16" + ], + "shorads": [ + "HQ7Generator", + "SA8Generator", + "ZSU23Generator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator", "MolniyaGroupGenerator" + ] +} diff --git a/resources/factions/netherlands_1990.json b/resources/factions/netherlands_1990.json new file mode 100644 index 00000000..c102df32 --- /dev/null +++ b/resources/factions/netherlands_1990.json @@ -0,0 +1,58 @@ +{ + "country": "The Netherlands", + "name": "Netherlands 1990", + "authors": "Khopa", + "description": "Netherlands forces in the 90s.
", + "aircrafts": [ + "F_16C_50", + "F_5E_3", + "AH_64A" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "APC_M113", + "MBT_Leopard_1A3" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/north_korea_2000.json b/resources/factions/north_korea_2000.json new file mode 100644 index 00000000..ce1d4331 --- /dev/null +++ b/resources/factions/north_korea_2000.json @@ -0,0 +1,80 @@ +{ + "country": "North Korea", + "name": "North Korea 2000", + "authors": "Khopa", + "description": "North Korean army in the 2000s.
", + "aircrafts": [ + "MiG_15bis", + "MiG_19P", + "MiG_21Bis", + "MiG_23MLD", + "MiG_29A", + "Mi_8MT", + "Mi_24V" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "APC_BTR_80", + "IFV_BMP_1", + "MBT_T_55", + "MBT_T_72B", + "MBT_T_80U" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA9Generator", + "SA13Generator", + "ZSU23Generator", + "ZU23Generator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "navy_generators": [ + "GrishaGroupGenerator", + "MolniyaGroupGenerator" + ], + "has_jtac": false +} diff --git a/resources/factions/pakistan_2015.json b/resources/factions/pakistan_2015.json new file mode 100644 index 00000000..28d14bb4 --- /dev/null +++ b/resources/factions/pakistan_2015.json @@ -0,0 +1,73 @@ +{ + "country": "Pakistan", + "name": "Pakistan 2015", + "authors": "Khopa", + "description": "Pakistan circa 2015 for JF-17 and F-16 enthusiasts.
", + "aircrafts": [ + "JF_17", + "F_16C_50", + "MiG_21Bis", + "MiG_19P", + "Mi_8MT", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "KJ_2000" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "MBT_T_80U", + "MBT_T_55", + "ZBD_04A", + "APC_BTR_80", + "APC_M113" + ], + "artillery_units": [ + "MLRS_9A52_Smerch", + "SPH_2S9_Nona" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "HQ7Generator", + "ZU23UralGenerator", + "ZU23Generator" + ], + "sams": [ + "SA10Generator", + "SA2Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + ], + "carrier_names": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruiser": [ + ], + "requirements": {}, + "navy_generators": [ + "Type54GroupGenerator", + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "WingLoong_I" +} diff --git a/resources/factions/pmc_russian.json b/resources/factions/pmc_russian.json new file mode 100644 index 00000000..57451f5d --- /dev/null +++ b/resources/factions/pmc_russian.json @@ -0,0 +1,36 @@ +{ + "country": "Russia", + "name": "Private Military Company - Russian", + "authors": "Khopa", + "description": "A private military company using Russian units.
", + "aircrafts": [ + "L_39C", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Ka_50" + ], + "frontline_units": [ + "APC_Cobra", + "APC_BTR_80", + "ARV_BRDM_2" + ], + "artillery_units": [ + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA13Generator", + "SA9Generator" + ], + "sams": [ + "SA13Generator" + ] +} diff --git a/resources/factions/pmc_us.json b/resources/factions/pmc_us.json new file mode 100644 index 00000000..4c8c6230 --- /dev/null +++ b/resources/factions/pmc_us.json @@ -0,0 +1,33 @@ +{ + "country": "USA", + "name": "Private Military Company - USA", + "authors": "Khopa", + "description": "A private military company using western units.
", + "aircrafts": [ + "C_101CC", + "UH_1H", + "Mi_8MT", + "SA342M" + ], + "frontline_units": [ + "APC_M1043_HMMWV_Armament", + "IFV_MCV_80" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "AvengerGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/pmc_us_with_mb339.json b/resources/factions/pmc_us_with_mb339.json new file mode 100644 index 00000000..3d42fa03 --- /dev/null +++ b/resources/factions/pmc_us_with_mb339.json @@ -0,0 +1,37 @@ +{ + "country": "USA", + "name": "Private Military Company - USA (MB339)", + "authors": "Khopa", + "description": "A private military company using western units (And using the MB339 mod).
", + "aircrafts": [ + "MB_339PAN", + "C_101CC", + "UH_1H", + "Mi_8MT", + "SA342M" + ], + "frontline_units": [ + "APC_M1043_HMMWV_Armament", + "IFV_MCV_80" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "AvengerGenerator" + ], + "requirements": { + "MB-339A/PAN by Frecce Tricolori Virtuali": "http://www.freccetricolorivirtuali.net/" + }, + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/russia_1955.json b/resources/factions/russia_1955.json new file mode 100644 index 00000000..a00d71cf --- /dev/null +++ b/resources/factions/russia_1955.json @@ -0,0 +1,56 @@ +{ + "country": "Russia", + "name": "Russia 1955", + "authors": "Khopa", + "description": "Soviet army around 1955, during the Korean War
", + "aircrafts": [ + "MiG_15bis" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "FDDM_Grad", + "APC_MTLB", + "MBT_T_55", + "AAA_ZU_23_on_Ural_375" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "EarlyColdWarFlakGenerator" + ], + "sams": [ + "EarlyColdWarFlakGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": false, + "doctrine": "coldwar" +} diff --git a/resources/factions/russia_1965.json b/resources/factions/russia_1965.json new file mode 100644 index 00000000..6dcc7f1c --- /dev/null +++ b/resources/factions/russia_1965.json @@ -0,0 +1,67 @@ +{ + "country": "Russia", + "name": "Russia 1965", + "authors": "Khopa", + "description": "Soviet army in the 60s, ideal to fly the Mig-19 or the Mig-21.
", + "aircrafts": [ + "MiG_15bis", + "MiG_19P", + "MiG_21Bis", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "APC_BTR_80", + "ARV_BTR_RD", + "IFV_BMD_1", + "IFV_BMP_1", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "ZSU23Generator", + "EarlyColdWarFlakGenerator", + "ZU23Generator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": false, + "doctrine": "coldwar" +} diff --git a/resources/factions/russia_1975.json b/resources/factions/russia_1975.json new file mode 100644 index 00000000..337cf1bc --- /dev/null +++ b/resources/factions/russia_1975.json @@ -0,0 +1,77 @@ +{ + "country": "Russia", + "name": "Russia 1975", + "authors": "Khopa", + "description": "Soviet army in the late 70s, using their prototype Mig-29A.
", + "aircrafts": [ + "MiG_21Bis", + "MiG_23MLD", + "MiG_25PD", + "MiG_29A", + "Su_17M4", + "Su_24M", + "Su_25", + "Mi_8MT", + "Mi_24V" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "APC_BTR_80", + "IFV_BMD_1", + "IFV_BMP_1", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S9_Nona", + "SPH_2S1_Gvozdika" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "ColdWarFlakGenerator", + "ZSU23Generator", + "ZU23Generator" + ], + "sams": [ + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "KiloSubGroupGenerator", "MolniyaGroupGenerator" + ], + "has_jtac": false, + "doctrine": "coldwar" +} diff --git a/resources/factions/russia_1990.json b/resources/factions/russia_1990.json new file mode 100644 index 00000000..01142f33 --- /dev/null +++ b/resources/factions/russia_1990.json @@ -0,0 +1,83 @@ +{ + "country": "Russia", + "name": "Russia 1990", + "authors": "Khopa", + "description": "Soviet/Russian army in the 90s.
", + "aircrafts": [ + "MiG_25PD", + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_33", + "Ka_50", + "Mi_8MT", + "Mi_24V", + "Tu_22M3", + "Tu_95MS" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_BTR_80", + "IFV_BMP_1", + "IFV_BMP_2", + "MBT_T_72B", + "MBT_T_80U" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA13Generator", + "SA8Generator" + ], + "sams": [ + "SA6Generator", + "SA11Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": {}, + "carrier_names": [ + "Admiral Kuznetov", + "Admiral Gorshkov" + ], + "navy_generators": [ + "RussianNavyGroupGenerator", + "KiloSubGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/russia_2010.json b/resources/factions/russia_2010.json new file mode 100644 index 00000000..d911f2a6 --- /dev/null +++ b/resources/factions/russia_2010.json @@ -0,0 +1,88 @@ +{ + "country": "Russia", + "name": "Russia 2010", + "authors": "Khopa", + "description": "Russian army in the early 2010s.
", + "aircrafts": [ + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_30", + "Su_33", + "Su_34", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Mi_28N", + "Ka_50", + "Tu_22M3" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "IFV_BMP_2", + "IFV_BMP_3", + "APC_BTR_80", + "MBT_T_90", + "MBT_T_80U", + "MBT_T_72B" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA19Generator", + "SA13Generator" + ], + "sams": [ + "SA11Generator", + "SA10Generator", + "SA6Generator", + "SA19Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": {}, + "carrier_names": [ + "Admiral Kuznetov" + ], + "navy_generators": [ + "RussianNavyGroupGenerator", + "KiloSubGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/russia_2020.json b/resources/factions/russia_2020.json new file mode 100644 index 00000000..51dd2ea7 --- /dev/null +++ b/resources/factions/russia_2020.json @@ -0,0 +1,87 @@ +{ + "country": "Russia", + "name": "Russia 2020 (Modded)", + "authors": "Khopa", + "description": "Russia in 2020, using the Su-57 mod by Cubanace.
", + "aircrafts": [ + "MiG_29S", + "MiG_31", + "Su_24M", + "Su_25", + "Su_25T", + "Su_27", + "Su_30", + "Su_33", + "Su_34", + "Su_57", + "L_39ZA", + "Mi_8MT", + "Mi_24V", + "Mi_28N", + "Ka_50", + "Tu_22M3" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_2", + "IFV_BMP_3", + "MBT_T_90", + "MBT_T_80U", + "MBT_T_72B" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S19_Msta" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA19Generator" + ], + "sams": [ + "SA11Generator", + "SA10Generator", + "SA19Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "aircraft_carrier": [ + "CV_1143_5_Admiral_Kuznetsov" + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + "FF_1135M_Rezky" + ], + "cruisers": [ + "FSG_1241_1MP_Molniya" + ], + "requirements": { + "SU-57 Felon By CubanAce Simulations": "https://www.digitalcombatsimulator.com/fr/files/2539621/" + }, + "carrier_names": [ + "Admiral Kuznetov" + ], + "navy_generators": [ + "RussianNavyGroupGenerator", + "KiloSubGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/spain_1990.json b/resources/factions/spain_1990.json new file mode 100644 index 00000000..991c929c --- /dev/null +++ b/resources/factions/spain_1990.json @@ -0,0 +1,67 @@ +{ + "country": "Spain", + "name": "Spain 1990", + "authors": "Khopa", + "description": "Spain in the 90s
", + "aircrafts": [ + "FA_18C_hornet", + "AV8BNA", + "F_5E_3", + "C_101CC", + "UH_1H" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "MBT_Leopard_2", + "APC_M113" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "Principe de Asturias" + ], + "helicopter_carrier_names": [ + "Juan Carlos I" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/sweden_1990.json b/resources/factions/sweden_1990.json new file mode 100644 index 00000000..6a844be5 --- /dev/null +++ b/resources/factions/sweden_1990.json @@ -0,0 +1,45 @@ +{ + "country": "Sweden", + "name": "Sweden 1990", + "authors": "Khopa", + "description": "Sweden in the 90s.
Note : Since we're missing the Draken and the Air-to-Air variant of the Viggen, this faction will struggle in air-to-air scenarios.
", + "aircrafts": [ + "AJS37", + "UH_1H" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "IFV_MCV_80", + "MBT_Leopard_2", + "APC_M1126_Stryker_ICV" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/syria_1948.json b/resources/factions/syria_1948.json new file mode 100644 index 00000000..012cb500 --- /dev/null +++ b/resources/factions/syria_1948.json @@ -0,0 +1,49 @@ +{ + "country": "Syria", + "name": "Syria 1948", + "authors": "Khopa", + "description": "Syria and Arab armies in the 1948 war against Israel.
", + "aircrafts": [ + "SpitfireLFMkIX", + "SpitfireLFMkIXCW" + ], + "frontline_units": [ + "AC_Sd_Kfz_234_2_Puma", + "APC_Sd_Kfz_251", + "MT_Pz_Kpfw_IV_Ausf_H", + "MT_M4_Sherman" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_SMLE_No_4_Mk_1" + ], + "shorads": [ + "FlakGenerator" + ], + "sams": [ + "FlakGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "carrier_names": [ + ], + "navy_generators": [ + "SchnellbootGroupGenerator" + ], "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "doctrine": "ww2" +} diff --git a/resources/factions/syria_1967.json b/resources/factions/syria_1967.json new file mode 100644 index 00000000..22d1b445 --- /dev/null +++ b/resources/factions/syria_1967.json @@ -0,0 +1,63 @@ +{ + "country": "Syria", + "name": "Syria 1967", + "authors": "Khopa", + "description": "Syria and Arab armies in the 1967 6 days war against Israel.
", + "aircrafts": [ + "MiG_15bis", + "MiG_19P", + "MiG_21Bis", + "Su_17M4", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "MT_Pz_Kpfw_IV_Ausf_H", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "ZU23Generator", + "ZU23UralGenerator" + ], + "sams": [ + "SA2Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator" + ], + "doctrine": "coldwar" +} diff --git a/resources/factions/syria_1967_with_ww2_weapons.json b/resources/factions/syria_1967_with_ww2_weapons.json new file mode 100644 index 00000000..f7d95399 --- /dev/null +++ b/resources/factions/syria_1967_with_ww2_weapons.json @@ -0,0 +1,69 @@ +{ + "country": "Syria", + "name": "Syria 1967 (With WW2 Weapons)", + "authors": "Khopa", + "description": "Syria and Arab armies in the 1967 6 days war against Israel. Using WW2 units to be more accurate (Yes, Syria used Panzer IV, Stug III and Jagdpanzer IV during this war)
", + "aircrafts": [ + "MiG_15bis", + "MiG_19P", + "MiG_21Bis", + + "Su_17M4", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "ARV_BRDM_2", + "MBT_T_55", + "MT_Pz_Kpfw_IV_Ausf_H", + "StuG_III_Ausf__G", + "TD_Jagdpanzer_IV" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "ZU23Generator", + "EarlyColdWarFlakGenerator", + "ZU23UralGenerator" + ], + "sams": [ + "EarlyColdWarFlakGenerator", + "SA2Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator" + ], "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "doctrine": "coldwar" +} diff --git a/resources/factions/syria_1973.json b/resources/factions/syria_1973.json new file mode 100644 index 00000000..1cd10e20 --- /dev/null +++ b/resources/factions/syria_1973.json @@ -0,0 +1,69 @@ +{ + "country": "Syria", + "name": "Syria 1973", + "authors": "Khopa", + "description": "Syria and Arab armies during the Yom Kippur War
" , + "aircrafts": [ + "MiG_21Bis", + "MiG_19P", + "MiG_15bis", + "Su_17M4", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "APC_MTLB", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "ZU23Generator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator" + ], + "doctrine": "coldwar" +} diff --git a/resources/factions/syria_1982.json b/resources/factions/syria_1982.json new file mode 100644 index 00000000..8dbfd92d --- /dev/null +++ b/resources/factions/syria_1982.json @@ -0,0 +1,71 @@ +{ + "country": "Syria", + "name": "Syria 1982", + "authors": "Khopa", + "description": "Syria and Arab armies in the 1982 invasion of Lebanon
", + "aircrafts": [ + "MiG_19P", + "MiG_21Bis", + "MiG_23MLD", + "MiG_25PD", + "Su_17M4", + "Mi_8MT" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "APC_MTLB", + "MBT_T_55", + "MBT_T_72B" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "ZU23Generator", + "EarlyColdWarFlakGenerator", + "ColdWarFlakGenerator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator" + ], + "ewrs": [ + "BoxSpringGenerator" + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator" + ] +} diff --git a/resources/factions/syria_2011.json b/resources/factions/syria_2011.json new file mode 100644 index 00000000..4de4c7d4 --- /dev/null +++ b/resources/factions/syria_2011.json @@ -0,0 +1,90 @@ +{ + "country": "Syria", + "name": "Syria 2011", + "authors": "Khopa", + "description": "Syrian Arab Army at the start of the Syrian Civil War.
", + "aircrafts": [ + "MiG_21Bis", + "MiG_23MLD", + "MiG_25PD", + "MiG_29S", + "Su_17M4", + "Su_24M", + "L_39ZA", + "Mi_24V", + "Mi_8MT", + "SA342M", + "SA342L" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "IFV_BMP_1", + "IFV_BMP_2", + "APC_BTR_80", + "ARV_BRDM_2", + "APC_MTLB", + "APC_Cobra", + "MBT_T_55", + "MBT_T_72B", + "MBT_T_90" + ], + "artillery_units": [ + "MLRS_9K57_Uragan_BM_27", + "SPH_2S9_Nona", + "MLRS_BM_21_Grad", + "SPH_2S1_Gvozdika" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA8Generator", + "SA9Generator", + "SA13Generator", + "SA19Generator", + "ZSU23Generator", + "ColdWarFlakGenerator" + ], + "sams": [ + "SA2Generator", + "SA3Generator", + "SA6Generator", + "SA10Generator", + "SA11Generator" + ], + "ewrs": [ + "BoxSpringGenerator", + "TallRackGenerator" + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "GrishaGroupGenerator", "MolniyaGroupGenerator" + ] +} diff --git a/resources/factions/turkey_2005.json b/resources/factions/turkey_2005.json new file mode 100644 index 00000000..22c7d023 --- /dev/null +++ b/resources/factions/turkey_2005.json @@ -0,0 +1,52 @@ +{ + "country": "Turkey", + "name": "Turkey 2005", + "authors": "Khopa", + "description": "Turkish army in the mid/late 2000s.
", + "aircrafts": [ + "F_16C_50", + "F_4E", + "UH_1H", + "AH_1W" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leopard_2", + "MBT_Leopard_1A3", + "MBT_M60A3_Patton", + "APC_Cobra", + "APC_BTR_80" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249", + "Paratrooper_AKS" + ], + "shorads": [ + "AvengerGenerator", + "ZSU23Generator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} diff --git a/resources/factions/uae_2005.json b/resources/factions/uae_2005.json new file mode 100644 index 00000000..f7e65986 --- /dev/null +++ b/resources/factions/uae_2005.json @@ -0,0 +1,50 @@ +{ + "country": "United Arab Emirates", + "name": "United Arab Emirates 2005", + "authors": "Khopa", + "description": "UAR army in the 2000s.
", + "aircrafts": [ + "M_2000C", + "Mirage_2000_5", + "F_16C_50", + "AH_64D" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Leclerc", + "TPz_Fuchs", + "IFV_BMP_3" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "RapierGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "WingLoong_I" +} diff --git a/resources/factions/uk_1944.json b/resources/factions/uk_1944.json new file mode 100644 index 00000000..81129848 --- /dev/null +++ b/resources/factions/uk_1944.json @@ -0,0 +1,65 @@ +{ + "country": "UK", + "name": "United Kingdom 1944", + "authors": "Khopa", + "description": "United Kingdom army in 1944.
", + "aircrafts": [ + "P_51D", + "P_51D_30_NA", + "P_47D_30", + "P_47D_30bl1", + "P_47D_40", + "SpitfireLFMkIX", + "SpitfireLFMkIXCW", + "A_20G", + "B_17G" + ], + "frontline_units": [ + "MT_M4A4_Sherman_Firefly", + "MT_M4_Sherman", + "APC_M2A1", + "CT_Cromwell_IV", + "CT_Centaur_IV", + "HIT_Churchill_VII", + "Daimler_Armoured_Car", + "LT_Mk_VII_Tetrarch" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Bedford_MWD", + "CCKW_353" + ], + "infantry_units": [ + "Infantry_SMLE_No_4_Mk_1" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "AllyWW2FlakGenerator", + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "WW2LSTGroupGenerator" + ], + "navy_group_count": 1, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2ally" +} diff --git a/resources/factions/uk_1990.json b/resources/factions/uk_1990.json new file mode 100644 index 00000000..39e7bedb --- /dev/null +++ b/resources/factions/uk_1990.json @@ -0,0 +1,73 @@ +{ + "country": "UK", + "name": "United Kingdom 1990", + "authors": "Khopa", + "description": "United Kingdom Army in the 1990s.
", + "aircrafts": [ + "Tornado_GR4", + "AV8BNA", + "F_4E", + "SA342M", + "AH_64A" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_Challenger_II", + "IFV_MCV_80", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator", + "RapierGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + "HMS Invincible", + "HMS Illustrious", + "HMS Ark Royal" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator", + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} \ No newline at end of file diff --git a/resources/factions/ukraine_2010.json b/resources/factions/ukraine_2010.json new file mode 100644 index 00000000..f1a4d9cb --- /dev/null +++ b/resources/factions/ukraine_2010.json @@ -0,0 +1,62 @@ +{ + "country": "Ukraine", + "name": "Ukraine 2010", + "authors": "Khopa", + "description": "Ukrainian army in the 2010s.
", + "aircrafts": [ + "Su_25", + "Su_25T", + "Su_24M", + "Su_27", + "MiG_29S", + "L_39ZA", + "Mi_8MT", + "Mi_24V" + ], + "awacs": [ + "A_50" + ], + "tankers": [ + "IL_78M" + ], + "frontline_units": [ + "APC_M1043_HMMWV_Armament", + "IFV_BMP_3", + "IFV_BMP_2", + "APC_BTR_80", + "MBT_T_80U", + "MBT_T_72B" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Paratrooper_AKS", + "Infantry_Soldier_Rus", + "Paratrooper_RPG_16" + ], + "shorads": [ + "SA9Generator", + "SA13Generator", + "SA19Generator" + ], + "sams": [ + "SA3Generator", + "SA10Generator", + "SA11Generator" + ], + "ewrs": [ + "BigBirdGenerator" + ], + "requirements": {}, + "carrier_names": [ + "Admiral Kuznetov", + "Admiral Gorshkov" + ], + "navy_generators": [ + "GrishaGroupGenerator" + ] +} diff --git a/resources/factions/us_aggressors.json b/resources/factions/us_aggressors.json new file mode 100644 index 00000000..9e2b41a2 --- /dev/null +++ b/resources/factions/us_aggressors.json @@ -0,0 +1,65 @@ +{ + "country": "USAF Aggressors", + "name": "USAF Aggressors", + "authors": "Khopa", + "description": "USAF aggresors.
", + "aircrafts": [ + "F_15C", + "F_15E", + "F_14B", + "FA_18C_hornet", + "F_16C_50", + "A_10A", + "A_10C", + "AV8BNA", + "UH_1H", + "AH_64D", + "Ka_50", + "B_52H", + "B_1B", + "F_117A", + "Su_27" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "MBT_Leopard_2", + "ATGM_M1134_Stryker", + "IFV_M2A2_Bradley", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "requirements": {}, + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} \ No newline at end of file diff --git a/resources/factions/usa_1944.json b/resources/factions/usa_1944.json new file mode 100644 index 00000000..87725411 --- /dev/null +++ b/resources/factions/usa_1944.json @@ -0,0 +1,62 @@ +{ + "country": "USA", + "name": "USA 1944", + "authors": "Khopa", + "description": "US army in 1944, western front.
", + "aircrafts": [ + "P_51D", + "P_51D_30_NA", + "P_47D_30", + "P_47D_30bl1", + "P_47D_40", + "SpitfireLFMkIX", + "A_20G", + "B_17G" + ], + "frontline_units": [ + "MT_M4A4_Sherman_Firefly", + "MT_M4_Sherman", + "APC_M2A1", + "M30_Cargo_Carrier", + "LAC_M8_Greyhound", + "TD_M10_GMC" + ], + "artillery_units": [ + "M12_GMC" + ], + "logistics_units": [ + "CCKW_353" + ], + "infantry_units": [ + "Infantry_M1_Garand" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "AllyWW2FlakGenerator", + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + "WW2LSTGroupGenerator" + ], + "navy_group_count": 1, + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2ally" +} diff --git a/resources/factions/usa_1955.json b/resources/factions/usa_1955.json new file mode 100644 index 00000000..c8a26c33 --- /dev/null +++ b/resources/factions/usa_1955.json @@ -0,0 +1,37 @@ +{ + "country": "USA", + "name": "USA 1955", + "authors": "Khopa", + "description": "US army in the 50s, circa Korean War.
", + "aircrafts": [ + "F_86F_Sabre", + "P_51D", + "P_51D_30_NA", + "B_52H" + ], + "frontline_units": [ + "MT_M4_Sherman", + "MBT_M60A3_Patton", + "APC_M2A1" + ], + "artillery_units": [ + "M12_GMC" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4" + ], + "shorads": [ + "BoforsGenerator" + ], + "sams": [ + "EarlyColdWarFlakGenerator" + ], + "doctrine": "ww2", + "building_set": "ww2ally", + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + } +} \ No newline at end of file diff --git a/resources/factions/usa_1960.json b/resources/factions/usa_1960.json new file mode 100644 index 00000000..34e3db33 --- /dev/null +++ b/resources/factions/usa_1960.json @@ -0,0 +1,34 @@ +{ + "country": "USA", + "name": "USA 1960", + "authors": "Khopa", + "description": "US army in the 60s, pre-Vietnam war.
", + "aircrafts": [ + "F_86F_Sabre", + "P_51D", + "P_51D_30_NA", + "B_52H", + "UH_1H" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "VulcanGenerator" + ], + "sams": [ + "VulcanGenerator" + ], + "requirements": {}, + "doctrine": "coldwar" +} \ No newline at end of file diff --git a/resources/factions/usa_1965.json b/resources/factions/usa_1965.json new file mode 100644 index 00000000..9b832a3d --- /dev/null +++ b/resources/factions/usa_1965.json @@ -0,0 +1,40 @@ +{ + "country": "USA", + "name": "USA 1965", + "authors": "Khopa", + "description": "US army in the late 60s, during Vietnam war.
", + "aircrafts": [ + "F_5E_3", + "F_4E", + "B_52H", + "UH_1H" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "VulcanGenerator", + "ChaparralGenerator", + "EarlyColdWarFlakGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "requirements": {}, + "doctrine": "coldwar" +} \ No newline at end of file diff --git a/resources/factions/usa_1975.json b/resources/factions/usa_1975.json new file mode 100644 index 00000000..77871754 --- /dev/null +++ b/resources/factions/usa_1975.json @@ -0,0 +1,44 @@ +{ + "country": "USA", + "name": "USA 1975", + "authors": "Khopa", + "description": "US army in the 70s at the end of the war in Vietnam.
", + "aircrafts": [ + "F_5E_3", + "F_4E", + "F_14B", + "B_52H", + "UH_1H" + ], + "frontline_units": [ + "MBT_M60A3_Patton", + "APC_M113" + ], + "artillery_units": [ + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "EarlyColdWarFlakGenerator", + "VulcanGenerator", + "ChaparralGenerator" + ], + "sams": [ + "HawkGenerator", + "ChaparralGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "requirements": {}, + "doctrine": "coldwar" +} \ No newline at end of file diff --git a/resources/factions/usa_1990.json b/resources/factions/usa_1990.json new file mode 100644 index 00000000..d1f1a270 --- /dev/null +++ b/resources/factions/usa_1990.json @@ -0,0 +1,89 @@ +{ + "country": "USA", + "name": "USA 1990", + "authors": "Khopa", + "description": "US army in the 90s, Gulf War/Desert Storm.
", + "aircrafts": [ + "F_15C", + "F_15E", + "F_14B", + "FA_18C_hornet", + "F_16C_50", + "A_10A", + "AV8BNA", + "UH_1H", + "AH_64A", + "B_52H", + "B_1B", + "F_117A" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "ATGM_M1134_Stryker", + "APC_M1126_Stryker_ICV", + "IFV_M2A2_Bradley", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "Oliver_Hazzard_Perry_class", + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator", + "ArleighBurkeGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} \ No newline at end of file diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json new file mode 100644 index 00000000..59c3a4f2 --- /dev/null +++ b/resources/factions/usa_2005.json @@ -0,0 +1,90 @@ +{ + "country": "USA", + "name": "USA 2005", + "authors": "Khopa", + "description": "USA in the 2000s.
", + "aircrafts": [ + "F_15C", + "F_15E", + "F_14B", + "FA_18C_hornet", + "F_16C_50", + "A_10C", + "A_10C_2", + "AV8BNA", + "UH_1H", + "AH_64D", + "B_52H", + "B_1B", + "F_117A" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "ATGM_M1134_Stryker", + "APC_M1126_Stryker_ICV", + "IFV_M2A2_Bradley", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "shorads": [ + "AvengerGenerator" + ], + "sams": [ + "HawkGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator", + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper" +} \ No newline at end of file diff --git a/resources/gulflandmap.p b/resources/gulflandmap.p index e0447fbd..77b367f1 100644 Binary files a/resources/gulflandmap.p and b/resources/gulflandmap.p differ diff --git a/resources/mizdata/caucasus/Anapa-Vityazevo.miz b/resources/mizdata/caucasus/Anapa-Vityazevo.miz new file mode 100644 index 00000000..6382b0bd Binary files /dev/null and b/resources/mizdata/caucasus/Anapa-Vityazevo.miz differ diff --git a/resources/mizdata/caucasus/Batumi.miz b/resources/mizdata/caucasus/Batumi.miz new file mode 100644 index 00000000..ac262ee8 Binary files /dev/null and b/resources/mizdata/caucasus/Batumi.miz differ diff --git a/resources/mizdata/caucasus/Beslan.miz b/resources/mizdata/caucasus/Beslan.miz new file mode 100644 index 00000000..238fa1c1 Binary files /dev/null and b/resources/mizdata/caucasus/Beslan.miz differ diff --git a/resources/mizdata/caucasus/Gelendzhik.miz b/resources/mizdata/caucasus/Gelendzhik.miz new file mode 100644 index 00000000..2c914465 Binary files /dev/null and b/resources/mizdata/caucasus/Gelendzhik.miz differ diff --git a/resources/mizdata/caucasus/Gudauta.miz b/resources/mizdata/caucasus/Gudauta.miz new file mode 100644 index 00000000..1411c3f3 Binary files /dev/null and b/resources/mizdata/caucasus/Gudauta.miz differ diff --git a/resources/mizdata/caucasus/Kobuleti.miz b/resources/mizdata/caucasus/Kobuleti.miz new file mode 100644 index 00000000..0c917e55 Binary files /dev/null and b/resources/mizdata/caucasus/Kobuleti.miz differ diff --git a/resources/mizdata/caucasus/Krasnodar-Center.miz b/resources/mizdata/caucasus/Krasnodar-Center.miz new file mode 100644 index 00000000..f25e54a6 Binary files /dev/null and b/resources/mizdata/caucasus/Krasnodar-Center.miz differ diff --git a/resources/mizdata/caucasus/Krasnodar-Pashkovsky.miz b/resources/mizdata/caucasus/Krasnodar-Pashkovsky.miz new file mode 100644 index 00000000..f2bd0f4f Binary files /dev/null and b/resources/mizdata/caucasus/Krasnodar-Pashkovsky.miz differ diff --git a/resources/mizdata/caucasus/Krymsk.miz b/resources/mizdata/caucasus/Krymsk.miz new file mode 100644 index 00000000..3359c2b8 Binary files /dev/null and b/resources/mizdata/caucasus/Krymsk.miz differ diff --git a/resources/mizdata/caucasus/Kutaisi.miz b/resources/mizdata/caucasus/Kutaisi.miz new file mode 100644 index 00000000..ff860fec Binary files /dev/null and b/resources/mizdata/caucasus/Kutaisi.miz differ diff --git a/resources/mizdata/caucasus/Maykop-Khanskaya.miz b/resources/mizdata/caucasus/Maykop-Khanskaya.miz new file mode 100644 index 00000000..92f139ee Binary files /dev/null and b/resources/mizdata/caucasus/Maykop-Khanskaya.miz differ diff --git a/resources/mizdata/caucasus/Mineralnye Vody.miz b/resources/mizdata/caucasus/Mineralnye Vody.miz new file mode 100644 index 00000000..cd84551b Binary files /dev/null and b/resources/mizdata/caucasus/Mineralnye Vody.miz differ diff --git a/resources/mizdata/caucasus/Mozdok.miz b/resources/mizdata/caucasus/Mozdok.miz new file mode 100644 index 00000000..1e19d13a Binary files /dev/null and b/resources/mizdata/caucasus/Mozdok.miz differ diff --git a/resources/mizdata/caucasus/Nalchik.miz b/resources/mizdata/caucasus/Nalchik.miz new file mode 100644 index 00000000..f29c44b0 Binary files /dev/null and b/resources/mizdata/caucasus/Nalchik.miz differ diff --git a/resources/mizdata/caucasus/Novorossiysk.miz b/resources/mizdata/caucasus/Novorossiysk.miz new file mode 100644 index 00000000..49076868 Binary files /dev/null and b/resources/mizdata/caucasus/Novorossiysk.miz differ diff --git a/resources/mizdata/caucasus/Senaki-Kolkhi.miz b/resources/mizdata/caucasus/Senaki-Kolkhi.miz new file mode 100644 index 00000000..63d6ff92 Binary files /dev/null and b/resources/mizdata/caucasus/Senaki-Kolkhi.miz differ diff --git a/resources/mizdata/caucasus/Sochi-Adler.miz b/resources/mizdata/caucasus/Sochi-Adler.miz new file mode 100644 index 00000000..c93339fc Binary files /dev/null and b/resources/mizdata/caucasus/Sochi-Adler.miz differ diff --git a/resources/mizdata/caucasus/Sukhumi-Babushara.miz b/resources/mizdata/caucasus/Sukhumi-Babushara.miz new file mode 100644 index 00000000..9677cefe Binary files /dev/null and b/resources/mizdata/caucasus/Sukhumi-Babushara.miz differ diff --git a/resources/mizdata/caucasus/Vaziani.miz b/resources/mizdata/caucasus/Vaziani.miz new file mode 100644 index 00000000..beb84168 Binary files /dev/null and b/resources/mizdata/caucasus/Vaziani.miz differ diff --git a/resources/mizdata/persiangulf/Al Ain International Airport.miz b/resources/mizdata/persiangulf/Al Ain International Airport.miz new file mode 100644 index 00000000..810f6349 Binary files /dev/null and b/resources/mizdata/persiangulf/Al Ain International Airport.miz differ diff --git a/resources/mizdata/persiangulf/Fujairah.miz b/resources/mizdata/persiangulf/Fujairah.miz new file mode 100644 index 00000000..a2e48ea8 Binary files /dev/null and b/resources/mizdata/persiangulf/Fujairah.miz differ diff --git a/resources/mizdata/syria/Eyn Shemer.miz b/resources/mizdata/syria/Eyn Shemer.miz new file mode 100644 index 00000000..c29451c1 Binary files /dev/null and b/resources/mizdata/syria/Eyn Shemer.miz differ diff --git a/resources/mizdata/syria/Khalkhalah Defenses.miz b/resources/mizdata/syria/Khalkhalah Defenses.miz new file mode 100644 index 00000000..7584826b Binary files /dev/null and b/resources/mizdata/syria/Khalkhalah Defenses.miz differ diff --git a/resources/mizdata/syria/King Hussein Air College.miz b/resources/mizdata/syria/King Hussein Air College.miz new file mode 100644 index 00000000..6998a4ac Binary files /dev/null and b/resources/mizdata/syria/King Hussein Air College.miz differ diff --git a/resources/mizdata/thechannel/Abbeville Drucat.miz b/resources/mizdata/thechannel/Abbeville Drucat.miz new file mode 100644 index 00000000..aa609d0d Binary files /dev/null and b/resources/mizdata/thechannel/Abbeville Drucat.miz differ diff --git a/resources/mizdata/thechannel/Detling.miz b/resources/mizdata/thechannel/Detling.miz new file mode 100644 index 00000000..d4cecfbb Binary files /dev/null and b/resources/mizdata/thechannel/Detling.miz differ diff --git a/resources/mizdata/thechannel/Dunkirk Mardyck.miz b/resources/mizdata/thechannel/Dunkirk Mardyck.miz new file mode 100644 index 00000000..1c86c3fc Binary files /dev/null and b/resources/mizdata/thechannel/Dunkirk Mardyck.miz differ diff --git a/resources/mizdata/thechannel/Hawkinge.miz b/resources/mizdata/thechannel/Hawkinge.miz new file mode 100644 index 00000000..29981845 Binary files /dev/null and b/resources/mizdata/thechannel/Hawkinge.miz differ diff --git a/resources/mizdata/thechannel/High Halden.miz b/resources/mizdata/thechannel/High Halden.miz new file mode 100644 index 00000000..9cf4d23e Binary files /dev/null and b/resources/mizdata/thechannel/High Halden.miz differ diff --git a/resources/mizdata/thechannel/Lympne.miz b/resources/mizdata/thechannel/Lympne.miz new file mode 100644 index 00000000..58e55610 Binary files /dev/null and b/resources/mizdata/thechannel/Lympne.miz differ diff --git a/resources/mizdata/thechannel/Manston.miz b/resources/mizdata/thechannel/Manston.miz new file mode 100644 index 00000000..ea7b9fda Binary files /dev/null and b/resources/mizdata/thechannel/Manston.miz differ diff --git a/resources/mizdata/thechannel/Merville Calonne.miz b/resources/mizdata/thechannel/Merville Calonne.miz new file mode 100644 index 00000000..e0be3b8a Binary files /dev/null and b/resources/mizdata/thechannel/Merville Calonne.miz differ diff --git a/resources/mizdata/thechannel/Saint Omer Longuenesse.miz b/resources/mizdata/thechannel/Saint Omer Longuenesse.miz new file mode 100644 index 00000000..7b3daa77 Binary files /dev/null and b/resources/mizdata/thechannel/Saint Omer Longuenesse.miz differ diff --git a/resources/nevlandmap.p b/resources/nevlandmap.p index 1a9d8cc4..1086ba36 100644 Binary files a/resources/nevlandmap.p and b/resources/nevlandmap.p differ diff --git a/resources/normandylandmap.p b/resources/normandylandmap.p index 0f41f8ea..e179bbf8 100644 Binary files a/resources/normandylandmap.p and b/resources/normandylandmap.p differ diff --git a/resources/plugins/_doc/0.png b/resources/plugins/_doc/0.png new file mode 100644 index 00000000..3320b908 Binary files /dev/null and b/resources/plugins/_doc/0.png differ diff --git a/resources/plugins/_doc/1.png b/resources/plugins/_doc/1.png new file mode 100644 index 00000000..069c45db Binary files /dev/null and b/resources/plugins/_doc/1.png differ diff --git a/resources/plugins/_doc/2.png b/resources/plugins/_doc/2.png new file mode 100644 index 00000000..97952fde Binary files /dev/null and b/resources/plugins/_doc/2.png differ diff --git a/resources/plugins/_doc/plugins_readme.md b/resources/plugins/_doc/plugins_readme.md new file mode 100644 index 00000000..8af9802b --- /dev/null +++ b/resources/plugins/_doc/plugins_readme.md @@ -0,0 +1,68 @@ +# LUA Plugin system + +This plugin system was made for injecting LUA scripts in dcs-liberation missions. + +The resources for the plugins are stored in the `resources/plugins` folder ; each plugin has its own folder. + +## How does the system work ? + +The application first reads the `resources/plugins/plugins.json` file to get a list of plugins to load, in order. +Each entry in this list should correspond to a subfolder of the `resources/plugins` directory, where a `plugin.json` file exists. +This file is the description of the plugin. + +### plugin.json + +The *base* and *jtacautolase* plugins both are included in the standard dcs-liberation distribution. +You can check their respective `plugin.json` files to understand how they work. +Here's a quick rundown of the file's components : + +- `skipUI` : if *true*, this plugin will not appear in the plugins selection user interface. Useful to force a plugin ON or OFF (see the *base* plugin) +- `nameInUI` : the title of the plugin as it will appear in the plugins selection user interface. +- `defaultValue` : the selection value of the plugin, when first installed ; if true, plugin is selected. +- `specificOptions` : a list of specific plugin options + - `nameInUI` : the title of the option as it will appear in the plugins specific options user interface. + - `mnemonic` : the short, technical name of the option. It's the name of the LUA variable passed to the configuration script, and the name of the option in the application's settings + - `defaultValue` : the selection value of the option, when first installed ; if true, option is selected. +- `scriptsWorkOrders` : a list of work orders that can be used to load or disable loading a specific LUA script + - `file` : the name of the LUA file in the plugin folder. + - `mnemonic` : the technical name of the LUA component. The filename may be more precise than needed (e.g. include a version number) ; this is used to load each file only once, and also to disable loading a file + - `disable` : if true, the script will be disabled instead of loaded +- `configurationWorkOrders` : a list of work orders that can be used to load a configuration LUA script (same description as above) + +## Standard plugins + +### The *base* plugin + +The *base* plugin contains the scripts that are going to be injected in every dcs-liberation missions. +It is mandatory. + +### The *JTACAutolase* plugin + +This plugin replaces the vanilla JTAC functionality in dcs-liberation. + +### Known third-party plugins + +Plugins not included with Liberation can be installed by adding them to the +`resources/plugins` directory and listing them in +`resources/plugins/plugins.json`. Below is a list of other plugins that can be +installed: + +* [VEAF](https://github.com/VEAF/dcs-liberation-veaf-framework) + +## Custom plugins + +The easiest way to create a custom plugin is to copy an existing plugin, and modify the files. + +## New settings pages + + + +Custom plugins can be enabled or disabled in the new *LUA Plugins* settings page. + + + +For plugins which expose specific options (such as "use smoke" for the *JTACAutoLase* plugin), the *LUA Plugins Options* settings page lists these options. + + + + diff --git a/resources/plugins/base/dcs_liberation.lua b/resources/plugins/base/dcs_liberation.lua new file mode 100644 index 00000000..722440a6 --- /dev/null +++ b/resources/plugins/base/dcs_liberation.lua @@ -0,0 +1,180 @@ +-- the state.json file will be updated according to this schedule, and also on each destruction or capture event +local WRITESTATE_SCHEDULE_IN_SECONDS = 60 + +logger = mist.Logger:new("DCSLiberation", "info") +logger:info("Check that json.lua is loaded : json = "..tostring(json)) + +killed_aircrafts = {} +killed_ground_units = {} +weapons_fired = {} +base_capture_events = {} +destroyed_objects_positions = {} +mission_ended = false + +local function ends_with(str, ending) + return ending == "" or str:sub(-#ending) == ending +end + +local function messageAll(message) + local msg = {} + msg.text = message + msg.displayTime = 25 + msg.msgFor = {coa = {'all'}} + mist.message.add(msg) +end + +function write_state() + local _debriefing_file_location = debriefing_file_location + if not debriefing_file_location then + _debriefing_file_location = "[nil]" + end + + local fp = io.open(_debriefing_file_location, 'w') + local game_state = { + ["killed_aircrafts"] = killed_aircrafts, + ["killed_ground_units"] = killed_ground_units, + ["weapons_fired"] = weapons_fired, + ["base_capture_events"] = base_capture_events, + ["mission_ended"] = mission_ended, + ["destroyed_objects_positions"] = destroyed_objects_positions, + } + if not json then + local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location) + logger:error(message) + messageAll(message) + end + fp:write(json:encode(game_state)) + fp:close() +end + +local function canWrite(name) + local f = io.open(name, "w") + if f then + f:close() + return true + end + return false +end + +local function testDebriefingFilePath(folderPath, folderName, useCurrentStamping) + if folderPath then + local filePath = nil + if not ends_with(folderPath, "\\") then + folderPath = folderPath .. "\\" + end + if useCurrentStamping then + filePath = string.format("%sstate-%s.json",folderPath, tostring(os.time())) + else + filePath = string.format("%sstate.json",folderPath) + end + local isOk = canWrite(filePath) + if isOk then + logger:info(string.format("The state.json file will be created in %s : (%s)",folderName, filePath)) + return filePath + end + end + return nil +end + +local function discoverDebriefingFilePath() + -- establish a search pattern into the following modes + -- 1. Environment variable LIBERATION_EXPORT_DIR, to support dedicated server hosting + -- 2. Embedded DCS Liberation dcsLiberation.installPath (set by the app to its install path), to support locally hosted single player + -- 3. System temporary folder, as set in the TEMP environment variable + -- 4. Working directory. + + local useCurrentStamping = nil + if os then + useCurrentStamping = os.getenv("LIBERATION_EXPORT_STAMPED_STATE") + end + + local installPath = nil + if dcsLiberation then + installPath = dcsLiberation.installPath + end + + if os then + local result = nil + -- try using the LIBERATION_EXPORT_DIR environment variable + result = testDebriefingFilePath(os.getenv("LIBERATION_EXPORT_DIR"), "LIBERATION_EXPORT_DIR", useCurrentStamping) + if result then + return result + end + -- no joy ? maybe there is a valid path in the mission ? + result = testDebriefingFilePath(installPath, "the DCS Liberation install folder", useCurrentStamping) + if result then + return result + end + -- there's always the possibility of using the system temporary folder + result = testDebriefingFilePath(os.getenv("TEMP"), "TEMP", useCurrentStamping) + if result then + return result + end + end + + -- nothing worked, let's try the last resort folder : current directory. + if lfs then + return testDebriefingFilePath(lfs.writedir(), "the working directory", useCurrentStamping) + end + + return nil +end + +debriefing_file_location = discoverDebriefingFilePath() + +write_state_error_handling = function() + local _debriefing_file_location = debriefing_file_location + if not debriefing_file_location then + _debriefing_file_location = "[nil]" + logger:error("Unable to find where to write DCS Liberation state") + end + + if pcall(write_state) then + else + messageAll("Unable to write DCS Liberation state to ".._debriefing_file_location.. + "\nYou can abort the mission in DCS Liberation.\n".. + "\n\nPlease fix your setup in DCS Liberation, make sure you are pointing to the right installation directory from the File/Preferences menu. Then after fixing the path restart DCS Liberation, and then restart DCS.".. + "\n\nYou can also try to fix the issue manually by replacing the file