Merge pull request #885 from DanAlbert/black

Set up black.
This commit is contained in:
Dan Albert 2021-02-12 20:21:41 -08:00 committed by GitHub
commit f8ae1e9076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
226 changed files with 8484 additions and 4469 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Black
a47bef1f1336fd264d0b175f4421758339a30acb

13
.github/workflows/black.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
black_args: ". --check"

6
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
language_version: python3

View File

@ -17,5 +17,5 @@ AAA_UNITS = [
AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_Kdo_G_40, AirDefence.AAA_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm AirDefence.AAA_Bofors_40mm,
] ]

View File

@ -1,17 +1,70 @@
import inspect import inspect
import dcs import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick'] DEFAULT_AVAILABLE_BUILDINGS = [
"fuel",
"ammo",
"comms",
"oil",
"ware",
"farp",
"fob",
"power",
"factory",
"derrick",
]
WW2_FREE = ['fuel', 'factory', 'ware', 'fob'] WW2_FREE = ["fuel", "factory", "ware", "fob"]
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob'] WW2_GERMANY_BUILDINGS = [
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob'] "fuel",
"factory",
"ww2bunker",
"ww2bunker",
"ww2bunker",
"allycamp",
"allycamp",
"fob",
]
WW2_ALLIES_BUILDINGS = [
"fuel",
"factory",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"fob",
]
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2', FORTIFICATION_BUILDINGS = [
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5', "Siegfried Line",
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis', "Concertina wire",
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3', "Concertina Wire",
'Belgian Gate', 'Container white'] "Czech hedgehogs 1",
"Czech hedgehogs 2",
"Dragonteeth 1",
"Dragonteeth 2",
"Dragonteeth 3",
"Dragonteeth 4",
"Dragonteeth 5",
"Haystack 1",
"Haystack 2",
"Haystack 3",
"Haystack 4",
"Hemmkurvenvenhindernis",
"Log posts 1",
"Log posts 2",
"Log posts 3",
"Log ramps 1",
"Log ramps 2",
"Log ramps 3",
"Belgian Gate",
"Container white",
]
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)] FORTIFICATION_UNITS = [
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)] c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
]
FORTIFICATION_UNITS_ID = [
c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
]

View File

@ -16,7 +16,7 @@ from dcs.planes import (
P_51D, P_51D,
P_51D_30_NA, P_51D_30_NA,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW SpitfireLFMkIXCW,
) )
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
@ -26,7 +26,6 @@ This list contains the aircraft that do not use the guns as the last resort weap
They'll RTB when they don't have gun ammo left They'll RTB when they don't have gun ammo left
""" """
GUNFIGHTERS = [ GUNFIGHTERS = [
# Cold War # Cold War
MiG_15bis, MiG_15bis,
MiG_19P, MiG_19P,
@ -34,11 +33,9 @@ GUNFIGHTERS = [
F_86F_Sabre, F_86F_Sabre,
A_4E_C, A_4E_C,
F_5E_3, F_5E_3,
# Trainers # Trainers
C_101CC, C_101CC,
L_39ZA, L_39ZA,
# WW2 # WW2
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
@ -51,5 +48,4 @@ GUNFIGHTERS = [
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
I_16, I_16,
]
]

View File

@ -23,7 +23,6 @@ from dcs.ships import (
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [ UNITS_WITH_RADAR = [
# Radars # Radars
AirDefence.SAM_SA_15_Tor_9A331, AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_11_Buk_CC_9S470M1, AirDefence.SAM_SA_11_Buk_CC_9S470M1,
@ -49,7 +48,6 @@ UNITS_WITH_RADAR = [
AirDefence.SAM_SA_3_S_125_TR_SNR, AirDefence.SAM_SA_3_S_125_TR_SNR,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_Self_Propelled_STR,
# Ships # Ships
CVN_70_Carl_Vinson, CVN_70_Carl_Vinson,
Oliver_Hazzard_Perry_class, Oliver_Hazzard_Perry_class,
@ -70,5 +68,5 @@ UNITS_WITH_RADAR = [
LHA_1_Tarawa, LHA_1_Tarawa,
Type_052B_Destroyer, Type_052B_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
Type_052C_Destroyer Type_052C_Destroyer,
] ]

View File

@ -28,7 +28,8 @@ class Weapon:
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self) introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
if introduction_year is None: if introduction_year is None:
logging.warning( logging.warning(
f"No introduction year for {self}, assuming always available") f"No introduction year for {self}, assuming always available"
)
return True return True
return date >= datetime.date(introduction_year, 1, 1) return date >= datetime.date(introduction_year, 1, 1)
@ -52,7 +53,7 @@ class Weapon:
return cls( return cls(
cast(str, weapon_data["clsid"]), cast(str, weapon_data["clsid"]),
cast(str, weapon_data["name"]), cast(str, weapon_data["name"]),
cast(int, weapon_data["weight"]) cast(int, weapon_data["weight"]),
) )
@classmethod @classmethod
@ -88,8 +89,11 @@ class Pylon:
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon: def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type. # In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name. # The only way to identify them is by their name.
pylons = [v for v in aircraft.__dict__.values() if pylons = [
inspect.isclass(v) and v.__name__.startswith("Pylon")] v
for v in aircraft.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith("Pylon")
]
# And that Pylon class has members with irrelevant names that have # And that Pylon class has members with irrelevant names that have
# values of (pylon number, allowed weapon). # values of (pylon number, allowed weapon).
@ -117,112 +121,92 @@ _WEAPON_FALLBACKS = [
(Weapons.ADM_141A_, None), (Weapons.ADM_141A_, None),
(Weapons.ADM_141A__, None), (Weapons.ADM_141A__, None),
(Weapons.ADM_141B, None), (Weapons.ADM_141B, None),
# AGM-114K Hellfire # AGM-114K Hellfire
(Weapons.AGM114x2_OH_58, Weapons.M260_HYDRA), # assuming OH-58 and not MQ-9 (Weapons.AGM114x2_OH_58, Weapons.M260_HYDRA), # assuming OH-58 and not MQ-9
(Weapons.AGM_114K, None), # Only for RQ-1 (Weapons.AGM_114K, None), # Only for RQ-1
(Weapons.AGM_114K___4, Weapons.LAU_61___19_2_75__rockets_MK151_HE), (Weapons.AGM_114K___4, Weapons.LAU_61___19_2_75__rockets_MK151_HE),
# AGM-119 Penguin # AGM-119 Penguin
(Weapons.AGM_119B_Penguin, Weapons.Mk_82), (Weapons.AGM_119B_Penguin, Weapons.Mk_82),
# AGM-122 Sidearm # AGM-122 Sidearm
(Weapons.AGM_122, None), # No known aircraft carries this (Weapons.AGM_122, None), # No known aircraft carries this
(Weapons.AGM_122_Sidearm, Weapons.GBU_12), # outer pylons harrier (Weapons.AGM_122_Sidearm, Weapons.GBU_12), # outer pylons harrier
(Weapons.AGM_122_Sidearm_, Weapons.LAU_117_AGM_65E), # internal pylons harrier (Weapons.AGM_122_Sidearm_, Weapons.LAU_117_AGM_65E), # internal pylons harrier
# AGM-154 JSOW # AGM-154 JSOW
(Weapons.AGM_154A, Weapons.GBU_12), (Weapons.AGM_154A, Weapons.GBU_12),
(Weapons.BRU_55___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12), (Weapons.BRU_55___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12),
(Weapons.BRU_57___2_x_AGM_154A, None), # doesn't exist on any aircraft yet (Weapons.BRU_57___2_x_AGM_154A, None), # doesn't exist on any aircraft yet
(Weapons.AGM_154B, Weapons.CBU_105), (Weapons.AGM_154B, Weapons.CBU_105),
(Weapons.AGM_154C, Weapons.GBU_12), (Weapons.AGM_154C, Weapons.GBU_12),
(Weapons.AGM_154C_4, Weapons.GBU_31_8), (Weapons.AGM_154C_4, Weapons.GBU_31_8),
(Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12), (Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12),
# AGM-45 Shrike # AGM-45 Shrike
(Weapons.AGM_45A, None), (Weapons.AGM_45A, None),
(Weapons.AGM_45B, Weapons.AGM_45A), (Weapons.AGM_45B, Weapons.AGM_45A),
(Weapons.AGM_45B_, Weapons.AGM_45A), (Weapons.AGM_45B_, Weapons.AGM_45A),
# AGM-62 Walleye # AGM-62 Walleye
(Weapons.AGM_62, Weapons.Mk_84), (Weapons.AGM_62, Weapons.Mk_84),
# AGM-65 Maverick # AGM-65 Maverick
(Weapons.AGM_65D, None), # doesn't exist (Weapons.AGM_65D, None), # doesn't exist
(Weapons.AGM_65E, None), # doesn't exist (Weapons.AGM_65E, None), # doesn't exist
(Weapons.AGM_65K, None), # doesn't exist (Weapons.AGM_65K, None), # doesn't exist
(Weapons.LAU_117_AGM_65A, None), # doesn't exist (Weapons.LAU_117_AGM_65A, None), # doesn't exist
(Weapons.LAU_117_AGM_65B, None), # doesn't exist (Weapons.LAU_117_AGM_65B, None), # doesn't exist
(
(Weapons.LAU_117_AGM_65D, Weapons.AGM_62), # Walleye is the predecessor to the maverick Weapons.LAU_117_AGM_65D,
Weapons.AGM_62,
), # Walleye is the predecessor to the maverick
(Weapons.LAU_117_AGM_65E, None), (Weapons.LAU_117_AGM_65E, None),
(Weapons.LAU_117_AGM_65F, Weapons.LAU_117_AGM_65D), (Weapons.LAU_117_AGM_65F, Weapons.LAU_117_AGM_65D),
(Weapons.LAU_117_AGM_65G, Weapons.LAU_117_AGM_65D), (Weapons.LAU_117_AGM_65G, Weapons.LAU_117_AGM_65D),
(Weapons.LAU_117_AGM_65H, Weapons.LAU_117_AGM_65D), (Weapons.LAU_117_AGM_65H, Weapons.LAU_117_AGM_65D),
(Weapons.LAU_117_AGM_65K, Weapons.LAU_117_AGM_65D), (Weapons.LAU_117_AGM_65K, Weapons.LAU_117_AGM_65D),
(Weapons.LAU_117_AGM_65L, None), (Weapons.LAU_117_AGM_65L, None),
(Weapons.LAU_88_AGM_65D_2, None), (Weapons.LAU_88_AGM_65D_2, None),
(Weapons.LAU_88_AGM_65D_2_, None), (Weapons.LAU_88_AGM_65D_2_, None),
(Weapons.LAU_88_AGM_65D_3, None), (Weapons.LAU_88_AGM_65D_3, None),
(Weapons.LAU_88_AGM_65D_ONE, None), (Weapons.LAU_88_AGM_65D_ONE, None),
(Weapons.LAU_88_AGM_65E_2, Weapons.LAU_88_AGM_65D_2), (Weapons.LAU_88_AGM_65E_2, Weapons.LAU_88_AGM_65D_2),
(Weapons.LAU_88_AGM_65E_2_, Weapons.LAU_88_AGM_65D_2_), (Weapons.LAU_88_AGM_65E_2_, Weapons.LAU_88_AGM_65D_2_),
(Weapons.LAU_88_AGM_65E_3, Weapons.LAU_88_AGM_65D_3), (Weapons.LAU_88_AGM_65E_3, Weapons.LAU_88_AGM_65D_3),
(Weapons.LAU_88_AGM_65H, Weapons.LAU_88_AGM_65D_2), (Weapons.LAU_88_AGM_65H, Weapons.LAU_88_AGM_65D_2),
(Weapons.LAU_88_AGM_65H_2_L, Weapons.LAU_88_AGM_65D_2_), (Weapons.LAU_88_AGM_65H_2_L, Weapons.LAU_88_AGM_65D_2_),
(Weapons.LAU_88_AGM_65H_2_R, Weapons.LAU_88_AGM_65D_2_), (Weapons.LAU_88_AGM_65H_2_R, Weapons.LAU_88_AGM_65D_2_),
(Weapons.LAU_88_AGM_65H_3, Weapons.LAU_88_AGM_65D_3), (Weapons.LAU_88_AGM_65H_3, Weapons.LAU_88_AGM_65D_3),
(Weapons.LAU_88_AGM_65K_2, Weapons.LAU_88_AGM_65D_2), (Weapons.LAU_88_AGM_65K_2, Weapons.LAU_88_AGM_65D_2),
(Weapons.LAU_88_AGM_65K_2_, Weapons.LAU_88_AGM_65D_2_), (Weapons.LAU_88_AGM_65K_2_, Weapons.LAU_88_AGM_65D_2_),
(Weapons.LAU_88_AGM_65K_3, Weapons.LAU_88_AGM_65D_3), (Weapons.LAU_88_AGM_65K_3, Weapons.LAU_88_AGM_65D_3),
# AGM-84 Harpoon # AGM-84 Harpoon
(Weapons.AGM_84, None), # doesn't exist (Weapons.AGM_84, None), # doesn't exist
(Weapons.AGM_84A, Weapons.Mk_82), (Weapons.AGM_84A, Weapons.Mk_82),
(Weapons.AGM_84A_8, Weapons._27_Mk_82), (Weapons.AGM_84A_8, Weapons._27_Mk_82),
(Weapons.AGM_84D, Weapons.AGM_62), (Weapons.AGM_84D, Weapons.AGM_62),
(Weapons.AGM_84E, Weapons.LAU_117_AGM_65F), (Weapons.AGM_84E, Weapons.LAU_117_AGM_65F),
(Weapons.AGM_84H, Weapons.AGM_84E), (Weapons.AGM_84H, Weapons.AGM_84E),
# AGM-86 ALCM # AGM-86 ALCM
(Weapons.AGM_86C, Weapons._27_Mk_82), (Weapons.AGM_86C, Weapons._27_Mk_82),
(Weapons.AGM_86C_20, Weapons._27_Mk_82), (Weapons.AGM_86C_20, Weapons._27_Mk_82),
(Weapons.AGM_86C_8, Weapons._27_Mk_82), (Weapons.AGM_86C_8, Weapons._27_Mk_82),
(Weapons.MER_6_AGM_86C, Weapons.MER_12_Mk_82), (Weapons.MER_6_AGM_86C, Weapons.MER_12_Mk_82),
# AGM-88 HARM # AGM-88 HARM
(Weapons.AGM_88C, Weapons.AGM_65D), (Weapons.AGM_88C, Weapons.AGM_65D),
(Weapons.AGM_88C_, Weapons.AGM_65D), (Weapons.AGM_88C_, Weapons.AGM_65D),
# AIM-120 AMRAAM
# AIM-120 AMRAAM
(Weapons.AIM_120B, Weapons.AIM_7MH), (Weapons.AIM_120B, Weapons.AIM_7MH),
(Weapons.LAU_115___AIM_120B, Weapons.LAU_115C_AIM_7MH), (Weapons.LAU_115___AIM_120B, Weapons.LAU_115C_AIM_7MH),
(Weapons.LAU_115_2_LAU_127_AIM_120B, Weapons.LAU_115C_AIM_7MH), (Weapons.LAU_115_2_LAU_127_AIM_120B, Weapons.LAU_115C_AIM_7MH),
(Weapons.AIM_120C, Weapons.AIM_120B), (Weapons.AIM_120C, Weapons.AIM_120B),
(Weapons.LAU_115___AIM_120C, Weapons.LAU_115___AIM_120B), (Weapons.LAU_115___AIM_120C, Weapons.LAU_115___AIM_120B),
(Weapons.LAU_115_2_LAU_127_AIM_120C, Weapons.LAU_115_2_LAU_127_AIM_120B), (Weapons.LAU_115_2_LAU_127_AIM_120C, Weapons.LAU_115_2_LAU_127_AIM_120B),
# AIM-54 Phoenix # AIM-54 Phoenix
(Weapons.AIM_54A_Mk47, None), (Weapons.AIM_54A_Mk47, None),
(Weapons.AIM_54A_Mk47_, None), (Weapons.AIM_54A_Mk47_, None),
(Weapons.AIM_54A_Mk47__, None), (Weapons.AIM_54A_Mk47__, None),
(Weapons.AIM_54A_Mk60, Weapons.AIM_54A_Mk47), (Weapons.AIM_54A_Mk60, Weapons.AIM_54A_Mk47),
(Weapons.AIM_54A_Mk60_, Weapons.AIM_54A_Mk47_), (Weapons.AIM_54A_Mk60_, Weapons.AIM_54A_Mk47_),
(Weapons.AIM_54A_Mk60__, Weapons.AIM_54A_Mk47__), (Weapons.AIM_54A_Mk60__, Weapons.AIM_54A_Mk47__),
(Weapons.AIM_54C_Mk47, Weapons.AIM_54A_Mk60), (Weapons.AIM_54C_Mk47, Weapons.AIM_54A_Mk60),
(Weapons.AIM_54C_Mk47_, Weapons.AIM_54A_Mk60_), (Weapons.AIM_54C_Mk47_, Weapons.AIM_54A_Mk60_),
(Weapons.AIM_54C_Mk47__, Weapons.AIM_54A_Mk60__), (Weapons.AIM_54C_Mk47__, Weapons.AIM_54A_Mk60__),
# AIM-7 Sparrow # AIM-7 Sparrow
(Weapons.AIM_7E, None), (Weapons.AIM_7E, None),
(Weapons.AIM_7F, Weapons.AIM_7E), (Weapons.AIM_7F, Weapons.AIM_7E),
@ -234,62 +218,61 @@ _WEAPON_FALLBACKS = [
(Weapons.AIM_7MH, Weapons.AIM_7M), (Weapons.AIM_7MH, Weapons.AIM_7M),
(Weapons.AIM_7MH_, Weapons.AIM_7M_), (Weapons.AIM_7MH_, Weapons.AIM_7M_),
(Weapons.AIM_7MH__, Weapons.AIM_7M__), (Weapons.AIM_7MH__, Weapons.AIM_7M__),
(Weapons.LAU_115C_AIM_7E, None), (Weapons.LAU_115C_AIM_7E, None),
(Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E), (Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E),
(Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F), (Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F),
(Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M), (Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M),
# AIM-9 Sidewinder # AIM-9 Sidewinder
(Weapons.AIM_9L_Sidewinder_IR_AAM, None), (Weapons.AIM_9L_Sidewinder_IR_AAM, None),
(Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM), (Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM),
(Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM), (Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM),
(Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM), (Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM),
(Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM), (Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM),
(Weapons.LAU_105_1_AIM_9L_L, None), (Weapons.LAU_105_1_AIM_9L_L, None),
(Weapons.LAU_105_1_AIM_9L_R, None), (Weapons.LAU_105_1_AIM_9L_R, None),
(Weapons.LAU_105_1_AIM_9M_L, Weapons.LAU_105_1_AIM_9L_L), (Weapons.LAU_105_1_AIM_9M_L, Weapons.LAU_105_1_AIM_9L_L),
(Weapons.LAU_105_1_AIM_9M_R, Weapons.LAU_105_1_AIM_9L_R), (Weapons.LAU_105_1_AIM_9M_R, Weapons.LAU_105_1_AIM_9L_R),
(Weapons.LAU_105_2_AIM_9L, None), (Weapons.LAU_105_2_AIM_9L, None),
(Weapons.LAU_105_2_AIM_9P5, Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM), (Weapons.LAU_105_2_AIM_9P5, Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM),
(Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_105_2_AIM_9L), (Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_105_2_AIM_9L),
(Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM), (
Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM,
Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM,
),
(Weapons.LAU_115_2_LAU_127_AIM_9L, None), (Weapons.LAU_115_2_LAU_127_AIM_9L, None),
(Weapons.LAU_115_2_LAU_127_AIM_9M, Weapons.LAU_115_2_LAU_127_AIM_9L), (Weapons.LAU_115_2_LAU_127_AIM_9M, Weapons.LAU_115_2_LAU_127_AIM_9L),
(Weapons.LAU_115_2_LAU_127_AIM_9X, Weapons.LAU_115_2_LAU_127_AIM_9M), (Weapons.LAU_115_2_LAU_127_AIM_9X, Weapons.LAU_115_2_LAU_127_AIM_9M),
(Weapons.LAU_115_LAU_127_AIM_9L, None), (Weapons.LAU_115_LAU_127_AIM_9L, None),
(Weapons.LAU_115_LAU_127_AIM_9M, Weapons.LAU_115_LAU_127_AIM_9L), (Weapons.LAU_115_LAU_127_AIM_9M, Weapons.LAU_115_LAU_127_AIM_9L),
(Weapons.LAU_115_LAU_127_AIM_9X, Weapons.LAU_115_LAU_127_AIM_9M), (Weapons.LAU_115_LAU_127_AIM_9X, Weapons.LAU_115_LAU_127_AIM_9M),
(Weapons.LAU_127_AIM_9L, None), (Weapons.LAU_127_AIM_9L, None),
(Weapons.LAU_127_AIM_9M, Weapons.LAU_127_AIM_9L), (Weapons.LAU_127_AIM_9M, Weapons.LAU_127_AIM_9L),
(Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M), (Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M),
(Weapons.LAU_138_AIM_9L, None), (Weapons.LAU_138_AIM_9L, None),
(Weapons.LAU_138_AIM_9M, Weapons.LAU_138_AIM_9L), (Weapons.LAU_138_AIM_9M, Weapons.LAU_138_AIM_9L),
(Weapons.LAU_7_AIM_9L, None), (Weapons.LAU_7_AIM_9L, None),
(Weapons.LAU_7_AIM_9M, Weapons.LAU_7_AIM_9L), (Weapons.LAU_7_AIM_9M, Weapons.LAU_7_AIM_9L),
(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM), (Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM),
(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM), (Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM),
(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9L), (Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9L),
(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM), (Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM),
(Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM, None), (Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM, None),
(Weapons.LAU_7___2_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM), (
(Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM), Weapons.LAU_7___2_AIM_9M_Sidewinder_IR_AAM,
(Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM), Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM,
),
(
Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM,
Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM,
),
(
Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM,
Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM,
),
# ALQ ECM Pods # ALQ ECM Pods
(Weapons.ALQ_131, None), (Weapons.ALQ_131, None),
(Weapons.ALQ_184, Weapons.ALQ_131), (Weapons.ALQ_184, Weapons.ALQ_131),
(Weapons.AN_ALQ_164_DECM_Pod, None), (Weapons.AN_ALQ_164_DECM_Pod, None),
# TGP Pods # TGP Pods
(Weapons.AN_AAQ_28_LITENING_, None), (Weapons.AN_AAQ_28_LITENING_, None),
(Weapons.AN_AAQ_28_LITENING, Weapons.Lantirn_F_16), (Weapons.AN_AAQ_28_LITENING, Weapons.Lantirn_F_16),
@ -300,17 +283,14 @@ _WEAPON_FALLBACKS = [
(Weapons.Lantirn_F_16, None), (Weapons.Lantirn_F_16, None),
(Weapons.Lantirn_Target_Pod, None), (Weapons.Lantirn_Target_Pod, None),
(Weapons.Pavetack_F_111, None), (Weapons.Pavetack_F_111, None),
# BLU-107 # BLU-107
(Weapons.BLU_107, None), (Weapons.BLU_107, None),
(Weapons.MER_6_BLU_107, Weapons.MER_6_Mk_82), (Weapons.MER_6_BLU_107, Weapons.MER_6_Mk_82),
# GBU-10 LGB # GBU-10 LGB
(Weapons.DIS_GBU_10, Weapons.Mk_84), (Weapons.DIS_GBU_10, Weapons.Mk_84),
(Weapons.GBU_10, Weapons.Mk_84), (Weapons.GBU_10, Weapons.Mk_84),
(Weapons.GBU_10_, Weapons.Mk_84), (Weapons.GBU_10_, Weapons.Mk_84),
(Weapons.GBU_10_2, Weapons.Mk_84), (Weapons.GBU_10_2, Weapons.Mk_84),
# GBU-12 LGB # GBU-12 LGB
(Weapons.AUF2_GBU_12_x_2, None), (Weapons.AUF2_GBU_12_x_2, None),
(Weapons.BRU_33___2_x_GBU_12, Weapons.BRU_33___2_x_Mk_82_), (Weapons.BRU_33___2_x_GBU_12, Weapons.BRU_33___2_x_Mk_82_),
@ -328,29 +308,23 @@ _WEAPON_FALLBACKS = [
(Weapons._2_GBU_12, Weapons._2_Mk_82), (Weapons._2_GBU_12, Weapons._2_Mk_82),
(Weapons._2_GBU_12_, Weapons._2_Mk_82_), (Weapons._2_GBU_12_, Weapons._2_Mk_82_),
(Weapons._3_GBU_12, Weapons._3_Mk_82_), (Weapons._3_GBU_12, Weapons._3_Mk_82_),
# GBU-15 LGB # GBU-15 LGB
(Weapons.GBU_15, Weapons.Mk_84), (Weapons.GBU_15, Weapons.Mk_84),
# GBU-16 LGB # GBU-16 LGB
(Weapons.BRU_33___2_x_GBU_16, None), (Weapons.BRU_33___2_x_GBU_16, None),
(Weapons.DIS_GBU_16, Weapons.Mk_83), (Weapons.DIS_GBU_16, Weapons.Mk_83),
(Weapons.GBU_16, Weapons.Mk_83), (Weapons.GBU_16, Weapons.Mk_83),
(Weapons.GBU_16_, Weapons.Mk_83_), (Weapons.GBU_16_, Weapons.Mk_83_),
# GBU-24 LGB # GBU-24 LGB
(Weapons.GBU_24, Weapons.GBU_10), (Weapons.GBU_24, Weapons.GBU_10),
(Weapons.GBU_24_, Weapons.GBU_10_), (Weapons.GBU_24_, Weapons.GBU_10_),
(Weapons.GBU_24__, Weapons.GBU_10_), (Weapons.GBU_24__, Weapons.GBU_10_),
# GBU-27 LGB # GBU-27 LGB
(Weapons.GBU_24, Weapons.GBU_10), (Weapons.GBU_24, Weapons.GBU_10),
(Weapons.GBU_24_, Weapons.GBU_10_), (Weapons.GBU_24_, Weapons.GBU_10_),
(Weapons.GBU_24__, Weapons.GBU_10_), (Weapons.GBU_24__, Weapons.GBU_10_),
# GBU-28 LGB # GBU-28 LGB
(Weapons.GBU_28, Weapons.GBU_15), (Weapons.GBU_28, Weapons.GBU_15),
# GBU-31 JDAM # GBU-31 JDAM
(Weapons.GBU_31V3B_8, Weapons.B_1B_Mk_84_8), (Weapons.GBU_31V3B_8, Weapons.B_1B_Mk_84_8),
(Weapons.GBU_31_8, Weapons.B_1B_Mk_84_8), (Weapons.GBU_31_8, Weapons.B_1B_Mk_84_8),
@ -358,150 +332,127 @@ _WEAPON_FALLBACKS = [
(Weapons.GBU_31_V_2_B, Weapons.GBU_24_), (Weapons.GBU_31_V_2_B, Weapons.GBU_24_),
(Weapons.GBU_31_V_3_B, Weapons.GBU_24_), (Weapons.GBU_31_V_3_B, Weapons.GBU_24_),
(Weapons.GBU_31_V_4_B, Weapons.GBU_24_), (Weapons.GBU_31_V_4_B, Weapons.GBU_24_),
# GBU-32 JDAM # GBU-32 JDAM
(Weapons.GBU_32_V_2_B, Weapons.GBU_16), (Weapons.GBU_32_V_2_B, Weapons.GBU_16),
# GBU-32 JDAM # GBU-32 JDAM
(Weapons.BRU_55___2_x_GBU_38, Weapons.BRU_33___2_x_Mk_82_), (Weapons.BRU_55___2_x_GBU_38, Weapons.BRU_33___2_x_Mk_82_),
(Weapons.BRU_57___2_x_GBU_38, None), # Doesn't exist (Weapons.BRU_57___2_x_GBU_38, None), # Doesn't exist
(Weapons.GBU_38, Weapons.Mk_82), (Weapons.GBU_38, Weapons.Mk_82),
(Weapons.GBU_38_16, Weapons.MK_82_28), (Weapons.GBU_38_16, Weapons.MK_82_28),
(Weapons._2_GBU_38, Weapons._2_Mk_82), (Weapons._2_GBU_38, Weapons._2_Mk_82),
(Weapons._2_GBU_38_, Weapons._2_Mk_82_), (Weapons._2_GBU_38_, Weapons._2_Mk_82_),
(Weapons._3_GBU_38, Weapons._3_Mk_82_), (Weapons._3_GBU_38, Weapons._3_Mk_82_),
# GBU-54 LJDAM # GBU-54 LJDAM
(Weapons.GBU_54_V_1_B, Weapons.GBU_38), (Weapons.GBU_54_V_1_B, Weapons.GBU_38),
(Weapons._2_GBU_54_V_1_B, Weapons._2_GBU_38), (Weapons._2_GBU_54_V_1_B, Weapons._2_GBU_38),
(Weapons._2_GBU_54_V_1_B_, Weapons._2_GBU_38_), (Weapons._2_GBU_54_V_1_B_, Weapons._2_GBU_38_),
(Weapons._3_GBU_54_V_1_B, Weapons._3_GBU_38), (Weapons._3_GBU_54_V_1_B, Weapons._3_GBU_38),
# CBU-52 # CBU-52
(Weapons.CBU_52B, None), (Weapons.CBU_52B, None),
# CBU-87 CEM # CBU-87 CEM
(Weapons.CBU_87, Weapons.Mk_82), (Weapons.CBU_87, Weapons.Mk_82),
(Weapons.TER_9A___2_x_CBU_87, Weapons.TER_9A___2_x_Mk_82), (Weapons.TER_9A___2_x_CBU_87, Weapons.TER_9A___2_x_Mk_82),
(Weapons.TER_9A___2_x_CBU_87_, Weapons.TER_9A___2_x_Mk_82_), (Weapons.TER_9A___2_x_CBU_87_, Weapons.TER_9A___2_x_Mk_82_),
(Weapons.TER_9A___3_x_CBU_87, Weapons.TER_9A___3_x_Mk_82), (Weapons.TER_9A___3_x_CBU_87, Weapons.TER_9A___3_x_Mk_82),
# CBU-97 # CBU-97
(Weapons.CBU_97, Weapons.Mk_82), (Weapons.CBU_97, Weapons.Mk_82),
(Weapons.TER_9A___2_x_CBU_97, Weapons.TER_9A___2_x_Mk_82), (Weapons.TER_9A___2_x_CBU_97, Weapons.TER_9A___2_x_Mk_82),
(Weapons.TER_9A___2_x_CBU_97_, Weapons.TER_9A___2_x_Mk_82_), (Weapons.TER_9A___2_x_CBU_97_, Weapons.TER_9A___2_x_Mk_82_),
(Weapons.TER_9A___3_x_CBU_97, Weapons.TER_9A___3_x_Mk_82), (Weapons.TER_9A___3_x_CBU_97, Weapons.TER_9A___3_x_Mk_82),
# CBU-99 (It's a bomb made in 1968, I'm not bothering right now with backups) # CBU-99 (It's a bomb made in 1968, I'm not bothering right now with backups)
# CBU-103 # CBU-103
(Weapons.BRU_57___2_x_CBU_103, None), # doesn't exist... (Weapons.BRU_57___2_x_CBU_103, None), # doesn't exist...
(Weapons.CBU_103, Weapons.CBU_87), (Weapons.CBU_103, Weapons.CBU_87),
# CBU-105 # CBU-105
(Weapons.BRU_57___2_x_CBU_105, None), # doesn't exist... (Weapons.BRU_57___2_x_CBU_105, None), # doesn't exist...
(Weapons.CBU_105, Weapons.CBU_97), (Weapons.CBU_105, Weapons.CBU_97),
(
(Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_APKWS, Weapons.LAU_131___7_2_75__rockets_M151__HE_), Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_APKWS,
(Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M282___MPP_APKWS, Weapons.LAU_131___7_2_75__rockets_M151__HE_), Weapons.LAU_131___7_2_75__rockets_M151__HE_,
(Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M151___HE_APKWS, Weapons.LAU_68_3___7_2_75__rockets_M151__HE_), ),
(Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M282___MPP_APKWS, Weapons.LAU_68_3___7_2_75__rockets_M151__HE_), (
Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M282___MPP_APKWS,
Weapons.LAU_131___7_2_75__rockets_M151__HE_,
),
(
Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M151___HE_APKWS,
Weapons.LAU_68_3___7_2_75__rockets_M151__HE_,
),
(
Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M282___MPP_APKWS,
Weapons.LAU_68_3___7_2_75__rockets_M151__HE_,
),
# Russia # Russia
# KAB-1500 # KAB-1500
(Weapons.KAB_1500Kr, None), (Weapons.KAB_1500Kr, None),
(Weapons.KAB_1500LG_Pr, Weapons.KAB_1500Kr), (Weapons.KAB_1500LG_Pr, Weapons.KAB_1500Kr),
(Weapons.KAB_1500L, Weapons.KAB_1500LG_Pr), (Weapons.KAB_1500L, Weapons.KAB_1500LG_Pr),
# KAB-500 # KAB-500
(Weapons.KAB_500kr, Weapons.FAB_500_M62), (Weapons.KAB_500kr, Weapons.FAB_500_M62),
(Weapons.KAB_500L, Weapons.KAB_500kr), (Weapons.KAB_500L, Weapons.KAB_500kr),
(Weapons.KAB_500S, Weapons.KAB_500L), (Weapons.KAB_500S, Weapons.KAB_500L),
# KH Series # KH Series
(Weapons.Kh_22N, None), (Weapons.Kh_22N, None),
(Weapons.Kh_23L, None), (Weapons.Kh_23L, None),
(Weapons.Kh_25ML, None), (Weapons.Kh_25ML, None),
(Weapons.Kh_25ML_, None), (Weapons.Kh_25ML_, None),
(Weapons.Kh_25ML__, None), (Weapons.Kh_25ML__, None),
(Weapons.Kh_25MP, None), (Weapons.Kh_25MP, None),
(Weapons.Kh_25MPU, Weapons.Kh_25MP), (Weapons.Kh_25MPU, Weapons.Kh_25MP),
(Weapons.Kh_25MR, None), (Weapons.Kh_25MR, None),
(Weapons.Kh_25MR_, None), (Weapons.Kh_25MR_, None),
(Weapons.Kh_28__AS_9_Kyle_, None), (Weapons.Kh_28__AS_9_Kyle_, None),
(Weapons.Kh_29L, Weapons.Kh_25ML), (Weapons.Kh_29L, Weapons.Kh_25ML),
(Weapons.Kh_29L_, Weapons.Kh_25ML_), (Weapons.Kh_29L_, Weapons.Kh_25ML_),
(Weapons.Kh_29L__, Weapons.Kh_25ML__), (Weapons.Kh_29L__, Weapons.Kh_25ML__),
(Weapons.Kh_29T, Weapons.Kh_25MR), (Weapons.Kh_29T, Weapons.Kh_25MR),
(Weapons.Kh_29T_, Weapons.Kh_25MR_), (Weapons.Kh_29T_, Weapons.Kh_25MR_),
(Weapons.Kh_29T_, Weapons.Kh_25MR_), (Weapons.Kh_29T_, Weapons.Kh_25MR_),
(Weapons.Kh_31A, None), (Weapons.Kh_31A, None),
(Weapons.Kh_31A_, None), (Weapons.Kh_31A_, None),
(Weapons.Kh_31A__, None), (Weapons.Kh_31A__, None),
(Weapons.Kh_31P, Weapons.Kh_25MP), (Weapons.Kh_31P, Weapons.Kh_25MP),
(Weapons.Kh_31P_, Weapons.Kh_25MP), (Weapons.Kh_31P_, Weapons.Kh_25MP),
(Weapons.Kh_31P__, Weapons.Kh_25MP), (Weapons.Kh_31P__, Weapons.Kh_25MP),
(Weapons.Kh_35, Weapons.Kh_31A), (Weapons.Kh_35, Weapons.Kh_31A),
(Weapons.Kh_35_, Weapons.Kh_31A_), (Weapons.Kh_35_, Weapons.Kh_31A_),
(Weapons.Kh_35_6, None), (Weapons.Kh_35_6, None),
(Weapons.Kh_41, None), (Weapons.Kh_41, None),
(Weapons.Kh_58U, Weapons.Kh_31P), (Weapons.Kh_58U, Weapons.Kh_31P),
(Weapons.Kh_58U_, Weapons.Kh_31P_), (Weapons.Kh_58U_, Weapons.Kh_31P_),
(Weapons.Kh_59M, Weapons.Kh_31A), (Weapons.Kh_59M, Weapons.Kh_31A),
(Weapons.Kh_65, None), (Weapons.Kh_65, None),
(Weapons.Kh_65_6, None), (Weapons.Kh_65_6, None),
(Weapons.Kh_65_8, None), (Weapons.Kh_65_8, None),
(Weapons.Kh_66_Grom__21__APU_68, None), (Weapons.Kh_66_Grom__21__APU_68, None),
# ECM # ECM
(Weapons.L175V_Khibiny_ECM_pod, None), (Weapons.L175V_Khibiny_ECM_pod, None),
# R-13 # R-13
(Weapons.R_13M, None), (Weapons.R_13M, None),
(Weapons.R_13M1, Weapons.R_13M), (Weapons.R_13M1, Weapons.R_13M),
# R-24 # R-24
(Weapons.R_24R, None), (Weapons.R_24R, None),
(Weapons.R_24T, None), (Weapons.R_24T, None),
# R-27 # R-27
(Weapons.R_27T, Weapons.R_24T), (Weapons.R_27T, Weapons.R_24T),
(Weapons.R_27R, Weapons.R_24R), (Weapons.R_27R, Weapons.R_24R),
(Weapons.R_27ER, Weapons.R_27R), (Weapons.R_27ER, Weapons.R_27R),
(Weapons.R_27ET, Weapons.R_27T), (Weapons.R_27ET, Weapons.R_27T),
# R-33 # R-33
(Weapons.R_33, None), (Weapons.R_33, None),
# R-3 # R-3
(Weapons.R_3S, Weapons.R_13M), (Weapons.R_3S, Weapons.R_13M),
(Weapons.R_3R, Weapons.R_3S), (Weapons.R_3R, Weapons.R_3S),
# R-40 # R-40
(Weapons.R_40R, None), (Weapons.R_40R, None),
(Weapons.R_40T, None), (Weapons.R_40T, None),
# R-55 # R-55
(Weapons.R_55, None), (Weapons.R_55, None),
(Weapons.RS2US, None), (Weapons.RS2US, None),
# R-60 # R-60
(Weapons.R_60, Weapons.R_13M1), (Weapons.R_60, Weapons.R_13M1),
(Weapons.R_60_x_2, Weapons.R_13M1), (Weapons.R_60_x_2, Weapons.R_13M1),
(Weapons.R_60_x_2_, Weapons.R_13M1), (Weapons.R_60_x_2_, Weapons.R_13M1),
(Weapons.APU_60_1_R_60M, Weapons.R_3S), (Weapons.APU_60_1_R_60M, Weapons.R_3S),
(Weapons.R_60M, Weapons.R_60), (Weapons.R_60M, Weapons.R_60),
(Weapons.R_60M_, Weapons.R_60), (Weapons.R_60M_, Weapons.R_60),
@ -509,44 +460,39 @@ _WEAPON_FALLBACKS = [
(Weapons.R_60M_2_, Weapons.R_60M), (Weapons.R_60M_2_, Weapons.R_60M),
(Weapons.R_60M_x_2, Weapons.R_60M), (Weapons.R_60M_x_2, Weapons.R_60M),
(Weapons.R_60M_x_2_, Weapons.R_60M), (Weapons.R_60M_x_2_, Weapons.R_60M),
# R-73 # R-73
(Weapons.R_73, Weapons.R_60M), (Weapons.R_73, Weapons.R_60M),
(Weapons.R_73_, None), (Weapons.R_73_, None),
# R-77 # R-77
(Weapons.R_77, Weapons.R_27ER), (Weapons.R_77, Weapons.R_27ER),
(Weapons.R_77_, None), (Weapons.R_77_, None),
# UK # UK
# ALARM # ALARM
(Weapons.ALARM, None), (Weapons.ALARM, None),
(Weapons.ALARM_2, None), (Weapons.ALARM_2, None),
# France # France
# BLG-66 Belouga # BLG-66 Belouga
(Weapons.AUF2_BLG_66_AC_x_2, Weapons.AUF2_MK_82_x_2), (Weapons.AUF2_BLG_66_AC_x_2, Weapons.AUF2_MK_82_x_2),
(Weapons.BLG_66_AC_Belouga, Weapons.Mk_82), (Weapons.BLG_66_AC_Belouga, Weapons.Mk_82),
(Weapons.BLG_66_AC_Belouga_, Weapons.Mk_82), (Weapons.BLG_66_AC_Belouga_, Weapons.Mk_82),
# HOT-3 # HOT-3
(Weapons.HOT3, None), (Weapons.HOT3, None),
(Weapons.HOT3_, None), (Weapons.HOT3_, None),
# Magic 2 # Magic 2
(Weapons.Matra_Magic_II, None), (Weapons.Matra_Magic_II, None),
(Weapons.R_550_Magic_2, None), (Weapons.R_550_Magic_2, None),
# Super 530D # Super 530D
(Weapons.Matra_Super_530D, Weapons.Matra_Magic_II), (Weapons.Matra_Super_530D, Weapons.Matra_Magic_II),
(Weapons.Super_530D, None) (Weapons.Super_530D, None),
] ]
WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict( WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict(
lambda: cast(Optional[Weapon], None), lambda: cast(Optional[Weapon], None),
((Weapon.from_pydcs(a), b if b is None else Weapon.from_pydcs(b)) (
for a, b in _WEAPON_FALLBACKS)) (Weapon.from_pydcs(a), b if b is None else Weapon.from_pydcs(b))
for a, b in _WEAPON_FALLBACKS
),
)
WEAPON_INTRODUCTION_YEARS = { WEAPON_INTRODUCTION_YEARS = {
@ -556,44 +502,34 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.ADM_141A_): 1987, Weapon.from_pydcs(Weapons.ADM_141A_): 1987,
Weapon.from_pydcs(Weapons.ADM_141A__): 1987, Weapon.from_pydcs(Weapons.ADM_141A__): 1987,
Weapon.from_pydcs(Weapons.ADM_141B): 1987, Weapon.from_pydcs(Weapons.ADM_141B): 1987,
# AGM-114K Hellfire # AGM-114K Hellfire
Weapon.from_pydcs(Weapons.AGM114x2_OH_58): 1993, Weapon.from_pydcs(Weapons.AGM114x2_OH_58): 1993,
Weapon.from_pydcs(Weapons.AGM_114K): 1993, Weapon.from_pydcs(Weapons.AGM_114K): 1993,
Weapon.from_pydcs(Weapons.AGM_114K___4): 1993, Weapon.from_pydcs(Weapons.AGM_114K___4): 1993,
# AGM-119 Penguin # AGM-119 Penguin
Weapon.from_pydcs(Weapons.AGM_119B_Penguin): 1972, Weapon.from_pydcs(Weapons.AGM_119B_Penguin): 1972,
# AGM-122 Sidearm # AGM-122 Sidearm
Weapon.from_pydcs(Weapons.AGM_122): 1986, Weapon.from_pydcs(Weapons.AGM_122): 1986,
Weapon.from_pydcs(Weapons.AGM_122_Sidearm): 1986, Weapon.from_pydcs(Weapons.AGM_122_Sidearm): 1986,
Weapon.from_pydcs(Weapons.AGM_122_Sidearm_): 1986, Weapon.from_pydcs(Weapons.AGM_122_Sidearm_): 1986,
# AGM-154 JSOW # AGM-154 JSOW
Weapon.from_pydcs(Weapons.AGM_154A): 1998, Weapon.from_pydcs(Weapons.AGM_154A): 1998,
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154A): 1998, Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154A): 1998,
Weapon.from_pydcs(Weapons.BRU_57___2_x_AGM_154A): 1998, Weapon.from_pydcs(Weapons.BRU_57___2_x_AGM_154A): 1998,
Weapon.from_pydcs(Weapons.AGM_154B): 2005, Weapon.from_pydcs(Weapons.AGM_154B): 2005,
Weapon.from_pydcs(Weapons.AGM_154C): 2005, Weapon.from_pydcs(Weapons.AGM_154C): 2005,
Weapon.from_pydcs(Weapons.AGM_154C_4): 2005, Weapon.from_pydcs(Weapons.AGM_154C_4): 2005,
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005, Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005,
# AGM-45 Shrike # AGM-45 Shrike
Weapon.from_pydcs(Weapons.AGM_45A): 1965, Weapon.from_pydcs(Weapons.AGM_45A): 1965,
Weapon.from_pydcs(Weapons.AGM_45B): 1970, Weapon.from_pydcs(Weapons.AGM_45B): 1970,
Weapon.from_pydcs(Weapons.AGM_45B_): 1970, Weapon.from_pydcs(Weapons.AGM_45B_): 1970,
# AGM-62 Walleye # AGM-62 Walleye
Weapon.from_pydcs(Weapons.AGM_62): 1972, Weapon.from_pydcs(Weapons.AGM_62): 1972,
# AGM-65 Maverick # AGM-65 Maverick
Weapon.from_pydcs(Weapons.AGM_65D): 1983, Weapon.from_pydcs(Weapons.AGM_65D): 1983,
Weapon.from_pydcs(Weapons.AGM_65E): 1985, Weapon.from_pydcs(Weapons.AGM_65E): 1985,
Weapon.from_pydcs(Weapons.AGM_65K): 2007, Weapon.from_pydcs(Weapons.AGM_65K): 2007,
Weapon.from_pydcs(Weapons.LAU_117_AGM_65A): 1972, Weapon.from_pydcs(Weapons.LAU_117_AGM_65A): 1972,
Weapon.from_pydcs(Weapons.LAU_117_AGM_65B): 1972, Weapon.from_pydcs(Weapons.LAU_117_AGM_65B): 1972,
Weapon.from_pydcs(Weapons.LAU_117_AGM_65D): 1986, Weapon.from_pydcs(Weapons.LAU_117_AGM_65D): 1986,
@ -603,25 +539,20 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.LAU_117_AGM_65H): 2002, Weapon.from_pydcs(Weapons.LAU_117_AGM_65H): 2002,
Weapon.from_pydcs(Weapons.LAU_117_AGM_65K): 2002, Weapon.from_pydcs(Weapons.LAU_117_AGM_65K): 2002,
Weapon.from_pydcs(Weapons.LAU_117_AGM_65L): 1985, Weapon.from_pydcs(Weapons.LAU_117_AGM_65L): 1985,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_2): 1983, Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_2): 1983,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_2_): 1983, Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_2_): 1983,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_3): 1983, Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_3): 1983,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_ONE): 1983, Weapon.from_pydcs(Weapons.LAU_88_AGM_65D_ONE): 1983,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_2): 1985, Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_2): 1985,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_2_): 1985, Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_2_): 1985,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_3): 1985, Weapon.from_pydcs(Weapons.LAU_88_AGM_65E_3): 1985,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65H): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65H): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_2_L): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_2_L): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_2_R): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_2_R): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_3): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65H_3): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_2): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_2): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_2_): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_2_): 2007,
Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_3): 2007, Weapon.from_pydcs(Weapons.LAU_88_AGM_65K_3): 2007,
# AGM-84 Harpoon # AGM-84 Harpoon
Weapon.from_pydcs(Weapons.AGM_84): 1979, Weapon.from_pydcs(Weapons.AGM_84): 1979,
Weapon.from_pydcs(Weapons.AGM_84A): 1979, Weapon.from_pydcs(Weapons.AGM_84A): 1979,
@ -630,41 +561,33 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.AGM_84E): 1990, Weapon.from_pydcs(Weapons.AGM_84E): 1990,
Weapon.from_pydcs(Weapons.AGM_84E_SLAM): 1990, Weapon.from_pydcs(Weapons.AGM_84E_SLAM): 1990,
Weapon.from_pydcs(Weapons.AGM_84H): 1998, Weapon.from_pydcs(Weapons.AGM_84H): 1998,
# AGM-86 ALCM # AGM-86 ALCM
Weapon.from_pydcs(Weapons.AGM_86C): 1986, Weapon.from_pydcs(Weapons.AGM_86C): 1986,
Weapon.from_pydcs(Weapons.AGM_86C_20): 1986, Weapon.from_pydcs(Weapons.AGM_86C_20): 1986,
Weapon.from_pydcs(Weapons.AGM_86C_8): 1986, Weapon.from_pydcs(Weapons.AGM_86C_8): 1986,
Weapon.from_pydcs(Weapons.MER_6_AGM_86C): 1986, Weapon.from_pydcs(Weapons.MER_6_AGM_86C): 1986,
# AGM-88 HARM # AGM-88 HARM
Weapon.from_pydcs(Weapons.AGM_88C): 1983, Weapon.from_pydcs(Weapons.AGM_88C): 1983,
Weapon.from_pydcs(Weapons.AGM_88C_): 1983, Weapon.from_pydcs(Weapons.AGM_88C_): 1983,
# for future reference: 1983 is the A model IOC. B model in 1986 and C model in 1994. # for future reference: 1983 is the A model IOC. B model in 1986 and C model in 1994.
# AIM-120 AMRAAM # AIM-120 AMRAAM
Weapon.from_pydcs(Weapons.AIM_120B): 1994, Weapon.from_pydcs(Weapons.AIM_120B): 1994,
Weapon.from_pydcs(Weapons.AIM_120C): 1996, Weapon.from_pydcs(Weapons.AIM_120C): 1996,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120B): 1994, Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120B): 1994,
Weapon.from_pydcs(Weapons.LAU_115___AIM_120B): 1994, Weapon.from_pydcs(Weapons.LAU_115___AIM_120B): 1994,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996, Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996,
Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996, Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996,
# AIM-54 Phoenix # AIM-54 Phoenix
Weapon.from_pydcs(Weapons.AIM_54A_Mk47): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk47): 1974,
Weapon.from_pydcs(Weapons.AIM_54A_Mk47_): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk47_): 1974,
Weapon.from_pydcs(Weapons.AIM_54A_Mk47__): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk47__): 1974,
Weapon.from_pydcs(Weapons.AIM_54A_Mk60): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk60): 1974,
Weapon.from_pydcs(Weapons.AIM_54A_Mk60_): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk60_): 1974,
Weapon.from_pydcs(Weapons.AIM_54A_Mk60__): 1974, Weapon.from_pydcs(Weapons.AIM_54A_Mk60__): 1974,
Weapon.from_pydcs(Weapons.AIM_54C): 1974, Weapon.from_pydcs(Weapons.AIM_54C): 1974,
Weapon.from_pydcs(Weapons.AIM_54C_Mk47): 1974, Weapon.from_pydcs(Weapons.AIM_54C_Mk47): 1974,
Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1974, Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1974,
Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1974, Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1974,
# AIM-7 Sparrow # AIM-7 Sparrow
Weapon.from_pydcs(Weapons.AIM_7E): 1963, Weapon.from_pydcs(Weapons.AIM_7E): 1963,
Weapon.from_pydcs(Weapons.AIM_7F): 1976, Weapon.from_pydcs(Weapons.AIM_7F): 1976,
@ -676,62 +599,49 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.AIM_7MH): 1987, Weapon.from_pydcs(Weapons.AIM_7MH): 1987,
Weapon.from_pydcs(Weapons.AIM_7MH_): 1987, Weapon.from_pydcs(Weapons.AIM_7MH_): 1987,
Weapon.from_pydcs(Weapons.AIM_7MH__): 1987, Weapon.from_pydcs(Weapons.AIM_7MH__): 1987,
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7E): 1963, Weapon.from_pydcs(Weapons.LAU_115C_AIM_7E): 1963,
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976, Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976,
Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982, Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982,
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987, Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987,
# AIM-9 Sidewinder # AIM-9 Sidewinder
Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977, Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977,
Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1982, Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1982,
Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1980, Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1980,
Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978, Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978,
Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003, Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003,
Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9L_L): 1977, Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9L_L): 1977,
Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9L_R): 1977, Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9L_R): 1977,
Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9M_L): 1982, Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9M_L): 1982,
Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9M_R): 1982, Weapon.from_pydcs(Weapons.LAU_105_1_AIM_9M_R): 1982,
Weapon.from_pydcs(Weapons.LAU_105_2_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_105_2_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_105_2_AIM_9P5): 1980, Weapon.from_pydcs(Weapons.LAU_105_2_AIM_9P5): 1980,
Weapon.from_pydcs(Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM): 1982, Weapon.from_pydcs(Weapons.LAU_105___2_AIM_9M_Sidewinder_IR_AAM): 1982,
Weapon.from_pydcs(Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM): 1978, Weapon.from_pydcs(Weapons.LAU_105___2_AIM_9P_Sidewinder_IR_AAM): 1978,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9M): 1982, Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9M): 1982,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9X): 2003, Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9X): 2003,
Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9M): 1982, Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9M): 1982,
Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9X): 2003, Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9X): 2003,
Weapon.from_pydcs(Weapons.LAU_127_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_127_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_127_AIM_9M): 1982, Weapon.from_pydcs(Weapons.LAU_127_AIM_9M): 1982,
Weapon.from_pydcs(Weapons.LAU_127_AIM_9X): 2003, Weapon.from_pydcs(Weapons.LAU_127_AIM_9X): 2003,
Weapon.from_pydcs(Weapons.LAU_138_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_138_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_138_AIM_9M): 1982, Weapon.from_pydcs(Weapons.LAU_138_AIM_9M): 1982,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9L): 1977, Weapon.from_pydcs(Weapons.LAU_7_AIM_9L): 1977,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9M): 1982, Weapon.from_pydcs(Weapons.LAU_7_AIM_9M): 1982,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM): 1982, Weapon.from_pydcs(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM): 1982,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM): 1980, Weapon.from_pydcs(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM): 1980,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM): 1978, Weapon.from_pydcs(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM): 1978,
Weapon.from_pydcs(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM): 2003, Weapon.from_pydcs(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM): 2003,
Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM): 1977, Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM): 1977,
Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9M_Sidewinder_IR_AAM): 1982, Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9M_Sidewinder_IR_AAM): 1982,
Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM): 1980, Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9P5_Sidewinder_IR_AAM): 1980,
Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM): 1978, Weapon.from_pydcs(Weapons.LAU_7___2_AIM_9P_Sidewinder_IR_AAM): 1978,
# ALQ ECM Pods # ALQ ECM Pods
Weapon.from_pydcs(Weapons.ALQ_131): 1970, Weapon.from_pydcs(Weapons.ALQ_131): 1970,
Weapon.from_pydcs(Weapons.ALQ_184): 1989, Weapon.from_pydcs(Weapons.ALQ_184): 1989,
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984, Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
# TGP Pods # TGP Pods
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING): 1995, Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING): 1995,
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING_): 1995, Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING_): 1995,
@ -742,17 +652,14 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985, Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985,
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985, Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985,
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982, Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
# BLU-107 # BLU-107
Weapon.from_pydcs(Weapons.BLU_107): 1983, Weapon.from_pydcs(Weapons.BLU_107): 1983,
Weapon.from_pydcs(Weapons.MER_6_BLU_107): 1983, Weapon.from_pydcs(Weapons.MER_6_BLU_107): 1983,
# GBU-10 LGB # GBU-10 LGB
Weapon.from_pydcs(Weapons.DIS_GBU_10): 1976, Weapon.from_pydcs(Weapons.DIS_GBU_10): 1976,
Weapon.from_pydcs(Weapons.GBU_10): 1976, Weapon.from_pydcs(Weapons.GBU_10): 1976,
Weapon.from_pydcs(Weapons.GBU_10_): 1976, Weapon.from_pydcs(Weapons.GBU_10_): 1976,
Weapon.from_pydcs(Weapons.GBU_10_2): 1976, Weapon.from_pydcs(Weapons.GBU_10_2): 1976,
# GBU-12 LGB # GBU-12 LGB
Weapon.from_pydcs(Weapons.AUF2_GBU_12_x_2): 1976, Weapon.from_pydcs(Weapons.AUF2_GBU_12_x_2): 1976,
Weapon.from_pydcs(Weapons.BRU_33___2_x_GBU_12): 1976, Weapon.from_pydcs(Weapons.BRU_33___2_x_GBU_12): 1976,
@ -770,10 +677,8 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons._2_GBU_12): 1976, Weapon.from_pydcs(Weapons._2_GBU_12): 1976,
Weapon.from_pydcs(Weapons._2_GBU_12_): 1976, Weapon.from_pydcs(Weapons._2_GBU_12_): 1976,
Weapon.from_pydcs(Weapons._3_GBU_12): 1976, Weapon.from_pydcs(Weapons._3_GBU_12): 1976,
# GBU-15 LGB # GBU-15 LGB
Weapon.from_pydcs(Weapons.GBU_15): 1975, Weapon.from_pydcs(Weapons.GBU_15): 1975,
# GBU-16 LGB # GBU-16 LGB
Weapon.from_pydcs(Weapons.BRU_33___2_x_GBU_16): 1976, Weapon.from_pydcs(Weapons.BRU_33___2_x_GBU_16): 1976,
Weapon.from_pydcs(Weapons.DIS_GBU_16): 1976, Weapon.from_pydcs(Weapons.DIS_GBU_16): 1976,
@ -783,20 +688,16 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons._2_GBU_16_): 1976, Weapon.from_pydcs(Weapons._2_GBU_16_): 1976,
Weapon.from_pydcs(Weapons._3_GBU_16): 1976, Weapon.from_pydcs(Weapons._3_GBU_16): 1976,
Weapon.from_pydcs(Weapons._3_GBU_16_): 1976, Weapon.from_pydcs(Weapons._3_GBU_16_): 1976,
# GBU-24 LGB # GBU-24 LGB
Weapon.from_pydcs(Weapons.GBU_24): 1986, Weapon.from_pydcs(Weapons.GBU_24): 1986,
Weapon.from_pydcs(Weapons.GBU_24_): 1986, Weapon.from_pydcs(Weapons.GBU_24_): 1986,
Weapon.from_pydcs(Weapons.GBU_24__): 1986, Weapon.from_pydcs(Weapons.GBU_24__): 1986,
# GBU-27 LGB # GBU-27 LGB
Weapon.from_pydcs(Weapons.GBU_27): 1991, Weapon.from_pydcs(Weapons.GBU_27): 1991,
Weapon.from_pydcs(Weapons.GBU_27_2): 1991, Weapon.from_pydcs(Weapons.GBU_27_2): 1991,
Weapon.from_pydcs(Weapons.GBU_27_4): 1991, Weapon.from_pydcs(Weapons.GBU_27_4): 1991,
# GBU-28 # GBU-28
Weapon.from_pydcs(Weapons.GBU_28): 1991, Weapon.from_pydcs(Weapons.GBU_28): 1991,
# GBU-31 JDAM # GBU-31 JDAM
Weapon.from_pydcs(Weapons.GBU_31V3B_8): 2001, Weapon.from_pydcs(Weapons.GBU_31V3B_8): 2001,
Weapon.from_pydcs(Weapons.GBU_31_8): 2001, Weapon.from_pydcs(Weapons.GBU_31_8): 2001,
@ -804,10 +705,8 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.GBU_31_V_2_B): 2001, Weapon.from_pydcs(Weapons.GBU_31_V_2_B): 2001,
Weapon.from_pydcs(Weapons.GBU_31_V_3_B): 2001, Weapon.from_pydcs(Weapons.GBU_31_V_3_B): 2001,
Weapon.from_pydcs(Weapons.GBU_31_V_4_B): 2001, Weapon.from_pydcs(Weapons.GBU_31_V_4_B): 2001,
# GBU-32 JDAM # GBU-32 JDAM
Weapon.from_pydcs(Weapons.GBU_32_V_2_B): 2002, Weapon.from_pydcs(Weapons.GBU_32_V_2_B): 2002,
# GBU-38 JDAM # GBU-38 JDAM
Weapon.from_pydcs(Weapons.BRU_55___2_x_GBU_38): 2005, Weapon.from_pydcs(Weapons.BRU_55___2_x_GBU_38): 2005,
Weapon.from_pydcs(Weapons.BRU_57___2_x_GBU_38): 2005, Weapon.from_pydcs(Weapons.BRU_57___2_x_GBU_38): 2005,
@ -816,53 +715,40 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons._2_GBU_38): 2005, Weapon.from_pydcs(Weapons._2_GBU_38): 2005,
Weapon.from_pydcs(Weapons._2_GBU_38_): 2005, Weapon.from_pydcs(Weapons._2_GBU_38_): 2005,
Weapon.from_pydcs(Weapons._3_GBU_38): 2005, Weapon.from_pydcs(Weapons._3_GBU_38): 2005,
# GBU-54 LJDAM # GBU-54 LJDAM
Weapon.from_pydcs(Weapons.GBU_54_V_1_B): 2008, Weapon.from_pydcs(Weapons.GBU_54_V_1_B): 2008,
Weapon.from_pydcs(Weapons._2_GBU_54_V_1_B): 2008, Weapon.from_pydcs(Weapons._2_GBU_54_V_1_B): 2008,
Weapon.from_pydcs(Weapons._2_GBU_54_V_1_B_): 2008, Weapon.from_pydcs(Weapons._2_GBU_54_V_1_B_): 2008,
Weapon.from_pydcs(Weapons._3_GBU_54_V_1_B): 2008, Weapon.from_pydcs(Weapons._3_GBU_54_V_1_B): 2008,
# CBU-52 # CBU-52
Weapon.from_pydcs(Weapons.CBU_52B): 1970, Weapon.from_pydcs(Weapons.CBU_52B): 1970,
# CBU-87 CEM # CBU-87 CEM
Weapon.from_pydcs(Weapons.CBU_87): 1986, Weapon.from_pydcs(Weapons.CBU_87): 1986,
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_87): 1986, Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_87): 1986,
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_87_): 1986, Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_87_): 1986,
Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_87): 1986, Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_87): 1986,
# CBU-97 # CBU-97
Weapon.from_pydcs(Weapons.CBU_97): 1992, Weapon.from_pydcs(Weapons.CBU_97): 1992,
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97): 1992, Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97): 1992,
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97_): 1992, Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97_): 1992,
Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_97): 1992, Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_97): 1992,
# CBU-99 # CBU-99
Weapon.from_pydcs(Weapons.BRU_33___2_x_CBU_99): 1968, Weapon.from_pydcs(Weapons.BRU_33___2_x_CBU_99): 1968,
Weapon.from_pydcs(Weapons.CBU_99): 1968, Weapon.from_pydcs(Weapons.CBU_99): 1968,
Weapon.from_pydcs(Weapons.BRU_33___2_x_Mk_20_Rockeye): 1968, Weapon.from_pydcs(Weapons.BRU_33___2_x_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons.DIS_MK_20): 1968, Weapon.from_pydcs(Weapons.DIS_MK_20): 1968,
Weapon.from_pydcs(Weapons.DIS_MK_20_DUAL_L): 1968, Weapon.from_pydcs(Weapons.DIS_MK_20_DUAL_L): 1968,
Weapon.from_pydcs(Weapons.DIS_MK_20_DUAL_R): 1968, Weapon.from_pydcs(Weapons.DIS_MK_20_DUAL_R): 1968,
Weapon.from_pydcs(Weapons.HSAB_9_Mk_20_Rockeye): 1968, Weapon.from_pydcs(Weapons.HSAB_9_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons.MAK79_2_MK_20): 1968, Weapon.from_pydcs(Weapons.MAK79_2_MK_20): 1968,
Weapon.from_pydcs(Weapons.MAK79_2_MK_20_): 1968, Weapon.from_pydcs(Weapons.MAK79_2_MK_20_): 1968,
Weapon.from_pydcs(Weapons.MAK79_MK_20): 1968, Weapon.from_pydcs(Weapons.MAK79_MK_20): 1968,
Weapon.from_pydcs(Weapons.MAK79_MK_20_): 1968, Weapon.from_pydcs(Weapons.MAK79_MK_20_): 1968,
Weapon.from_pydcs(Weapons.MER_6_Mk_20_Rockeye): 1968, Weapon.from_pydcs(Weapons.MER_6_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons.Mk_20): 1968, Weapon.from_pydcs(Weapons.Mk_20): 1968,
Weapon.from_pydcs(Weapons.Mk_20_): 1968, Weapon.from_pydcs(Weapons.Mk_20_): 1968,
Weapon.from_pydcs(Weapons.Mk_20_18): 1968, Weapon.from_pydcs(Weapons.Mk_20_18): 1968,
Weapon.from_pydcs(Weapons.Mk_20_Rockeye__6): 1968, Weapon.from_pydcs(Weapons.Mk_20_Rockeye__6): 1968,
Weapon.from_pydcs(Weapons._2_MK_20): 1968, Weapon.from_pydcs(Weapons._2_MK_20): 1968,
Weapon.from_pydcs(Weapons._2_MK_20_): 1968, Weapon.from_pydcs(Weapons._2_MK_20_): 1968,
Weapon.from_pydcs(Weapons._2_MK_20__): 1968, Weapon.from_pydcs(Weapons._2_MK_20__): 1968,
@ -872,123 +758,100 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye): 1968, Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye_): 1968, Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye_): 1968,
Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye__): 1968, Weapon.from_pydcs(Weapons._2_Mk_20_Rockeye__): 1968,
Weapon.from_pydcs(Weapons._3_Mk_20_Rockeye): 1968, Weapon.from_pydcs(Weapons._3_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons._3_Mk_20_Rockeye_): 1968, Weapon.from_pydcs(Weapons._3_Mk_20_Rockeye_): 1968,
# CBU-103 # CBU-103
Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_103): 2000, Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_103): 2000,
Weapon.from_pydcs(Weapons.CBU_103): 2000, Weapon.from_pydcs(Weapons.CBU_103): 2000,
# CBU-105 # CBU-105
Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_105): 2000, Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_105): 2000,
Weapon.from_pydcs(Weapons.CBU_105): 2000, Weapon.from_pydcs(Weapons.CBU_105): 2000,
# APKWS # APKWS
Weapon.from_pydcs(Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_APKWS): 2016, Weapon.from_pydcs(
Weapon.from_pydcs(Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M282___MPP_APKWS): 2016, Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_APKWS
Weapon.from_pydcs(Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M151___HE_APKWS): 2016, ): 2016,
Weapon.from_pydcs(Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M282___MPP_APKWS): 2016, Weapon.from_pydcs(
Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M282___MPP_APKWS
): 2016,
Weapon.from_pydcs(
Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M151___HE_APKWS
): 2016,
Weapon.from_pydcs(
Weapons._3_x_LAU_131_pods___21_x_2_75__Hydra___Laser_Guided_M282___MPP_APKWS
): 2016,
# Russia # Russia
# KAB-1500 # KAB-1500
Weapon.from_pydcs(Weapons.KAB_1500Kr): 1985, Weapon.from_pydcs(Weapons.KAB_1500Kr): 1985,
Weapon.from_pydcs(Weapons.KAB_1500L): 1995, Weapon.from_pydcs(Weapons.KAB_1500L): 1995,
Weapon.from_pydcs(Weapons.KAB_1500LG_Pr): 1990, Weapon.from_pydcs(Weapons.KAB_1500LG_Pr): 1990,
# KAB-500 # KAB-500
Weapon.from_pydcs(Weapons.KAB_500kr): 1980, Weapon.from_pydcs(Weapons.KAB_500kr): 1980,
Weapon.from_pydcs(Weapons.KAB_500L): 1995, Weapon.from_pydcs(Weapons.KAB_500L): 1995,
Weapon.from_pydcs(Weapons.KAB_500S): 2000, Weapon.from_pydcs(Weapons.KAB_500S): 2000,
# Kh Series # Kh Series
Weapon.from_pydcs(Weapons.Kh_22N): 1962, Weapon.from_pydcs(Weapons.Kh_22N): 1962,
Weapon.from_pydcs(Weapons.Kh_23L): 1975, Weapon.from_pydcs(Weapons.Kh_23L): 1975,
Weapon.from_pydcs(Weapons.Kh_25ML): 1975, Weapon.from_pydcs(Weapons.Kh_25ML): 1975,
Weapon.from_pydcs(Weapons.Kh_25ML_): 1975, Weapon.from_pydcs(Weapons.Kh_25ML_): 1975,
Weapon.from_pydcs(Weapons.Kh_25ML__): 1975, Weapon.from_pydcs(Weapons.Kh_25ML__): 1975,
Weapon.from_pydcs(Weapons.Kh_25MP): 1975, Weapon.from_pydcs(Weapons.Kh_25MP): 1975,
Weapon.from_pydcs(Weapons.Kh_25MPU): 1980, Weapon.from_pydcs(Weapons.Kh_25MPU): 1980,
Weapon.from_pydcs(Weapons.Kh_25MPU_): 1980, Weapon.from_pydcs(Weapons.Kh_25MPU_): 1980,
Weapon.from_pydcs(Weapons.Kh_25MPU__): 1980, Weapon.from_pydcs(Weapons.Kh_25MPU__): 1980,
Weapon.from_pydcs(Weapons.Kh_25MR): 1975, Weapon.from_pydcs(Weapons.Kh_25MR): 1975,
Weapon.from_pydcs(Weapons.Kh_25MR_): 1975, Weapon.from_pydcs(Weapons.Kh_25MR_): 1975,
Weapon.from_pydcs(Weapons.Kh_28__AS_9_Kyle_): 1973, Weapon.from_pydcs(Weapons.Kh_28__AS_9_Kyle_): 1973,
Weapon.from_pydcs(Weapons.Kh_29L): 1980, Weapon.from_pydcs(Weapons.Kh_29L): 1980,
Weapon.from_pydcs(Weapons.Kh_29L_): 1980, Weapon.from_pydcs(Weapons.Kh_29L_): 1980,
Weapon.from_pydcs(Weapons.Kh_29L__): 1980, Weapon.from_pydcs(Weapons.Kh_29L__): 1980,
Weapon.from_pydcs(Weapons.Kh_29T): 1980, Weapon.from_pydcs(Weapons.Kh_29T): 1980,
Weapon.from_pydcs(Weapons.Kh_29T_): 1980, Weapon.from_pydcs(Weapons.Kh_29T_): 1980,
Weapon.from_pydcs(Weapons.Kh_29T__): 1980, Weapon.from_pydcs(Weapons.Kh_29T__): 1980,
Weapon.from_pydcs(Weapons.Kh_31A): 1980, Weapon.from_pydcs(Weapons.Kh_31A): 1980,
Weapon.from_pydcs(Weapons.Kh_31A_): 1980, Weapon.from_pydcs(Weapons.Kh_31A_): 1980,
Weapon.from_pydcs(Weapons.Kh_31A__): 1980, Weapon.from_pydcs(Weapons.Kh_31A__): 1980,
Weapon.from_pydcs(Weapons.Kh_31P): 1980, Weapon.from_pydcs(Weapons.Kh_31P): 1980,
Weapon.from_pydcs(Weapons.Kh_31P_): 1980, Weapon.from_pydcs(Weapons.Kh_31P_): 1980,
Weapon.from_pydcs(Weapons.Kh_31P__): 1980, Weapon.from_pydcs(Weapons.Kh_31P__): 1980,
Weapon.from_pydcs(Weapons.Kh_35): 2003, Weapon.from_pydcs(Weapons.Kh_35): 2003,
Weapon.from_pydcs(Weapons.Kh_35_): 2003, Weapon.from_pydcs(Weapons.Kh_35_): 2003,
Weapon.from_pydcs(Weapons.Kh_35_6): 2003, Weapon.from_pydcs(Weapons.Kh_35_6): 2003,
Weapon.from_pydcs(Weapons.Kh_41): 1984, Weapon.from_pydcs(Weapons.Kh_41): 1984,
Weapon.from_pydcs(Weapons.Kh_58U): 1985, Weapon.from_pydcs(Weapons.Kh_58U): 1985,
Weapon.from_pydcs(Weapons.Kh_58U_): 1985, Weapon.from_pydcs(Weapons.Kh_58U_): 1985,
Weapon.from_pydcs(Weapons.Kh_59M): 1990, Weapon.from_pydcs(Weapons.Kh_59M): 1990,
Weapon.from_pydcs(Weapons.Kh_65): 1992, Weapon.from_pydcs(Weapons.Kh_65): 1992,
Weapon.from_pydcs(Weapons.Kh_65_6): 1992, Weapon.from_pydcs(Weapons.Kh_65_6): 1992,
Weapon.from_pydcs(Weapons.Kh_65_8): 1992, Weapon.from_pydcs(Weapons.Kh_65_8): 1992,
Weapon.from_pydcs(Weapons.Kh_66_Grom__21__APU_68): 1968, Weapon.from_pydcs(Weapons.Kh_66_Grom__21__APU_68): 1968,
# ECM # ECM
Weapon.from_pydcs(Weapons.L175V_Khibiny_ECM_pod): 1982, Weapon.from_pydcs(Weapons.L175V_Khibiny_ECM_pod): 1982,
# R-13 # R-13
Weapon.from_pydcs(Weapons.R_13M): 1961, Weapon.from_pydcs(Weapons.R_13M): 1961,
Weapon.from_pydcs(Weapons.R_13M1): 1965, Weapon.from_pydcs(Weapons.R_13M1): 1965,
# R-24 # R-24
Weapon.from_pydcs(Weapons.R_24R): 1981, Weapon.from_pydcs(Weapons.R_24R): 1981,
Weapon.from_pydcs(Weapons.R_24T): 1981, Weapon.from_pydcs(Weapons.R_24T): 1981,
# R-27 # R-27
Weapon.from_pydcs(Weapons.R_27ER): 1983, Weapon.from_pydcs(Weapons.R_27ER): 1983,
Weapon.from_pydcs(Weapons.R_27ET): 1986, Weapon.from_pydcs(Weapons.R_27ET): 1986,
Weapon.from_pydcs(Weapons.R_27R): 1983, Weapon.from_pydcs(Weapons.R_27R): 1983,
Weapon.from_pydcs(Weapons.R_27T): 1983, Weapon.from_pydcs(Weapons.R_27T): 1983,
# R-33 # R-33
Weapon.from_pydcs(Weapons.R_33): 1981, Weapon.from_pydcs(Weapons.R_33): 1981,
# R-3 # R-3
Weapon.from_pydcs(Weapons.R_3R): 1966, Weapon.from_pydcs(Weapons.R_3R): 1966,
Weapon.from_pydcs(Weapons.R_3S): 1962, Weapon.from_pydcs(Weapons.R_3S): 1962,
# R-40 # R-40
Weapon.from_pydcs(Weapons.R_40R): 1976, Weapon.from_pydcs(Weapons.R_40R): 1976,
Weapon.from_pydcs(Weapons.R_40T): 1976, Weapon.from_pydcs(Weapons.R_40T): 1976,
# R-55 # R-55
Weapon.from_pydcs(Weapons.R_55): 1957, Weapon.from_pydcs(Weapons.R_55): 1957,
Weapon.from_pydcs(Weapons.RS2US): 1957, Weapon.from_pydcs(Weapons.RS2US): 1957,
# R-60 # R-60
Weapon.from_pydcs(Weapons.R_60): 1973, Weapon.from_pydcs(Weapons.R_60): 1973,
Weapon.from_pydcs(Weapons.R_60_x_2): 1973, Weapon.from_pydcs(Weapons.R_60_x_2): 1973,
Weapon.from_pydcs(Weapons.R_60_x_2_): 1973, Weapon.from_pydcs(Weapons.R_60_x_2_): 1973,
Weapon.from_pydcs(Weapons.APU_60_1_R_60M): 1982, Weapon.from_pydcs(Weapons.APU_60_1_R_60M): 1982,
Weapon.from_pydcs(Weapons.R_60M): 1982, Weapon.from_pydcs(Weapons.R_60M): 1982,
Weapon.from_pydcs(Weapons.R_60M_): 1982, Weapon.from_pydcs(Weapons.R_60M_): 1982,
@ -996,36 +859,28 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.R_60M_2_): 1982, Weapon.from_pydcs(Weapons.R_60M_2_): 1982,
Weapon.from_pydcs(Weapons.R_60M_x_2): 1982, Weapon.from_pydcs(Weapons.R_60M_x_2): 1982,
Weapon.from_pydcs(Weapons.R_60M_x_2_): 1982, Weapon.from_pydcs(Weapons.R_60M_x_2_): 1982,
# R-73 # R-73
Weapon.from_pydcs(Weapons.R_73): 1984, Weapon.from_pydcs(Weapons.R_73): 1984,
Weapon.from_pydcs(Weapons.R_73_): 1984, Weapon.from_pydcs(Weapons.R_73_): 1984,
# R-77 # R-77
Weapon.from_pydcs(Weapons.R_77): 2002, Weapon.from_pydcs(Weapons.R_77): 2002,
Weapon.from_pydcs(Weapons.R_77_): 2002, Weapon.from_pydcs(Weapons.R_77_): 2002,
# UK # UK
# ALARM # ALARM
Weapon.from_pydcs(Weapons.ALARM): 1990, Weapon.from_pydcs(Weapons.ALARM): 1990,
Weapon.from_pydcs(Weapons.ALARM_2): 1990, Weapon.from_pydcs(Weapons.ALARM_2): 1990,
# France # France
# BLG-66 Belouga # BLG-66 Belouga
Weapon.from_pydcs(Weapons.AUF2_BLG_66_AC_x_2): 1979, Weapon.from_pydcs(Weapons.AUF2_BLG_66_AC_x_2): 1979,
Weapon.from_pydcs(Weapons.BLG_66_AC_Belouga): 1979, Weapon.from_pydcs(Weapons.BLG_66_AC_Belouga): 1979,
Weapon.from_pydcs(Weapons.BLG_66_AC_Belouga_): 1979, Weapon.from_pydcs(Weapons.BLG_66_AC_Belouga_): 1979,
# HOT-3 # HOT-3
Weapon.from_pydcs(Weapons.HOT3): 1998, Weapon.from_pydcs(Weapons.HOT3): 1998,
Weapon.from_pydcs(Weapons.HOT3_): 1998, Weapon.from_pydcs(Weapons.HOT3_): 1998,
# Magic 2 # Magic 2
Weapon.from_pydcs(Weapons.Matra_Magic_II): 1986, Weapon.from_pydcs(Weapons.Matra_Magic_II): 1986,
Weapon.from_pydcs(Weapons.R_550_Magic_2): 1986, Weapon.from_pydcs(Weapons.R_550_Magic_2): 1986,
# Super 530D # Super 530D
Weapon.from_pydcs(Weapons.Matra_Super_530D): 1988, Weapon.from_pydcs(Weapons.Matra_Super_530D): 1988,
Weapon.from_pydcs(Weapons.Super_530D): 1988, Weapon.from_pydcs(Weapons.Super_530D): 1988,
} }

View File

@ -25,6 +25,7 @@ from dcs.helicopters import (
helicopter_map, helicopter_map,
) )
from dcs.mapping import Point from dcs.mapping import Point
# mypy can't resolve these if they're wildcard imports for some reason. # mypy can't resolve these if they're wildcard imports for some reason.
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
@ -112,7 +113,7 @@ from dcs.planes import (
WingLoong_I, WingLoong_I,
Yak_40, Yak_40,
plane_map, plane_map,
I_16 I_16,
) )
from dcs.ships import ( from dcs.ships import (
Armed_speedboat, Armed_speedboat,
@ -166,6 +167,7 @@ from dcs.vehicles import (
import pydcs_extensions.frenchpack.frenchpack as frenchpack import pydcs_extensions.frenchpack.frenchpack as frenchpack
import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams
# PATCH pydcs data with MODS # PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
@ -222,46 +224,122 @@ vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19 vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_54K6_CP.id] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP vehicle_map[
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN highdigitsams.SAM_SA_10B_S_300PS_54K6_CP.id
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN ] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE vehicle_map[
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR ] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR vehicle_map[
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR ] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6 vehicle_map[
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck ] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E vehicle_map[
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE ] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE vehicle_map[
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2 highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck.id] = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck ] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2 vehicle_map[
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2 highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S457_CP.id] = highdigitsams.SAM_SA_12_S_300V_9S457_CP ] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A82_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A82_LN vehicle_map[
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A83_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A83_LN highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S15_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S15_SR ] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S19_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S19_SR vehicle_map[
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S32_TR.id] = highdigitsams.SAM_SA_12_S_300V_9S32_TR highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP.id] = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP ] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR vehicle_map[
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR.id] = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR ] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN vehicle_map[
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id
vehicle_map[highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2.id] = highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2 ] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
vehicle_map[highdigitsams.SAM_SA_2__V759__LN_SM_90.id] = highdigitsams.SAM_SA_2__V759__LN_SM_90 vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck.id
] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E.id
] = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E.id
] = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE.id
] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE.id
] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE
vehicle_map[
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2.id
] = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2
vehicle_map[
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck.id
] = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
vehicle_map[
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2.id
] = highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2
vehicle_map[
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2.id
] = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9S457_CP.id
] = highdigitsams.SAM_SA_12_S_300V_9S457_CP
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9A82_LN.id
] = highdigitsams.SAM_SA_12_S_300V_9A82_LN
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9A83_LN.id
] = highdigitsams.SAM_SA_12_S_300V_9A83_LN
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9S15_SR.id
] = highdigitsams.SAM_SA_12_S_300V_9S15_SR
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9S19_SR.id
] = highdigitsams.SAM_SA_12_S_300V_9S19_SR
vehicle_map[
highdigitsams.SAM_SA_12_S_300V_9S32_TR.id
] = highdigitsams.SAM_SA_12_S_300V_9S32_TR
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP.id
] = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR.id
] = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR.id
] = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR.id
] = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN.id
] = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
vehicle_map[
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN.id
] = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
vehicle_map[
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2.id
] = highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2
vehicle_map[
highdigitsams.SAM_SA_2__V759__LN_SM_90.id
] = highdigitsams.SAM_SA_2__V759__LN_SM_90
vehicle_map[highdigitsams.SAM_HQ_2_LN_SM_90.id] = highdigitsams.SAM_HQ_2_LN_SM_90 vehicle_map[highdigitsams.SAM_HQ_2_LN_SM_90.id] = highdigitsams.SAM_HQ_2_LN_SM_90
vehicle_map[highdigitsams.SAM_SA_3__V_601P__LN_5P73.id] = highdigitsams.SAM_SA_3__V_601P__LN_5P73 vehicle_map[
vehicle_map[highdigitsams.SAM_SA_24_Igla_S_manpad.id] = highdigitsams.SAM_SA_24_Igla_S_manpad highdigitsams.SAM_SA_3__V_601P__LN_5P73.id
vehicle_map[highdigitsams.SAM_SA_14_Strela_3_manpad.id] = highdigitsams.SAM_SA_14_Strela_3_manpad ] = highdigitsams.SAM_SA_3__V_601P__LN_5P73
vehicle_map[
highdigitsams.SAM_SA_24_Igla_S_manpad.id
] = highdigitsams.SAM_SA_24_Igla_S_manpad
vehicle_map[
highdigitsams.SAM_SA_14_Strela_3_manpad.id
] = highdigitsams.SAM_SA_14_Strela_3_manpad
vehicle_map[highdigitsams.Polyana_D4M1_C2_node.id] = highdigitsams.Polyana_D4M1_C2_node vehicle_map[highdigitsams.Polyana_D4M1_C2_node.id] = highdigitsams.Polyana_D4M1_C2_node
vehicle_map[highdigitsams._34Ya6E_Gazetchik_E_decoy.id] = highdigitsams._34Ya6E_Gazetchik_E_decoy vehicle_map[
highdigitsams._34Ya6E_Gazetchik_E_decoy.id
] = highdigitsams._34Ya6E_Gazetchik_E_decoy
""" """
---------- BEGINNING OF CONFIGURATION SECTION ---------- BEGINNING OF CONFIGURATION SECTION
@ -308,7 +386,6 @@ PRICES = {
JF_17: 20, JF_17: 20,
Su_30: 24, Su_30: 24,
Su_57: 40, Su_57: 40,
SpitfireLFMkIX: 14, SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14, SpitfireLFMkIXCW: 14,
I_16: 10, I_16: 10,
@ -317,7 +394,6 @@ PRICES = {
FW_190A8: 14, FW_190A8: 14,
A_20G: 22, A_20G: 22,
Ju_88A4: 24, Ju_88A4: 24,
F_5E_3: 8, F_5E_3: 8,
MiG_15bis: 4, MiG_15bis: 4,
MiG_19P: 6, MiG_19P: 6,
@ -328,7 +404,6 @@ PRICES = {
C_101CC: 6, C_101CC: 6,
A_4E_C: 8, A_4E_C: 8,
MB_339PAN: 6, MB_339PAN: 6,
AV8BNA: 14, AV8BNA: 14,
M_2000C: 16, M_2000C: 16,
Mirage_2000_5: 20, Mirage_2000_5: 20,
@ -342,7 +417,6 @@ PRICES = {
F_22A: 40, F_22A: 40,
Tornado_IDS: 20, Tornado_IDS: 20,
Tornado_GR4: 20, Tornado_GR4: 20,
# bomber # bomber
Su_17M4: 10, Su_17M4: 10,
Su_25: 15, Su_25: 15,
@ -352,12 +426,10 @@ PRICES = {
Su_24M: 20, Su_24M: 20,
Su_24MR: 24, Su_24MR: 24,
MiG_27K: 20, MiG_27K: 20,
A_10A: 16, A_10A: 16,
A_10C: 22, A_10C: 22,
A_10C_2: 24, A_10C_2: 24,
S_3B: 10, S_3B: 10,
# heli # heli
Ka_50: 13, Ka_50: 13,
SA342M: 8, SA342M: 8,
@ -373,7 +445,6 @@ PRICES = {
AH_64D: 30, AH_64D: 30,
OH_58D: 6, OH_58D: 6,
SH_60B: 6, SH_60B: 6,
# Bombers # Bombers
B_52H: 35, B_52H: 35,
B_1B: 50, B_1B: 50,
@ -382,7 +453,6 @@ PRICES = {
Tu_22M3: 40, Tu_22M3: 40,
Tu_95MS: 35, Tu_95MS: 35,
F_111F: 21, F_111F: 21,
# special # special
IL_76MD: 30, IL_76MD: 30,
An_26B: 25, An_26B: 25,
@ -393,14 +463,12 @@ PRICES = {
KC_135: 25, KC_135: 25,
KC130: 25, KC130: 25,
KC135MPRS: 25, KC135MPRS: 25,
A_50: 50, A_50: 50,
KJ_2000: 50, KJ_2000: 50,
E_3A: 50, E_3A: 50,
E_2C: 50, E_2C: 50,
C_130: 25, C_130: 25,
Hercules: 25, Hercules: 25,
# WW2 # WW2
P_51D_30_NA: 18, P_51D_30_NA: 18,
P_51D: 16, P_51D: 16,
@ -408,17 +476,14 @@ PRICES = {
P_47D_30bl1: 16, P_47D_30bl1: 16,
P_47D_40: 18, P_47D_40: 18,
B_17G: 30, B_17G: 30,
# Drones # Drones
MQ_9_Reaper: 12, MQ_9_Reaper: 12,
RQ_1A_Predator: 6, RQ_1A_Predator: 6,
WingLoong_I: 6, WingLoong_I: 6,
# Modded # Modded
Rafale_M: 26, Rafale_M: 26,
Rafale_A_S: 26, Rafale_A_S: 26,
Rafale_B: 26, Rafale_B: 26,
# armor # armor
Armor.APC_MTLB: 4, Armor.APC_MTLB: 4,
Armor.FDDM_Grad: 4, Armor.FDDM_Grad: 4,
@ -437,7 +502,6 @@ PRICES = {
Armor.IFV_BMP_3: 18, Armor.IFV_BMP_3: 18,
Armor.ZBD_04A: 12, Armor.ZBD_04A: 12,
Armor.ZTZ_96B: 30, Armor.ZTZ_96B: 30,
Armor.APC_Cobra: 4, Armor.APC_Cobra: 4,
Armor.APC_M113: 6, Armor.APC_M113: 6,
Armor.APC_M1043_HMMWV_Armament: 2, Armor.APC_M1043_HMMWV_Armament: 2,
@ -457,10 +521,8 @@ PRICES = {
Armor.IFV_Marder: 10, Armor.IFV_Marder: 10,
Armor.IFV_MCV_80: 10, Armor.IFV_MCV_80: 10,
Armor.IFV_LAV_25: 7, Armor.IFV_LAV_25: 7,
Artillery.MLRS_M270: 55, Artillery.MLRS_M270: 55,
Artillery.SPH_M109_Paladin: 25, Artillery.SPH_M109_Paladin: 25,
Artillery.SPH_2S9_Nona: 12, Artillery.SPH_2S9_Nona: 12,
Artillery.SPH_2S1_Gvozdika: 18, Artillery.SPH_2S1_Gvozdika: 18,
Artillery.SPH_2S3_Akatsia: 24, Artillery.SPH_2S3_Akatsia: 24,
@ -470,14 +532,11 @@ PRICES = {
Artillery.MLRS_9A52_Smerch: 40, Artillery.MLRS_9A52_Smerch: 40,
Artillery._2B11_mortar: 4, Artillery._2B11_mortar: 4,
Artillery.SpGH_Dana: 26, Artillery.SpGH_Dana: 26,
Unarmed.Transport_UAZ_469: 3, Unarmed.Transport_UAZ_469: 3,
Unarmed.Transport_Ural_375: 3, Unarmed.Transport_Ural_375: 3,
Infantry.Infantry_M4: 1, Infantry.Infantry_M4: 1,
Infantry.Soldier_AK: 1, Infantry.Soldier_AK: 1,
Unarmed.Transport_M818: 3, Unarmed.Transport_M818: 3,
# WW2 # WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24, Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
Armor.MT_Pz_Kpfw_IV_Ausf_H: 16, Armor.MT_Pz_Kpfw_IV_Ausf_H: 16,
@ -504,22 +563,18 @@ PRICES = {
Armor.Daimler_Armoured_Car: 8, Armor.Daimler_Armoured_Car: 8,
Armor.LT_Mk_VII_Tetrarch: 8, Armor.LT_Mk_VII_Tetrarch: 8,
Armor.M4_Tractor: 2, Armor.M4_Tractor: 2,
# ship # ship
CV_1143_5_Admiral_Kuznetsov: 100, CV_1143_5_Admiral_Kuznetsov: 100,
CVN_74_John_C__Stennis: 100, CVN_74_John_C__Stennis: 100,
LHA_1_Tarawa: 50, LHA_1_Tarawa: 50,
Bulk_cargo_ship_Yakushev: 10, Bulk_cargo_ship_Yakushev: 10,
Armed_speedboat: 10, Armed_speedboat: 10,
Dry_cargo_ship_Ivanov: 10, Dry_cargo_ship_Ivanov: 10,
Tanker_Elnya_160: 10, Tanker_Elnya_160: 10,
# Air Defence units # Air Defence units
AirDefence.SAM_SA_19_Tunguska_2S6: 30, AirDefence.SAM_SA_19_Tunguska_2S6: 30,
AirDefence.SAM_SA_6_Kub_LN_2P25: 20, AirDefence.SAM_SA_6_Kub_LN_2P25: 20,
AirDefence.SAM_SA_3_S_125_LN_5P73: 6, AirDefence.SAM_SA_3_S_125_LN_5P73: 6,
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30, AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25, AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28, AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
@ -558,7 +613,6 @@ PRICES = {
AirDefence.SAM_SA_18_Igla_S_comm: 8, AirDefence.SAM_SA_18_Igla_S_comm: 8,
AirDefence.EWR_1L13: 30, AirDefence.EWR_1L13: 30,
AirDefence.SAM_SA_6_Kub_STR_9S91: 22, AirDefence.SAM_SA_6_Kub_STR_9S91: 22,
AirDefence.EWR_55G6: 30, AirDefence.EWR_55G6: 30,
AirDefence.CP_9S80M1_Sborka: 10, AirDefence.CP_9S80M1_Sborka: 10,
AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14, AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14,
@ -589,7 +643,6 @@ PRICES = {
AirDefence.AAA_M1_37mm: 7, AirDefence.AAA_M1_37mm: 7,
AirDefence.AAA_M45_Quadmount: 4, AirDefence.AAA_M45_Quadmount: 4,
AirDefence.AA_gun_QF_3_7: 10, AirDefence.AA_gun_QF_3_7: 10,
# FRENCH PACK MOD # FRENCH PACK MOD
frenchpack.AMX_10RCR: 10, frenchpack.AMX_10RCR: 10,
frenchpack.AMX_10RCR_SEPAR: 12, frenchpack.AMX_10RCR_SEPAR: 12,
@ -619,7 +672,6 @@ PRICES = {
frenchpack.DIM__TOYOTA_GREEN: 2, frenchpack.DIM__TOYOTA_GREEN: 2,
frenchpack.DIM__TOYOTA_DESERT: 2, frenchpack.DIM__TOYOTA_DESERT: 2,
frenchpack.DIM__KAMIKAZE: 6, frenchpack.DIM__KAMIKAZE: 6,
# SA-10 # SA-10
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18, AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24, AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24,
@ -627,11 +679,9 @@ PRICES = {
AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30, AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22, AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22, AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
# High digit sams mod # High digit sams mod
highdigitsams.AAA_SON_9_Fire_Can: 8, highdigitsams.AAA_SON_9_Fire_Can: 8,
highdigitsams.AAA_100mm_KS_19: 10, highdigitsams.AAA_100mm_KS_19: 10,
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20, highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20,
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24, highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24,
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24, highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24,
@ -641,14 +691,12 @@ PRICES = {
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26, highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26,
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32, highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32,
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32, highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32,
highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22, highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22,
highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26, highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26,
highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26, highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26,
highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34, highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34,
highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34, highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34,
highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28, highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28,
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26, highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30, highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32, highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32,
@ -656,21 +704,17 @@ PRICES = {
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38, highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28, highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28, highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28,
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27, highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27,
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33, highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33,
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40, highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40,
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30, highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30,
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30, highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30,
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45, highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45,
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45, highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45,
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35, highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35,
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32, highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32,
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32, highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32,
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40, highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40,
} }
""" """
@ -728,7 +772,7 @@ UNIT_BY_TASK = {
SpitfireLFMkIX, SpitfireLFMkIX,
A_4E_C, A_4E_C,
Rafale_M, Rafale_M,
SA342Mistral SA342Mistral,
], ],
CAS: [ CAS: [
AH_1W, AH_1W,
@ -779,18 +823,12 @@ UNIT_BY_TASK = {
Tu_160, Tu_160,
Tu_22M3, Tu_22M3,
Tu_95MS, Tu_95MS,
UH_1H, UH_1H,
SH_60B, SH_60B,
WingLoong_I, WingLoong_I,
Hercules Hercules,
],
Transport: [
IL_76MD,
An_26B,
An_30M,
Yak_40,
C_130
], ],
Transport: [IL_76MD, An_26B, An_30M, Yak_40, C_130],
Refueling: [ Refueling: [
IL_78M, IL_78M,
KC_135, KC_135,
@ -798,12 +836,7 @@ UNIT_BY_TASK = {
S_3B_Tanker, S_3B_Tanker,
KC135MPRS, KC135MPRS,
], ],
AWACS: [ AWACS: [E_3A, E_2C, A_50, KJ_2000],
E_3A,
E_2C,
A_50,
KJ_2000
],
PinpointStrike: [ PinpointStrike: [
Armor.APC_MTLB, Armor.APC_MTLB,
Armor.APC_MTLB, Armor.APC_MTLB,
@ -851,7 +884,6 @@ UNIT_BY_TASK = {
Armor.MBT_T_80U, Armor.MBT_T_80U,
Armor.MBT_T_90, Armor.MBT_T_90,
Armor.ZTZ_96B, Armor.ZTZ_96B,
Armor.APC_Cobra, Armor.APC_Cobra,
Armor.APC_Cobra, Armor.APC_Cobra,
Armor.APC_Cobra, Armor.APC_Cobra,
@ -895,7 +927,6 @@ UNIT_BY_TASK = {
Armor.MBT_Leopard_2, Armor.MBT_Leopard_2,
Armor.MBT_Challenger_II, Armor.MBT_Challenger_II,
Armor.MBT_Merkava_Mk__4, Armor.MBT_Merkava_Mk__4,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G, Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H, Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I, Armor.HT_Pz_Kpfw_VI_Tiger_I,
@ -945,7 +976,6 @@ UNIT_BY_TASK = {
Artillery.Sturmpanzer_IV_Brummbär, Artillery.Sturmpanzer_IV_Brummbär,
Armor.Daimler_Armoured_Car, Armor.Daimler_Armoured_Car,
Armor.LT_Mk_VII_Tetrarch, Armor.LT_Mk_VII_Tetrarch,
Artillery.MLRS_M270, Artillery.MLRS_M270,
Artillery.SPH_M109_Paladin, Artillery.SPH_M109_Paladin,
Artillery.SPH_2S9_Nona, Artillery.SPH_2S9_Nona,
@ -959,7 +989,6 @@ UNIT_BY_TASK = {
Artillery.SpGH_Dana, Artillery.SpGH_Dana,
Artillery.M12_GMC, Artillery.M12_GMC,
Artillery.Sturmpanzer_IV_Brummbär, Artillery.Sturmpanzer_IV_Brummbär,
AirDefence.AAA_ZU_23_on_Ural_375, AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375, AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZSU_57_2, AirDefence.AAA_ZSU_57_2,
@ -983,12 +1012,10 @@ UNIT_BY_TASK = {
AirDefence.AAA_Bofors_40mm, AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm, AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7, AirDefence.AA_gun_QF_3_7,
frenchpack.DIM__TOYOTA_BLUE, frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_DESERT, frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__TOYOTA_GREEN, frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__KAMIKAZE, frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR, frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR, frenchpack.AMX_10RCR_SEPAR,
frenchpack.ERC_90, frenchpack.ERC_90,
@ -1006,15 +1033,29 @@ UNIT_BY_TASK = {
frenchpack.DIM__TOYOTA_GREEN, frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT, frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE, frenchpack.DIM__KAMIKAZE,
], ],
AirDefence: [ AirDefence: [],
Reconnaissance: [
Unarmed.Transport_M818,
Unarmed.Transport_Ural_375,
Unarmed.Transport_UAZ_469,
],
Nothing: [
Infantry.Infantry_M4,
Infantry.Soldier_AK,
], ],
Reconnaissance: [Unarmed.Transport_M818, Unarmed.Transport_Ural_375, Unarmed.Transport_UAZ_469],
Nothing: [Infantry.Infantry_M4, Infantry.Soldier_AK, ],
Embarking: [], Embarking: [],
Carriage: [CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, ], Carriage: [
CargoTransportation: [Dry_cargo_ship_Ivanov, Bulk_cargo_ship_Yakushev, Tanker_Elnya_160, Armed_speedboat, ] CVN_74_John_C__Stennis,
LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
],
CargoTransportation: [
Dry_cargo_ship_Ivanov,
Bulk_cargo_ship_Yakushev,
Tanker_Elnya_160,
Armed_speedboat,
],
} }
""" """
@ -1022,7 +1063,6 @@ Units from AirDefense category of UNIT_BY_TASK that will be removed from use if
""" """
SAM_BAN = [ SAM_BAN = [
AirDefence.SAM_Linebacker_M6, AirDefence.SAM_Linebacker_M6,
AirDefence.SAM_SA_9_Strela_1_9P31, AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_8_Osa_9A33, AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_19_Tunguska_2S6, AirDefence.SAM_SA_19_Tunguska_2S6,
@ -1051,20 +1091,19 @@ SAM_CONVERT = {
AirDefence.SAM_Hawk_TR_AN_MPQ_46: AirDefence.SAM_Hawk_PCP, AirDefence.SAM_Hawk_TR_AN_MPQ_46: AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Hawk_SR_AN_MPQ_50: AirDefence.SAM_Hawk_PCP, AirDefence.SAM_Hawk_SR_AN_MPQ_50: AirDefence.SAM_Hawk_PCP,
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_PCP, AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_PCP,
'except': { "except": {
# this radar is shared between the two S300's. if we attempt to find a SAM site at a base and can't find one # this radar is shared between the two S300's. if we attempt to find a SAM site at a base and can't find one
# model, we can safely assume the other was deployed # model, we can safely assume the other was deployed
# well, perhaps not safely, but we'll make the assumption anyway :p # well, perhaps not safely, but we'll make the assumption anyway :p
AirDefence.SAM_SA_10_S_300PS_TR_30N6: AirDefence.SAM_SA_10_S_300PS_CP_54K6, AirDefence.SAM_SA_10_S_300PS_TR_30N6: AirDefence.SAM_SA_10_S_300PS_CP_54K6,
AirDefence.SAM_SR_P_19: AirDefence.SAM_SA_2_LN_SM_90 AirDefence.SAM_SR_P_19: AirDefence.SAM_SA_2_LN_SM_90,
} },
} }
""" """
Units that will always be spawned in the air Units that will always be spawned in the air
""" """
TAKEOFF_BAN: List[Type[FlyingType]] = [ TAKEOFF_BAN: List[Type[FlyingType]] = []
]
""" """
Units that will be always spawned in the air if launched from the carrier Units that will be always spawned in the air if launched from the carrier
@ -1135,19 +1174,18 @@ EXPANDED_TASK_PAYLOAD_OVERRIDE = {
"BARCAP": ("CAP HEAVY", "CAP"), "BARCAP": ("CAP HEAVY", "CAP"),
"CAS": ("CAS MAVERICK F", "CAS"), "CAS": ("CAS MAVERICK F", "CAS"),
"INTERCEPTION": ("CAP HEAVY", "CAP"), "INTERCEPTION": ("CAP HEAVY", "CAP"),
"STRIKE": ("STRIKE",), "STRIKE": ("STRIKE",),
"ANTISHIP": ("ANTISHIP",), "ANTISHIP": ("ANTISHIP",),
"SEAD": ("SEAD",), "SEAD": ("SEAD",),
"DEAD": ("SEAD",), "DEAD": ("SEAD",),
"ESCORT": ("CAP HEAVY", "CAP"), "ESCORT": ("CAP HEAVY", "CAP"),
"BAI": ( "BAI", "CAS MAVERICK F", "CAS"), "BAI": ("BAI", "CAS MAVERICK F", "CAS"),
"SWEEP": ("CAP HEAVY", "CAP"), "SWEEP": ("CAP HEAVY", "CAP"),
"OCA_RUNWAY": ("RUNWAY_ATTACK","RUNWAY_STRIKE","STRIKE"), "OCA_RUNWAY": ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"),
"OCA_AIRCRAFT": ("OCA","CAS MAVERICK F", "CAS") "OCA_AIRCRAFT": ("OCA", "CAS MAVERICK F", "CAS"),
} }
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
B_1B: COMMON_OVERRIDE, B_1B: COMMON_OVERRIDE,
B_52H: COMMON_OVERRIDE, B_52H: COMMON_OVERRIDE,
F_117A: COMMON_OVERRIDE, F_117A: COMMON_OVERRIDE,
@ -1254,11 +1292,9 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
AH_64A: COMMON_OVERRIDE, AH_64A: COMMON_OVERRIDE,
SH_60B: COMMON_OVERRIDE, SH_60B: COMMON_OVERRIDE,
Hercules: COMMON_OVERRIDE, Hercules: COMMON_OVERRIDE,
Su_25TM: { Su_25TM: {
SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410", SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410",
}, },
} }
""" """
@ -1319,9 +1355,17 @@ TIME_PERIODS = {
} }
REWARDS = { REWARDS = {
"power": 4, "warehouse": 2, "ware": 2, "fuel": 2, "ammo": 2, "power": 4,
"farp": 1, "fob": 1, "factory": 10, "comms": 10, "oil": 10, "warehouse": 2,
"derrick": 8 "ware": 2,
"fuel": 2,
"ammo": 2,
"farp": 1,
"fob": 1,
"factory": 10,
"comms": 10,
"oil": 10,
"derrick": 8,
} }
CARRIER_CAPABLE = [ CARRIER_CAPABLE = [
@ -1334,7 +1378,6 @@ CARRIER_CAPABLE = [
Rafale_M, Rafale_M,
S_3B, S_3B,
E_2C, E_2C,
UH_1H, UH_1H,
Mi_8MT, Mi_8MT,
Ka_50, Ka_50,
@ -1342,7 +1385,6 @@ CARRIER_CAPABLE = [
OH_58D, OH_58D,
UH_60A, UH_60A,
SH_60B, SH_60B,
SA342L, SA342L,
SA342M, SA342M,
SA342Minigun, SA342Minigun,
@ -1351,7 +1393,6 @@ CARRIER_CAPABLE = [
LHA_CAPABLE = [ LHA_CAPABLE = [
AV8BNA, AV8BNA,
UH_1H, UH_1H,
Mi_8MT, Mi_8MT,
Ka_50, Ka_50,
@ -1359,11 +1400,10 @@ LHA_CAPABLE = [
OH_58D, OH_58D,
UH_60A, UH_60A,
SH_60B, SH_60B,
SA342L, SA342L,
SA342M, SA342M,
SA342Minigun, SA342Minigun,
SA342Mistral SA342Mistral,
] ]
""" """
@ -1422,27 +1462,50 @@ def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]:
MANPADS: List[VehicleType] = [ MANPADS: List[VehicleType] = [
AirDefence.SAM_SA_18_Igla_MANPADS, AirDefence.SAM_SA_18_Igla_MANPADS,
AirDefence.SAM_SA_18_Igla_S_MANPADS, AirDefence.SAM_SA_18_Igla_S_MANPADS,
AirDefence.Stinger_MANPADS AirDefence.Stinger_MANPADS,
] ]
INFANTRY: List[VehicleType] = [ INFANTRY: List[VehicleType] = [
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Soldier_RPG, Infantry.Soldier_RPG,
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Infantry_M4,
Infantry.Soldier_M249, Infantry.Soldier_M249,
Artillery._2B11_mortar, Artillery._2B11_mortar,
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
Infantry.Soldier_AK,
Infantry.Soldier_AK,
Infantry.Soldier_AK,
Infantry.Soldier_AK,
Infantry.Paratrooper_RPG_16, Infantry.Paratrooper_RPG_16,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Georgian_soldier_with_M4,
Infantry.Georgian_soldier_with_M4,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_Mauser_98,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Soldier_Insurgents,
Infantry.Infantry_Soldier_Insurgents,
Infantry.Infantry_Soldier_Insurgents,
] ]
@ -1465,6 +1528,7 @@ def unit_type_name(unit_type) -> str:
def unit_type_name_2(unit_type) -> str: def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id return unit_type.name and unit_type.name or unit_type.id
def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str: def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str:
original_name = unit_type.name and unit_type.name or unit_type.id original_name = unit_type.name and unit_type.name or unit_type.id
default_value = None default_value = None
@ -1493,6 +1557,7 @@ def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> s
return default_value return default_value
return faction_value return faction_value
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]: def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map: if name in vehicle_map:
return vehicle_map[name] return vehicle_map[name]
@ -1526,9 +1591,13 @@ def task_name(task) -> str:
return task.name return task.name
def choose_units(for_task: Task, factor: float, count: int, country: str) -> List[UnitType]: def choose_units(
for_task: Task, factor: float, count: int, country: str
) -> List[UnitType]:
suitable_unittypes = find_unittype(for_task, country) suitable_unittypes = find_unittype(for_task, country)
suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()] suitable_unittypes = [
x for x in suitable_unittypes if x not in helicopter_map.values()
]
suitable_unittypes.sort(key=lambda x: PRICES[x]) suitable_unittypes.sort(key=lambda x: PRICES[x])
idx = int(len(suitable_unittypes) * factor) idx = int(len(suitable_unittypes) * factor)
@ -1576,7 +1645,10 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict
def assigned_units_split(fd: AssignedUnitsDict) -> 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()}, return (
{k: v1 for k, (v1, v2) in fd.items()},
{k: v2 for k, (v1, v2) in fd.items()},
)
def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict: def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict:
@ -1620,7 +1692,9 @@ def _validate_db():
total_set = set() total_set = set()
for t, unit_collection in UNIT_BY_TASK.items(): for t, unit_collection in UNIT_BY_TASK.items():
for unit_type in set(unit_collection): for unit_type in set(unit_collection):
assert unit_type not in total_set, "{} is duplicate for task {}".format(unit_type, t) assert unit_type not in total_set, "{} is duplicate for task {}".format(
unit_type, t
)
total_set.add(unit_type) total_set.add(unit_type)
# check prices # check prices

View File

@ -96,13 +96,14 @@ class StateData:
# them when they've already dead. Dedup. # them when they've already dead. Dedup.
killed_ground_units=list(set(data["killed_ground_units"])), killed_ground_units=list(set(data["killed_ground_units"])),
destroyed_statics=data["destroyed_objects_positions"], destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"] base_capture_events=data["base_capture_events"],
) )
class Debriefing: class Debriefing:
def __init__(self, state_data: Dict[str, Any], game: Game, def __init__(
unit_map: UnitMap) -> None: self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data) self.state_data = StateData.from_json(state_data)
self.unit_map = unit_map self.unit_map = unit_map
@ -135,12 +136,9 @@ class Debriefing:
yield from self.ground_losses.enemy_airfields yield from self.ground_losses.enemy_airfields
def casualty_count(self, control_point: ControlPoint) -> int: def casualty_count(self, control_point: ControlPoint) -> int:
return len( return len([x for x in self.front_line_losses if x.origin == control_point])
[x for x in self.front_line_losses if x.origin == control_point]
)
def front_line_losses_by_type( def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int) losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player: if player:
losses = self.ground_losses.player_front_line losses = self.ground_losses.player_front_line
@ -221,8 +219,10 @@ class Debriefing:
# deaths, so we expect to see quite a few unclaimed dead ground # deaths, so we expect to see quite a few unclaimed dead ground
# units. We should start tracking those and covert this to a # units. We should start tracking those and covert this to a
# warning. # warning.
logging.debug(f"Death of untracked ground unit {unit_name} will " logging.debug(
"have no effect. This may be normal behavior.") f"Death of untracked ground unit {unit_name} will "
"have no effect. This may be normal behavior."
)
return losses return losses
@ -234,15 +234,16 @@ class Debriefing:
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures): for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base not in [x[1] for x in last_base_cap_indexes]: if base not in [x[1] for x in last_base_cap_indexes]:
last_base_cap_indexes.append((idx, base)) last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes] return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
class PollDebriefingFileThread(threading.Thread): class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check """Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition.""" regularly for the stopped() condition."""
def __init__(self, callback: Callable[[Debriefing], None], def __init__(
game: Game, unit_map: UnitMap) -> None: self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
) -> None:
super().__init__() super().__init__()
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.callback = callback self.callback = callback
@ -261,7 +262,10 @@ class PollDebriefingFileThread(threading.Thread):
else: else:
last_modified = 0 last_modified = 0
while not self.stopped(): while not self.stopped():
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified: if (
os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified
):
with open("state.json", "r") as json_file: with open("state.json", "r") as json_file:
json_data = json.load(json_file) json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game, self.unit_map) debriefing = Debriefing(json_data, self.game, self.unit_map)
@ -270,8 +274,9 @@ class PollDebriefingFileThread(threading.Thread):
time.sleep(5) time.sleep(5)
def wait_for_debriefing(callback: Callable[[Debriefing], None], def wait_for_debriefing(
game: Game, unit_map) -> PollDebriefingFileThread: callback: Callable[[Debriefing], None], game: Game, unit_map
) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map) thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start() thread.start()
return thread return thread

View File

@ -37,7 +37,15 @@ class Event:
to_cp = None # type: ControlPoint to_cp = None # type: ControlPoint
difficulty = 1 # type: int difficulty = 1 # type: int
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): def __init__(
self,
game,
from_cp: ControlPoint,
target_cp: ControlPoint,
location: Point,
attacker_name: str,
defender_name: str,
):
self.game = game self.game = game
self.from_cp = from_cp self.from_cp = from_cp
self.to_cp = target_cp self.to_cp = target_cp
@ -57,12 +65,14 @@ class Event:
Operation.prepare(self.game) Operation.prepare(self.game)
unit_map = Operation.generate() unit_map = Operation.generate()
Operation.current_mission.save( Operation.current_mission.save(
persistency.mission_path_for("liberation_nextturn.miz")) persistency.mission_path_for("liberation_nextturn.miz")
)
return unit_map return unit_map
@staticmethod @staticmethod
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses, def _transfer_aircraft(
for_player: bool) -> None: ato: AirTaskingOrder, losses: AirLosses, for_player: bool
) -> None:
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
# No need to transfer to the same location. # No need to transfer to the same location.
@ -77,13 +87,16 @@ class Event:
if flight.arrival.captured != for_player: if flight.arrival.captured != for_player:
logging.info( logging.info(
f"Not transferring {flight} because {flight.arrival} " f"Not transferring {flight} because {flight.arrival} "
"was captured") "was captured"
)
continue continue
transfer_count = losses.surviving_flight_members(flight) transfer_count = losses.surviving_flight_members(flight)
if transfer_count < 0: if transfer_count < 0:
logging.error(f"{flight} had {flight.count} aircraft but " logging.error(
f"{transfer_count} losses were recorded.") f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded."
)
continue continue
aircraft = flight.unit_type aircraft = flight.unit_type
@ -91,7 +104,8 @@ class Event:
if available < transfer_count: if available < transfer_count:
logging.error( logging.error(
f"Found killed {aircraft} from {flight.departure} but " f"Found killed {aircraft} from {flight.departure} but "
f"that airbase has only {available} available.") f"that airbase has only {available} available."
)
continue continue
flight.departure.base.aircraft[aircraft] -= transfer_count flight.departure.base.aircraft[aircraft] -= transfer_count
@ -101,10 +115,12 @@ class Event:
flight.arrival.base.aircraft[aircraft] += transfer_count flight.arrival.base.aircraft[aircraft] += transfer_count
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None: def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses, self._transfer_aircraft(
for_player=True) self.game.blue_ato, debriefing.air_losses, for_player=True
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses, )
for_player=False) self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
)
@staticmethod @staticmethod
def commit_air_losses(debriefing: Debriefing) -> None: def commit_air_losses(debriefing: Debriefing) -> None:
@ -115,7 +131,8 @@ class Event:
if available <= 0: if available <= 0:
logging.error( logging.error(
f"Found killed {aircraft} from {cp} but that airbase has " f"Found killed {aircraft} from {cp} but that airbase has "
"none available.") "none available."
)
continue continue
logging.info(f"{aircraft} destroyed from {cp}") logging.info(f"{aircraft} destroyed from {cp}")
@ -130,7 +147,8 @@ class Event:
if available <= 0: if available <= 0:
logging.error( logging.error(
f"Found killed {unit_type} from {control_point} but that " f"Found killed {unit_type} from {control_point} but that "
"airbase has none available.") "airbase has none available."
)
continue continue
logging.info(f"{unit_type} destroyed from {control_point}") logging.info(f"{unit_type} destroyed from {control_point}")
@ -149,11 +167,14 @@ class Event:
def commit_building_losses(self, debriefing: Debriefing) -> None: def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses: for loss in debriefing.building_losses:
loss.ground_object.kill() loss.ground_object.kill()
self.game.informations.append(Information( self.game.informations.append(
"Building destroyed", Information(
f"{loss.ground_object.dcs_identifier} has been destroyed at " "Building destroyed",
f"location {loss.ground_object.obj_name}", self.game.turn f"{loss.ground_object.dcs_identifier} has been destroyed at "
)) f"location {loss.ground_object.obj_name}",
self.game.turn,
)
)
@staticmethod @staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None: def commit_damaged_runways(debriefing: Debriefing) -> None:
@ -171,9 +192,9 @@ class Event:
# ------------------------------ # ------------------------------
# Captured bases # Captured bases
#if self.game.player_country in db.BLUEFOR_FACTIONS: # if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE coalition = 2 # Value in DCS mission event for BLUE
#else: # else:
# coalition = 1 # Value in DCS mission event for RED # coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events: for captured in debriefing.base_capture_events:
@ -187,12 +208,22 @@ class Event:
if cp.captured and new_owner_coalition != coalition: if cp.captured and new_owner_coalition != coalition:
for_player = False for_player = False
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn) info = Information(
cp.name + " lost !",
"The ennemy took control of "
+ cp.name
+ "\nShame on us !",
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
captured_cps.append(cp) captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition: elif not (cp.captured) and new_owner_coalition == coalition:
for_player = True for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn) info = Information(
cp.name + " captured !",
"We took control of " + cp.name + "! Great job !",
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
captured_cps.append(cp) captured_cps.append(cp)
else: else:
@ -218,7 +249,12 @@ class Event:
for cp in self.game.theater.player_points(): for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured] enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps: for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name) print(
"Compute frontline progression for : "
+ cp.name
+ " to "
+ enemy_cp.name
)
delta = 0.0 delta = 0.0
player_won = True player_won = True
@ -234,7 +270,11 @@ class Event:
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties) ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH] player_aggresive = cp.stances[enemy_cp.id] in [
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH,
]
if ally_units_alive == 0: if ally_units_alive == 0:
player_won = False player_won = False
@ -259,11 +299,17 @@ class Event:
delta = DEFEAT_INFLUENCE delta = DEFEAT_INFLUENCE
elif ally_casualties > enemy_casualties: elif ally_casualties > enemy_casualties:
if ally_units_alive > 2*enemy_units_alive and player_aggresive: if (
ally_units_alive > 2 * enemy_units_alive
and player_aggresive
):
# Even with casualties if the enemy is overwhelmed, they are going to lose ground # Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True player_won = True
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
elif ally_units_alive > 3*enemy_units_alive and player_aggresive: elif (
ally_units_alive > 3 * enemy_units_alive
and player_aggresive
):
player_won = True player_won = True
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
else: else:
@ -275,7 +321,10 @@ class Event:
delta = STRONG_DEFEAT_INFLUENCE delta = STRONG_DEFEAT_INFLUENCE
# No progress with defensive strategies # No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]: if player_won and cp.stances[enemy_cp.id] in [
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
]:
print("Defensive stance, progress is limited") print("Defensive stance, progress is limited")
delta = MINOR_DEFEAT_INFLUENCE delta = MINOR_DEFEAT_INFLUENCE
@ -283,28 +332,40 @@ class Event:
print(cp.name + " won ! factor > " + str(delta)) print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta) cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta) enemy_cp.base.affect_strength(-delta)
info = Information("Frontline Report", info = Information(
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name, "Frontline Report",
self.game.turn) "Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
else: else:
print(cp.name + " lost ! factor > " + str(delta)) print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta) enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta) cp.base.affect_strength(-delta)
info = Information("Frontline Report", info = Information(
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name, "Frontline Report",
self.game.turn) "Our ground forces from "
+ cp.name
+ " are losing ground against the enemy forces from "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info) self.game.informations.append(info)
def redeploy_units(self, cp: ControlPoint) -> None: def redeploy_units(self, cp: ControlPoint) -> None:
"""" """ "
Auto redeploy units to newly captured base Auto redeploy units to newly captured base
""" """
ally_connected_cps = [ocp for ocp in cp.connected_points if ally_connected_cps = [
cp.captured == ocp.captured] ocp for ocp in cp.connected_points if cp.captured == ocp.captured
enemy_connected_cps = [ocp for ocp in cp.connected_points if ]
cp.captured != ocp.captured] enemy_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
]
# If the newly captured cp does not have enemy connected cp, # If the newly captured cp does not have enemy connected cp,
# then it is not necessary to redeploy frontline units there. # then it is not necessary to redeploy frontline units there.
@ -315,8 +376,7 @@ class Event:
for ally_cp in ally_connected_cps: for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp) self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint, def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
source: ControlPoint) -> None:
total_units_redeployed = 0 total_units_redeployed = 0
moved_units = {} moved_units = {}
@ -333,8 +393,7 @@ class Event:
for frontline_unit, count in source.base.armor.items(): for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor) moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int( total_units_redeployed = total_units_redeployed + int(count * move_factor)
count * move_factor)
destination.base.commision_units(moved_units) destination.base.commision_units(moved_units)
source.base.commit_losses(moved_units) source.base.commit_losses(moved_units)
@ -362,7 +421,6 @@ class Event:
class UnitsDeliveryEvent: class UnitsDeliveryEvent:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.to_cp = control_point self.to_cp = control_point
self.units: Dict[Type[UnitType], int] = {} self.units: Dict[Type[UnitType], int] = {}
@ -390,8 +448,7 @@ class UnitsDeliveryEvent:
logging.error(f"Could not refund {unit_type.id}, price unknown") logging.error(f"Could not refund {unit_type.id}, price unknown")
continue continue
logging.info( logging.info(f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
game.adjust_budget(price * count, player=self.to_cp.captured) game.adjust_budget(price * count, player=self.to_cp.captured)
def available_next_turn(self, unit_type: Type[UnitType]) -> int: def available_next_turn(self, unit_type: Type[UnitType]) -> int:
@ -409,13 +466,13 @@ class UnitsDeliveryEvent:
aircraft = unit_type.id aircraft = unit_type.id
name = self.to_cp.name name = self.to_cp.name
if count >= 0: if count >= 0:
bought_units[unit_type] = count bought_units[unit_type] = count
game.message( game.message(
f"{coalition} reinforcements: {aircraft} x {count} at {name}") f"{coalition} reinforcements: {aircraft} x {count} at {name}"
)
else: else:
sold_units[unit_type] = -count sold_units[unit_type] = -count
game.message( game.message(f"{coalition} sold: {aircraft} x {-count} at {name}")
f"{coalition} sold: {aircraft} x {-count} at {name}")
self.to_cp.base.commision_units(bought_units) self.to_cp.base.commision_units(bought_units)
self.to_cp.base.commit_losses(sold_units) self.to_cp.base.commit_losses(sold_units)
self.units = {} self.units = {}

View File

@ -7,5 +7,6 @@ class FrontlineAttackEvent(Event):
Currently the same as its parent, but here for legacy compatibility as well as to allow for Currently the same as its parent, but here for legacy compatibility as well as to allow for
future unique Event handling future unique Event handling
""" """
def __str__(self): def __str__(self):
return "Frontline attack" return "Frontline attack"

View File

@ -10,8 +10,18 @@ from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence 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.building_data import (
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE 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 from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@ -109,8 +119,7 @@ class Faction:
building_set: List[str] = field(default_factory=list) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field( liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option #: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless #: for the mission. This option enables GPS for capable aircraft regardless
@ -128,7 +137,11 @@ class Faction:
faction.country = json.get("country", "/") faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]: 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)) raise AssertionError(
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
faction.country
)
)
faction.name = json.get("name", "") faction.name = json.get("name", "")
if not faction.name: if not faction.name:
@ -141,14 +154,10 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", [])) faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.frontline_units = load_all_vehicles( faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
json.get("frontline_units", [])) faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.artillery_units = load_all_vehicles( faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
json.get("artillery_units", [])) faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.infantry_units = load_all_vehicles(
json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(
json.get("logistics_units", []))
faction.ewrs = json.get("ewrs", []) faction.ewrs = json.get("ewrs", [])
@ -163,13 +172,10 @@ class Faction:
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get( faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
"helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", []) faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships( faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
json.get("aircraft_carrier", [])) faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.helicopter_carrier = load_all_ships(
json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", [])) faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", [])) faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
@ -220,13 +226,18 @@ class Faction:
@property @property
def units(self) -> List[Type[UnitType]]: def units(self) -> List[Type[UnitType]]:
return (self.infantry_units + self.aircrafts + self.awacs + return (
self.artillery_units + self.frontline_units + self.infantry_units
self.tankers + self.logistics_units) + self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def unit_loader( def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
""" """
Find unit by name Find unit by name
:param unit: Unit name as string :param unit: Unit name as string
@ -250,9 +261,10 @@ def unit_loader(
def load_aircraft(name: str) -> Optional[Type[FlyingType]]: def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(Optional[FlyingType], unit_loader( return cast(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES] Optional[FlyingType],
)) unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def load_all_aircraft(data) -> List[Type[FlyingType]]: def load_all_aircraft(data) -> List[Type[FlyingType]]:
@ -265,9 +277,12 @@ def load_all_aircraft(data) -> List[Type[FlyingType]]:
def load_vehicle(name: str) -> Optional[Type[VehicleType]]: def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(Optional[FlyingType], unit_loader( return cast(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES] Optional[FlyingType],
)) unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]: def load_all_vehicles(data) -> List[Type[VehicleType]]:

View File

@ -78,10 +78,16 @@ class TurnState(Enum):
class Game: class Game:
def __init__(self, player_name: str, enemy_name: str, def __init__(
theater: ConflictTheater, start_date: datetime, self,
settings: Settings, player_budget: float, player_name: str,
enemy_budget: float) -> None: enemy_name: str,
theater: ConflictTheater,
start_date: datetime,
settings: Settings,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings self.settings = settings
self.events: List[Event] = [] self.events: List[Event] = []
self.theater = theater self.theater = theater
@ -112,9 +118,7 @@ class Game:
self.blue_ato = AirTaskingOrder() self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory( self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.theater.controlpoints
)
self.sanitize_sides() self.sanitize_sides()
@ -147,8 +151,9 @@ class Game:
self.on_load() self.on_load()
def generate_conditions(self) -> Conditions: def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date, return Conditions.generate(
self.current_turn_time_of_day, self.settings) self.theater, self.date, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self): def sanitize_sides(self):
""" """
@ -184,13 +189,24 @@ class Game:
return random.randint(1, 100) <= prob * mult return random.randint(1, 100) <= prob * mult
def _generate_player_event(self, event_class, player_cp, enemy_cp): def _generate_player_event(self, event_class, player_cp, enemy_cp):
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name)) self.events.append(
event_class(
self,
player_cp,
enemy_cp,
enemy_cp.position,
self.player_name,
self.enemy_name,
)
)
def _generate_events(self): def _generate_events(self):
for front_line in self.theater.conflicts(True): for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent, self._generate_player_event(
front_line.control_point_a, FrontlineAttackEvent,
front_line.control_point_b) front_line.control_point_a,
front_line.control_point_b,
)
def adjust_budget(self, amount: float, player: bool) -> None: def adjust_budget(self, amount: float, player: bool) -> None:
if player: if player:
@ -208,7 +224,7 @@ class Game:
self.enemy_budget += Income(self, player=False).total self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap: def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events # assert event in self.events
logging.info("Generating {} (regular)".format(event)) logging.info("Generating {} (regular)".format(event))
return event.generate() return event.generate()
@ -223,7 +239,11 @@ class Game:
def is_player_attack(self, event): def is_player_attack(self, event):
if isinstance(event, Event): if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name return (
event
and event.attacker_name
and event.attacker_name == self.player_name
)
else: else:
raise RuntimeError(f"{event} was passed when an Event type was expected") raise RuntimeError(f"{event} was passed when an Event type was expected")
@ -235,7 +255,9 @@ class Game:
def pass_turn(self, no_action: bool = False) -> None: def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn") logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0)
)
self.turn += 1 self.turn += 1
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
@ -281,7 +303,7 @@ class Game:
# Check for win or loss condition # Check for win or loss condition
turn_state = self.check_win_loss() turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS,TurnState.WIN): if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state) return self.process_win_loss(turn_state)
# Plan flights & combat for next turn # Plan flights & combat for next turn
@ -305,8 +327,11 @@ class Game:
self.plan_procurement(blue_planner, red_planner) self.plan_procurement(blue_planner, red_planner)
def plan_procurement(self, blue_planner: CoalitionMissionPlanner, def plan_procurement(
red_planner: CoalitionMissionPlanner) -> None: self,
blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner,
) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after # gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default # repairs) is split evenly between air and ground. For the default
@ -320,7 +345,7 @@ class Game:
manage_runways=self.settings.automate_runway_repair, manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements, manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements, manage_aircraft=self.settings.automate_aircraft_reinforcements,
front_line_budget_share=ground_portion front_line_budget_share=ground_portion,
).spend_budget(self.budget, blue_planner.procurement_requests) ).spend_budget(self.budget, blue_planner.procurement_requests)
self.enemy_budget = ProcurementAi( self.enemy_budget = ProcurementAi(
@ -330,7 +355,7 @@ class Game:
manage_runways=True, manage_runways=True,
manage_front_line=True, manage_front_line=True,
manage_aircraft=True, manage_aircraft=True,
front_line_budget_share=ground_portion front_line_budget_share=ground_portion,
).spend_budget(self.enemy_budget, red_planner.procurement_requests) ).spend_budget(self.enemy_budget, red_planner.procurement_requests)
def message(self, text: str) -> None: def message(self, text: str) -> None:
@ -361,10 +386,12 @@ class Game:
def compute_threat_zones(self) -> None: def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True) self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False) self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone, self.blue_navmesh = NavMesh.from_threat_zones(
self.theater) self.red_threat_zone, self.theater
self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone, )
self.theater) self.red_navmesh = NavMesh.from_threat_zones(
self.blue_threat_zone, self.theater
)
def threat_zone_for(self, player: bool) -> ThreatZones: def threat_zone_for(self, player: bool) -> ThreatZones:
if player: if player:
@ -386,9 +413,9 @@ class Game:
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line.control_point_a, position = Conflict.frontline_position(
front_line.control_point_b, front_line.control_point_a, front_line.control_point_b, self.theater
self.theater) )
zones.append(position[0]) zones.append(position[0])
zones.append(front_line.control_point_a.position) zones.append(front_line.control_point_a.position)
zones.append(front_line.control_point_b.position) zones.append(front_line.control_point_b.position)
@ -413,7 +440,10 @@ class Game:
d = cp.position.distance_to_point(cp2.position) d = cp.position.distance_to_point(cp2.position)
if d < min_distance: if d < min_distance:
min_distance = d min_distance = d
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2) cpoint = Point(
(cp.position.x + cp2.position.x) / 2,
(cp.position.y + cp2.position.y) / 2,
)
zones.append(cp.position) zones.append(cp.position)
zones.append(cp2.position) zones.append(cp2.position)
break break
@ -422,8 +452,7 @@ class Game:
if cpoint is not None: if cpoint is not None:
zones.append(cpoint) zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages, packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
self.red_ato.packages)
for package in packages: for package in packages:
if package.primary_task is FlightType.BARCAP: if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters, # BARCAPs will be planned at most locations on smaller theaters,
@ -460,7 +489,10 @@ class Game:
return False return False
else: else:
for z in self.__culling_zones: for z in self.__culling_zones:
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000: if (
z.distance_to_point(pos)
< self.settings.perf_culling_distance * 1000
):
return False return False
for p in self.__culling_points: for p in self.__culling_points:
if p.distance_to_point(pos) < 2500: if p.distance_to_point(pos) < 2500:
@ -502,6 +534,10 @@ class Game:
def process_win_loss(self, turn_state: TurnState): def process_win_loss(self, turn_state: TurnState):
if turn_state is TurnState.WIN: if turn_state is TurnState.WIN:
return self.message("Congratulations, you are victorious! Start a new campaign to continue.") return self.message(
"Congratulations, you are victorious! Start a new campaign to continue."
)
elif turn_state is TurnState.LOSS: elif turn_state is TurnState.LOSS:
return self.message("Game Over, you lose. Start a new campaign to continue.") return self.message(
"Game Over, you lose. Start a new campaign to continue."
)

View File

@ -46,10 +46,10 @@ class Income:
for tgo in tgos: for tgo in tgos:
if not tgo.is_dead: if not tgo.is_dead:
count += 1 count += 1
self.buildings.append(BuildingIncome(name, category, count, self.buildings.append(
REWARDS[category])) BuildingIncome(name, category, count, REWARDS[category])
)
self.from_bases = sum(cp.income_per_turn for cp in self.control_points) self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
self.total_buildings = sum(b.income for b in self.buildings) self.total_buildings = sum(b.income for b in self.buildings)
self.total = ((self.total_buildings + self.from_bases) * self.total = (self.total_buildings + self.from_bases) * self.multiplier
self.multiplier)

View File

@ -1,7 +1,7 @@
import datetime import datetime
class Information():
class Information:
def __init__(self, title="", text="", turn=0): def __init__(self, title="", text="", turn=0):
self.title = title self.title = title
self.text = text self.text = text
@ -9,9 +9,11 @@ class Information():
self.timestamp = datetime.datetime.now() self.timestamp = datetime.datetime.now()
def __str__(self): def __str__(self):
return '[{}][{}] {} {}'.format( return "[{}][{}] {} {}".format(
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '', self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
if self.timestamp is not None
else "",
self.turn, self.turn,
self.title, self.title,
self.text self.text,
) )

View File

@ -79,6 +79,7 @@ class ControlPointAircraftInventory:
class GlobalAircraftInventory: class GlobalAircraftInventory:
"""Game-wide aircraft inventory.""" """Game-wide aircraft inventory."""
def __init__(self, control_points: Iterable[ControlPoint]) -> None: def __init__(self, control_points: Iterable[ControlPoint]) -> None:
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = { self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
cp: ControlPointAircraftInventory(cp) for cp in control_points cp: ControlPointAircraftInventory(cp) for cp in control_points
@ -100,8 +101,8 @@ class GlobalAircraftInventory:
inventory.add_aircraft(aircraft, count) inventory.add_aircraft(aircraft, count)
def for_control_point( def for_control_point(
self, self, control_point: ControlPoint
control_point: ControlPoint) -> ControlPointAircraftInventory: ) -> ControlPointAircraftInventory:
"""Returns the inventory specific to the given control point.""" """Returns the inventory specific to the given control point."""
return self.inventories[control_point] return self.inventories[control_point]

View File

@ -7,8 +7,7 @@ class DestroyedUnit:
y: int y: int
name: str name: str
def __init__(self, x , y, name): def __init__(self, x, y, name):
self.x = x self.x = x
self.y = y self.y = y
self.name = name self.name = name

View File

@ -6,7 +6,7 @@ class FrontlineData:
This Data structure will store information about an existing frontline This Data structure will store information about an existing frontline
""" """
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint): def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint):
self.to_cp = to_cp self.to_cp = to_cp
self.from_cp = from_cp self.from_cp = from_cp
self.enemy_units_position = [] self.enemy_units_position = []

View File

@ -1,5 +1,6 @@
from typing import List from typing import List
class FactionTurnMetadata: class FactionTurnMetadata:
""" """
Store metadata about a faction Store metadata about a faction
@ -20,8 +21,8 @@ class GameTurnMetadata:
Store metadata about a game turn Store metadata about a game turn
""" """
allied_units:FactionTurnMetadata allied_units: FactionTurnMetadata
enemy_units:FactionTurnMetadata enemy_units: FactionTurnMetadata
def __init__(self): def __init__(self):
self.allied_units = FactionTurnMetadata() self.allied_units = FactionTurnMetadata()
@ -53,4 +54,3 @@ class GameStats:
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values()) turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
self.data_per_turn.append(turn_data) self.data_per_turn.append(turn_data)

View File

@ -114,9 +114,11 @@ class NavMesh:
return self.travel_cost(a, b) return self.travel_cost(a, b)
@staticmethod @staticmethod
def reconstruct_path(came_from: Dict[NavPoint, Optional[NavPoint]], def reconstruct_path(
origin: NavPoint, came_from: Dict[NavPoint, Optional[NavPoint]],
destination: NavPoint) -> List[Point]: origin: NavPoint,
destination: NavPoint,
) -> List[Point]:
current = destination current = destination
path: List[Point] = [] path: List[Point] = []
while current != origin: while current != origin:
@ -141,16 +143,14 @@ class NavMesh:
raise ValueError(f"Origin point {origin} is outside the navmesh") raise ValueError(f"Origin point {origin} is outside the navmesh")
destination_poly = self.localize(destination) destination_poly = self.localize(destination)
if destination_poly is None: if destination_poly is None:
raise ValueError( raise ValueError(f"Origin point {destination} is outside the navmesh")
f"Origin point {destination} is outside the navmesh")
return self._shortest_path( return self._shortest_path(
NavPoint(self.dcs_to_shapely_point(origin), origin_poly), NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
NavPoint(self.dcs_to_shapely_point(destination), destination_poly) NavPoint(self.dcs_to_shapely_point(destination), destination_poly),
) )
def _shortest_path(self, origin: NavPoint, def _shortest_path(self, origin: NavPoint, destination: NavPoint) -> List[Point]:
destination: NavPoint) -> List[Point]:
# Adapted from # Adapted from
# https://www.redblobgames.com/pathfinding/a-star/implementation.py. # https://www.redblobgames.com/pathfinding/a-star/implementation.py.
frontier = NavFrontier() frontier = NavFrontier()
@ -167,9 +167,7 @@ class NavMesh:
if current.poly == destination.poly: if current.poly == destination.poly:
# Made it to the correct nav poly. Add the leg from the border # Made it to the correct nav poly. Add the leg from the border
# to the target. # to the target.
cost = best_known[current] + self.travel_cost( cost = best_known[current] + self.travel_cost(current, destination)
current, destination
)
if cost < best_known[destination]: if cost < best_known[destination]:
best_known[destination] = cost best_known[destination] = cost
estimated = cost estimated = cost
@ -185,14 +183,10 @@ class NavMesh:
raise RuntimeError raise RuntimeError
_, neighbor_point = nearest_points(current.point, boundary) _, neighbor_point = nearest_points(current.point, boundary)
neighbor_nav = NavPoint(neighbor_point, neighbor) neighbor_nav = NavPoint(neighbor_point, neighbor)
cost = best_known[current] + self.travel_cost( cost = best_known[current] + self.travel_cost(current, neighbor_nav)
current, neighbor_nav
)
if cost < best_known[neighbor_nav]: if cost < best_known[neighbor_nav]:
best_known[neighbor_nav] = cost best_known[neighbor_nav] = cost
estimated = cost + self.travel_heuristic( estimated = cost + self.travel_heuristic(neighbor_nav, destination)
neighbor_nav, destination
)
frontier.push(neighbor_nav, estimated) frontier.push(neighbor_nav, estimated)
came_from[neighbor_nav] = current came_from[neighbor_nav] = current
@ -209,13 +203,16 @@ class NavMesh:
# threatened airbases at the map edges have room to retreat from the # threatened airbases at the map edges have room to retreat from the
# threat without running off the navmesh. # threat without running off the navmesh.
return box(*LineString(points).bounds).buffer( return box(*LineString(points).bounds).buffer(
nautical_miles(100).meters, resolution=1) nautical_miles(100).meters, resolution=1
)
@staticmethod @staticmethod
def create_navpolys(polys: List[Polygon], def create_navpolys(
threat_zones: ThreatZones) -> List[NavMeshPoly]: polys: List[Polygon], threat_zones: ThreatZones
return [NavMeshPoly(i, p, threat_zones.threatened(p)) ) -> List[NavMeshPoly]:
for i, p in enumerate(polys)] return [
NavMeshPoly(i, p, threat_zones.threatened(p)) for i, p in enumerate(polys)
]
@staticmethod @staticmethod
def associate_neighbors(polys: List[NavMeshPoly]) -> None: def associate_neighbors(polys: List[NavMeshPoly]) -> None:
@ -234,8 +231,7 @@ class NavMesh:
point = (int(x), int(y)) point = (int(x), int(y))
neighbors = {} neighbors = {}
for potential_neighbor in points_map[point]: for potential_neighbor in points_map[point]:
intersection = navpoly.poly.intersection( intersection = navpoly.poly.intersection(potential_neighbor.poly)
potential_neighbor.poly)
if not intersection.is_empty: if not intersection.is_empty:
potential_neighbor.neighbors[navpoly] = intersection potential_neighbor.neighbors[navpoly] = intersection
neighbors[potential_neighbor] = intersection neighbors[potential_neighbor] = intersection
@ -243,8 +239,9 @@ class NavMesh:
points_map[point].add(navpoly) points_map[point].add(navpoly)
@classmethod @classmethod
def from_threat_zones(cls, threat_zones: ThreatZones, def from_threat_zones(
theater: ConflictTheater) -> NavMesh: cls, threat_zones: ThreatZones, theater: ConflictTheater
) -> NavMesh:
# Simplify the threat poly to reduce the number of nav zones. Increase # Simplify the threat poly to reduce the number of nav zones. Increase
# the size of the zone and then simplify it with the buffer size as the # the size of the zone and then simplify it with the buffer size as the
# error margin. This will create a simpler poly around the threat zone. # error margin. This will create a simpler poly around the threat zone.

View File

@ -41,6 +41,7 @@ if TYPE_CHECKING:
class Operation: class Operation:
"""Static class for managing the final Mission generation""" """Static class for managing the final Mission generation"""
current_mission = None # type: Mission current_mission = None # type: Mission
airgen = None # type: AircraftConflictGenerator airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator triggersgen = None # type: TriggersGenerator
@ -84,7 +85,7 @@ class Operation:
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
frontline.position frontline.position,
) )
@classmethod @classmethod
@ -93,7 +94,7 @@ class Operation:
player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points() player_cp, enemy_cp = cls.game.theater.closest_opposing_control_points()
mid_point = player_cp.position.point_from_heading( mid_point = player_cp.position.point_from_heading(
player_cp.position.heading_between_point(enemy_cp.position), player_cp.position.heading_between_point(enemy_cp.position),
player_cp.position.distance_to_point(enemy_cp.position) / 2 player_cp.position.distance_to_point(enemy_cp.position) / 2,
) )
return Conflict( return Conflict(
cls.game.theater, cls.game.theater,
@ -103,7 +104,7 @@ class Operation:
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
cls.game.enemy_country, cls.game.enemy_country,
mid_point mid_point,
) )
@classmethod @classmethod
@ -118,9 +119,11 @@ class Operation:
p_country = cls.game.player_country p_country = cls.game.player_country
e_country = cls.game.enemy_country e_country = cls.game.enemy_country
cls.current_mission.coalition["blue"].add_country( cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()) country_dict[db.country_id_from_name(p_country)]()
)
cls.current_mission.coalition["red"].add_country( cls.current_mission.coalition["red"].add_country(
country_dict[db.country_id_from_name(e_country)]()) country_dict[db.country_id_from_name(e_country)]()
)
@classmethod @classmethod
def inject_lua_trigger(cls, contents: str, comment: str) -> None: def inject_lua_trigger(cls, contents: str, comment: str) -> None:
@ -133,12 +136,11 @@ class Operation:
cls.plugin_scripts.append(mnemonic) cls.plugin_scripts.append(mnemonic)
@classmethod @classmethod
def inject_plugin_script(cls, plugin_mnemonic: str, script: str, def inject_plugin_script(
script_mnemonic: str) -> None: cls, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
if script_mnemonic in cls.plugin_scripts: if script_mnemonic in cls.plugin_scripts:
logging.debug( logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
f"Skipping already loaded {script} for {plugin_mnemonic}"
)
else: else:
cls.plugin_scripts.append(script_mnemonic) cls.plugin_scripts.append(script_mnemonic)
@ -146,15 +148,12 @@ class Operation:
script_path = Path(plugin_path, script) script_path = Path(plugin_path, script)
if not script_path.exists(): if not script_path.exists():
logging.error( logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
)
return return
trigger = TriggerStart(comment=f"Load {script_mnemonic}") trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve() filename = script_path.resolve()
fileref = cls.current_mission.map_resource.add_resource_file( fileref = cls.current_mission.map_resource.add_resource_file(filename)
filename)
trigger.add_action(DoScriptFile(fileref)) trigger.add_action(DoScriptFile(fileref))
cls.current_mission.triggerrules.triggers.append(trigger) cls.current_mission.triggerrules.triggers.append(trigger)
@ -166,11 +165,10 @@ class Operation:
jtacs: List[JtacInfo], jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
): ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings) """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
"""
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game), KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game) BriefingGenerator(cls.current_mission, cls.game),
] ]
for gen in gens: for gen in gens:
for dynamic_runway in groundobjectgen.runways.values(): for dynamic_runway in groundobjectgen.runways.values():
@ -208,8 +206,9 @@ class Operation:
cls.radio_registry.reserve(frequency) cls.radio_registry.reserve(frequency)
@classmethod @classmethod
def assign_channels_to_flights(cls, flights: List[FlightData], def assign_channels_to_flights(
air_support: AirSupport) -> None: cls, flights: List[FlightData], air_support: AirSupport
) -> None:
"""Assigns preset radio channels for client flights.""" """Assigns preset radio channels for client flights."""
for flight in flights: for flight in flights:
if not flight.client_units: if not flight.client_units:
@ -217,8 +216,7 @@ class Operation:
cls.assign_channels_to_flight(flight, air_support) cls.assign_channels_to_flight(flight, air_support)
@staticmethod @staticmethod
def assign_channels_to_flight(flight: FlightData, def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight.""" """Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type airframe = flight.aircraft_type
@ -234,7 +232,9 @@ class Operation:
) )
@classmethod @classmethod
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: def _create_tacan_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
""" """
Dedup beacon/radio frequencies, since some maps have some frequencies Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times. used multiple times.
@ -246,13 +246,14 @@ class Operation:
unique_map_frequencies.add(beacon.frequency) unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan: if beacon.is_tacan:
if beacon.channel is None: if beacon.channel is None:
logging.error( logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
f"TACAN beacon has no channel: {beacon.callsign}")
else: else:
cls.tacan_registry.reserve(beacon.tacan_channel) cls.tacan_registry.reserve(beacon.tacan_channel)
@classmethod @classmethod
def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: def _create_radio_registry(
cls, unique_map_frequencies: Set[RadioFrequency]
) -> None:
cls.radio_registry = RadioRegistry() cls.radio_registry = RadioRegistry()
for data in AIRFIELD_DATA.values(): for data in AIRFIELD_DATA.values():
if data.theater == cls.game.theater.terrain.name and data.atc: if data.theater == cls.game.theater.terrain.name and data.atc:
@ -270,7 +271,7 @@ class Operation:
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.tacan_registry, cls.tacan_registry,
cls.unit_map cls.unit_map,
) )
cls.groundobjectgen.generate() cls.groundobjectgen.generate()
@ -284,10 +285,13 @@ class Operation:
continue continue
pos = Point(d["x"], d["z"]) pos = Point(d["x"], d["z"])
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units: if (
utype is not None
and not cls.game.position_culled(pos)
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group( cls.current_mission.static_group(
country=cls.current_mission.country( country=cls.current_mission.country(cls.game.player_country),
cls.game.player_country),
name="", name="",
_type=utype, _type=utype,
hidden=True, hidden=True,
@ -302,13 +306,13 @@ class Operation:
cls.create_unit_map() cls.create_unit_map()
cls.create_radio_registries() cls.create_radio_registries()
# Set mission time and weather conditions. # Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls.game.conditions).generate()
cls._generate_ground_units() cls._generate_ground_units()
cls._generate_destroyed_units() cls._generate_destroyed_units()
cls._generate_air_units() cls._generate_air_units()
cls.assign_channels_to_flights(cls.airgen.flights, cls.assign_channels_to_flights(
cls.airsupportgen.air_support) cls.airgen.flights, cls.airsupportgen.air_support
)
cls._generate_ground_conflicts() cls._generate_ground_conflicts()
# Triggers # Triggers
@ -317,14 +321,16 @@ class Operation:
# Setup combined arms parameters # Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0 cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]: if cls.game.player_country in [
country.name
for country in cls.current_mission.coalition["blue"].countries.values()
]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else: else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
# Options # Options
forcedoptionsgen = ForcedOptionsGenerator( forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
cls.current_mission, cls.game)
forcedoptionsgen.generate() forcedoptionsgen.generate()
# Generate Visuals Smoke Effects # Generate Visuals Smoke Effects
@ -341,13 +347,11 @@ class Operation:
plugin.inject_scripts(cls) plugin.inject_scripts(cls)
plugin.inject_configuration(cls) plugin.inject_configuration(cls)
cls.assign_channels_to_flights(cls.airgen.flights, cls.assign_channels_to_flights(
cls.airsupportgen.air_support) cls.airgen.flights, cls.airsupportgen.air_support
)
cls.notify_info_generators( cls.notify_info_generators(
cls.groundobjectgen, cls.groundobjectgen, cls.airsupportgen, cls.jtacs, cls.airgen
cls.airsupportgen,
cls.jtacs,
cls.airgen
) )
cls.reset_naming_ids() cls.reset_naming_ids()
return cls.unit_map return cls.unit_map
@ -359,29 +363,38 @@ class Operation:
# Air Support (Tanker & Awacs) # Air Support (Tanker & Awacs)
assert cls.radio_registry and cls.tacan_registry assert cls.radio_registry and cls.tacan_registry
cls.airsupportgen = AirSupportConflictGenerator( cls.airsupportgen = AirSupportConflictGenerator(
cls.current_mission, cls.air_conflict(), cls.game, cls.radio_registry, cls.current_mission,
cls.tacan_registry) cls.air_conflict(),
cls.game,
cls.radio_registry,
cls.tacan_registry,
)
cls.airsupportgen.generate() cls.airsupportgen.generate()
# Generate Aircraft Activity on the map # Generate Aircraft Activity on the map
cls.airgen = AircraftConflictGenerator( cls.airgen = AircraftConflictGenerator(
cls.current_mission, cls.game.settings, cls.game, cls.current_mission,
cls.radio_registry, cls.unit_map) cls.game.settings,
cls.game,
cls.radio_registry,
cls.unit_map,
)
cls.airgen.clear_parking_slots() cls.airgen.clear_parking_slots()
cls.airgen.generate_flights( cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato, cls.game.blue_ato,
cls.groundobjectgen.runways cls.groundobjectgen.runways,
) )
cls.airgen.generate_flights( cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato, cls.game.red_ato,
cls.groundobjectgen.runways cls.groundobjectgen.runways,
) )
cls.airgen.spawn_unused_aircraft( cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country)) cls.current_mission.country(cls.game.enemy_country),
)
@classmethod @classmethod
def _generate_ground_conflicts(cls) -> None: def _generate_ground_conflicts(cls) -> None:
@ -396,17 +409,19 @@ class Operation:
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
player_cp, player_cp,
enemy_cp, enemy_cp,
cls.game.theater cls.game.theater,
) )
# Generate frontline ops # Generate frontline ops
player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = GroundConflictGenerator( ground_conflict_gen = GroundConflictGenerator(
cls.current_mission, cls.current_mission,
conflict, cls.game, conflict,
player_gp, enemy_gp, cls.game,
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id], player_cp.stances[enemy_cp.id],
cls.unit_map cls.unit_map,
) )
ground_conflict_gen.generate() ground_conflict_gen.generate()
cls.jtacs.extend(ground_conflict_gen.jtacs) cls.jtacs.extend(ground_conflict_gen.jtacs)
@ -416,9 +431,12 @@ class Operation:
namegen.reset_numbers() namegen.reset_numbers()
@classmethod @classmethod
def generate_lua(cls, airgen: AircraftConflictGenerator, def generate_lua(
airsupportgen: AirSupportConflictGenerator, cls,
jtacs: List[JtacInfo]) -> None: airgen: AircraftConflictGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
) -> None:
# TODO: Refactor this # TODO: Refactor this
luaData = { luaData = {
"AircraftCarriers": {}, "AircraftCarriers": {},
@ -434,7 +452,7 @@ class Operation:
"callsign": tanker.callsign, "callsign": tanker.callsign,
"variant": tanker.variant, "variant": tanker.variant,
"radio": tanker.freq.mhz, "radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name "tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
} }
if airsupportgen.air_support.awacs: if airsupportgen.air_support.awacs:
@ -442,7 +460,7 @@ class Operation:
luaData["AWACs"][awacs.callsign] = { luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName, "dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign, "callsign": awacs.callsign,
"radio": awacs.freq.mhz "radio": awacs.freq.mhz,
} }
for jtac in jtacs: for jtac in jtacs:
@ -451,14 +469,16 @@ class Operation:
"callsign": jtac.callsign, "callsign": jtac.callsign,
"zone": jtac.region, "zone": jtac.region,
"dcsUnit": jtac.unit_name, "dcsUnit": jtac.unit_name,
"laserCode": jtac.code "laserCode": jtac.code,
} }
for flight in airgen.flights: for flight in airgen.flights:
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, if flight.friendly and flight.flight_type in [
FlightType.DEAD, FlightType.ANTISHIP,
FlightType.SEAD, FlightType.DEAD,
FlightType.STRIKE]: FlightType.SEAD,
FlightType.STRIKE,
]:
flightType = str(flight.flight_type) flightType = str(flight.flight_type)
flightTarget = flight.package.target flightTarget = flight.package.target
if flightTarget: if flightTarget:
@ -466,23 +486,27 @@ class Operation:
flightTargetType = None flightTargetType = None
if isinstance(flightTarget, TheaterGroundObject): if isinstance(flightTarget, TheaterGroundObject):
flightTargetName = flightTarget.obj_name flightTargetName = flightTarget.obj_name
flightTargetType = flightType + \ flightTargetType = (
f" TGT ({flightTarget.category})" flightType + f" TGT ({flightTarget.category})"
elif hasattr(flightTarget, 'name'): )
elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)" flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = { luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName, "name": flightTargetName,
"type": flightTargetType, "type": flightTargetType,
"position": {"x": flightTarget.position.x, "position": {
"y": flightTarget.position.y} "x": flightTarget.position.x,
"y": flightTarget.position.y,
},
} }
# set a LUA table with data from Liberation that we want to set # 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 # 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 # 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_location = "[[" + os.path.abspath(".") + "]]"
lua = """ lua = (
"""
-- setting configuration table -- setting configuration table
env.info("DCSLiberation|: setting configuration table") env.info("DCSLiberation|: setting configuration table")
@ -490,9 +514,12 @@ class Operation:
dcsLiberation = {} dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory -- 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 + """ dcsLiberation.installPath="""
+ state_location
+ """
""" """
)
# Process the tankers # Process the tankers
lua += """ lua += """

View File

@ -67,4 +67,3 @@ def autosave(game) -> bool:
except Exception: except Exception:
logging.exception("Could not save game") logging.exception("Could not save game")
return False return False

View File

@ -14,9 +14,9 @@ if TYPE_CHECKING:
class LuaPluginWorkOrder: class LuaPluginWorkOrder:
def __init__(
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str, self, parent_mnemonic: str, filename: str, mnemonic: str, disable: bool
disable: bool) -> None: ) -> None:
self.parent_mnemonic = parent_mnemonic self.parent_mnemonic = parent_mnemonic
self.filename = filename self.filename = filename
self.mnemonic = mnemonic self.mnemonic = mnemonic
@ -26,8 +26,9 @@ class LuaPluginWorkOrder:
if self.disable: if self.disable:
operation.bypass_plugin_script(self.mnemonic) operation.bypass_plugin_script(self.mnemonic)
else: else:
operation.inject_plugin_script(self.parent_mnemonic, self.filename, operation.inject_plugin_script(
self.mnemonic) self.parent_mnemonic, self.filename, self.mnemonic
)
class PluginSettings: class PluginSettings:
@ -45,8 +46,7 @@ class PluginSettings:
# Plugin options are saved in the game's Settings, but it's possible for # 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 # plugins to change across loads. If new plugins are added or new
# options added to those plugins, initialize the new settings. # options added to those plugins, initialize the new settings.
self.settings.initialize_plugin_option(self.identifier, self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
self.enabled_by_default)
@property @property
def enabled(self) -> bool: def enabled(self) -> bool:
@ -57,8 +57,7 @@ class PluginSettings:
class LuaPluginOption(PluginSettings): class LuaPluginOption(PluginSettings):
def __init__(self, identifier: str, name: str, def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
enabled_by_default: bool) -> None:
super().__init__(identifier, enabled_by_default) super().__init__(identifier, enabled_by_default)
self.name = name self.name = name
@ -80,24 +79,34 @@ class LuaPluginDefinition:
options = [] options = []
for option in data.get("specificOptions"): for option in data.get("specificOptions"):
option_id = option["mnemonic"] option_id = option["mnemonic"]
options.append(LuaPluginOption( options.append(
identifier=f"{name}.{option_id}", LuaPluginOption(
name=option.get("nameInUI", name), identifier=f"{name}.{option_id}",
enabled_by_default=option.get("defaultValue") name=option.get("nameInUI", name),
)) enabled_by_default=option.get("defaultValue"),
)
)
work_orders = [] work_orders = []
for work_order in data.get("scriptsWorkOrders"): for work_order in data.get("scriptsWorkOrders"):
work_orders.append(LuaPluginWorkOrder( work_orders.append(
name, work_order.get("file"), work_order["mnemonic"], LuaPluginWorkOrder(
work_order.get("disable", False) name,
)) work_order.get("file"),
work_order["mnemonic"],
work_order.get("disable", False),
)
)
config_work_orders = [] config_work_orders = []
for work_order in data.get("configurationWorkOrders"): for work_order in data.get("configurationWorkOrders"):
config_work_orders.append(LuaPluginWorkOrder( config_work_orders.append(
name, work_order.get("file"), work_order["mnemonic"], LuaPluginWorkOrder(
work_order.get("disable", False) name,
)) work_order.get("file"),
work_order["mnemonic"],
work_order.get("disable", False),
)
)
return cls( return cls(
identifier=name, identifier=name,
@ -106,16 +115,14 @@ class LuaPluginDefinition:
enabled_by_default=data.get("defaultValue", False), enabled_by_default=data.get("defaultValue", False),
options=options, options=options,
work_orders=work_orders, work_orders=work_orders,
config_work_orders=config_work_orders config_work_orders=config_work_orders,
) )
class LuaPlugin(PluginSettings): class LuaPlugin(PluginSettings):
def __init__(self, definition: LuaPluginDefinition) -> None: def __init__(self, definition: LuaPluginDefinition) -> None:
self.definition = definition self.definition = definition
super().__init__(self.definition.identifier, super().__init__(self.definition.identifier, self.definition.enabled_by_default)
self.definition.enabled_by_default)
@property @property
def name(self) -> str: def name(self) -> str:
@ -155,12 +162,12 @@ class LuaPlugin(PluginSettings):
for option in self.options: for option in self.options:
enabled = str(option.enabled).lower() enabled = str(option.enabled).lower()
name = option.identifier name = option.identifier
option_decls.append( option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
f" dcsLiberation.plugins.{name} = {enabled}")
joined_options = "\n".join(option_decls) joined_options = "\n".join(option_decls)
lua = textwrap.dedent(f"""\ lua = textwrap.dedent(
f"""\
-- {self.identifier} plugin configuration. -- {self.identifier} plugin configuration.
if dcsLiberation then if dcsLiberation then
@ -171,10 +178,10 @@ class LuaPlugin(PluginSettings):
{joined_options} {joined_options}
end end
""") """
)
operation.inject_lua_trigger( operation.inject_lua_trigger(lua, f"{self.identifier} plugin configuration")
lua, f"{self.identifier} plugin configuration")
for work_order in self.definition.config_work_orders: for work_order in self.definition.config_work_orders:
work_order.work(operation) work_order.work(operation)

View File

@ -27,7 +27,8 @@ class LuaPluginManager:
if not plugin_path.exists(): if not plugin_path.exists():
raise RuntimeError( raise RuntimeError(
f"Invalid plugin configuration: required plugin {name} " f"Invalid plugin configuration: required plugin {name} "
f"does not exist at {plugin_path}") f"does not exist at {plugin_path}"
)
logging.info(f"Loading plugin {name} from {plugin_path}") logging.info(f"Loading plugin {name} from {plugin_path}")
plugin = LuaPlugin.from_json(name, plugin_path) plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None: if plugin is not None:

View File

@ -2,7 +2,6 @@ from dcs import Point
class PointWithHeading(Point): class PointWithHeading(Point):
def __init__(self): def __init__(self):
super(PointWithHeading, self).__init__(0, 0) super(PointWithHeading, self).__init__(0, 0)
self.heading = 0 self.heading = 0
@ -13,4 +12,4 @@ class PointWithHeading(Point):
p.x = point.x p.x = point.x
p.y = point.y p.y = point.y
p.heading = heading p.heading = heading
return p return p

View File

@ -35,9 +35,16 @@ class AircraftProcurementRequest:
class ProcurementAi: class ProcurementAi:
def __init__(self, game: Game, for_player: bool, faction: Faction, def __init__(
manage_runways: bool, manage_front_line: bool, self,
manage_aircraft: bool, front_line_budget_share: float) -> None: game: Game,
for_player: bool,
faction: Faction,
manage_runways: bool,
manage_front_line: bool,
manage_aircraft: bool,
front_line_budget_share: float,
) -> None:
if front_line_budget_share > 1.0: if front_line_budget_share > 1.0:
raise ValueError raise ValueError
@ -51,8 +58,8 @@ class ProcurementAi:
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(not self.is_player)
def spend_budget( def spend_budget(
self, budget: float, self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
aircraft_requests: List[AircraftProcurementRequest]) -> float: ) -> float:
if self.manage_runways: if self.manage_runways:
budget = self.repair_runways(budget) budget = self.repair_runways(budget)
if self.manage_front_line: if self.manage_front_line:
@ -100,25 +107,30 @@ class ProcurementAi:
budget -= db.RUNWAY_REPAIR_COST budget -= db.RUNWAY_REPAIR_COST
if self.is_player: if self.is_player:
self.game.message( self.game.message(
"OPFOR has begun repairing the runway at " "OPFOR has begun repairing the runway at " f"{control_point}"
f"{control_point}"
) )
else: else:
self.game.message( self.game.message(
"We have begun repairing the runway at " "We have begun repairing the runway at " f"{control_point}"
f"{control_point}"
) )
return budget return budget
def random_affordable_ground_unit( def random_affordable_ground_unit(
self, budget: float, self, budget: float, cp: ControlPoint
cp: ControlPoint) -> Optional[Type[VehicleType]]: ) -> Optional[Type[VehicleType]]:
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if affordable_units = [
db.PRICES[u] <= budget] u
for u in self.faction.frontline_units + self.faction.artillery_units
if db.PRICES[u] <= budget
]
total_number_aa = cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count total_number_aa = (
total_non_aa = cp.base.total_armor + cp.pending_deliveries_count - total_number_aa cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
max_aa = math.ceil(total_non_aa/8) )
total_non_aa = (
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
)
max_aa = math.ceil(total_non_aa / 8)
# Limit the number of AA units the AI will buy # Limit the number of AA units the AI will buy
if not total_number_aa < max_aa: if not total_number_aa < max_aa:
@ -150,8 +162,12 @@ class ProcurementAi:
return budget return budget
def _affordable_aircraft_of_types( def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint, self,
number: int, max_price: float) -> Optional[Type[FlyingType]]: types: List[Type[FlyingType]],
airbase: ControlPoint,
number: int,
max_price: float,
) -> Optional[Type[FlyingType]]:
best_choice: Optional[Type[FlyingType]] = None best_choice: Optional[Type[FlyingType]] = None
for unit in [u for u in self.faction.aircrafts if u in types]: for unit in [u for u in self.faction.aircrafts if u in types]:
if db.PRICES[unit] * number > max_price: if db.PRICES[unit] * number > max_price:
@ -168,15 +184,15 @@ class ProcurementAi:
return best_choice return best_choice
def affordable_aircraft_for( def affordable_aircraft_for(
self, request: AircraftProcurementRequest, self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]: ) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_of_types( return self._affordable_aircraft_of_types(
aircraft_for_task(request.task_capability), aircraft_for_task(request.task_capability), airbase, request.number, budget
airbase, request.number, budget) )
def purchase_aircraft( def purchase_aircraft(
self, budget: float, self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
aircraft_requests: List[AircraftProcurementRequest]) -> float: ) -> float:
for request in aircraft_requests: for request in aircraft_requests:
for airbase in self.best_airbases_for(request): for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget) unit = self.affordable_aircraft_for(request, airbase, budget)
@ -201,11 +217,9 @@ class ProcurementAi:
return self.game.theater.enemy_points() return self.game.theater.enemy_points()
def best_airbases_for( def best_airbases_for(
self, self, request: AircraftProcurementRequest
request: AircraftProcurementRequest) -> Iterator[ControlPoint]: ) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields( distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
request.near
)
threatened = [] threatened = []
for cp in distance_cache.airfields_within(request.range): for cp in distance_cache.airfields_within(request.range):
if not cp.is_friendly(self.is_player): if not cp.is_friendly(self.is_player):

View File

@ -59,8 +59,7 @@ class Settings:
def plugin_settings_key(identifier: str) -> str: def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}" return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str, def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
default_value: bool) -> None:
try: try:
self.plugin_option(identifier) self.plugin_option(identifier)
except KeyError: except KeyError:

View File

@ -22,7 +22,6 @@ BASE_MIN_STRENGTH = 0
class Base: class Base:
def __init__(self): def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {} self.armor: Dict[Type[VehicleType], int] = {}
@ -57,23 +56,43 @@ class Base:
return sum(self.aa.values()) return sum(self.aa.values())
def total_units(self, task: Task) -> int: def total_units(self, task: Task) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]]) return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t in db.UNIT_BY_TASK[task]
]
)
def total_units_of_type(self, unit_type) -> int: def total_units_of_type(self, unit_type) -> int:
return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type]) return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t == unit_type
]
)
@property @property
def all_units(self): def all_units(self):
return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) return itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
def _find_best_unit(self, available_units: Dict[UnitType, int], def _find_best_unit(
for_type: Task, count: int) -> Dict[UnitType, int]: self, available_units: Dict[UnitType, int], for_type: Task, count: int
) -> Dict[UnitType, int]:
if count <= 0: if count <= 0:
logging.warning("{}: no units for {}".format(self, for_type)) logging.warning("{}: no units for {}".format(self, for_type))
return {} return {}
sorted_units = [key for key in available_units if sorted_units = [
key in db.UNIT_BY_TASK[for_type]] key for key in available_units if key in db.UNIT_BY_TASK[for_type]
]
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
result: Dict[UnitType, int] = {} result: Dict[UnitType, int] = {}
@ -94,14 +113,18 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result return result
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]: def _find_best_planes(
self, for_type: Task, count: int
) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count) return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
return self._find_best_unit(self.armor, for_type, count) return self._find_best_unit(self.armor, for_type, count)
def append_commision_points(self, for_type, points: float) -> int: def append_commision_points(self, for_type, points: float) -> int:
self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points self.commision_points[for_type] = (
self.commision_points.get(for_type, 0) + points
)
points = self.commision_points[for_type] points = self.commision_points[for_type]
if points >= 1: if points >= 1:
self.commision_points[for_type] = points - math.floor(points) self.commision_points[for_type] = points - math.floor(points)
@ -110,7 +133,9 @@ class Base:
return 0 return 0
def filter_units(self, applicable_units: typing.Collection): def filter_units(self, applicable_units: typing.Collection):
self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units} self.aircraft = {
k: v for k, v in self.aircraft.items() if k in applicable_units
}
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units} self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
def commision_units(self, units: typing.Dict[typing.Any, int]): def commision_units(self, units: typing.Dict[typing.Any, int]):
@ -122,7 +147,12 @@ class Base:
for_task = db.unit_task(unit_type) for_task = db.unit_task(unit_type)
target_dict = None target_dict = None
if for_task == AWACS or for_task == CAS or for_task == CAP or for_task == Embarking: if (
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
):
target_dict = self.aircraft target_dict = self.aircraft
elif for_task == PinpointStrike: elif for_task == PinpointStrike:
target_dict = self.armor target_dict = self.armor
@ -149,7 +179,7 @@ class Base:
if unit_type not in target_array: if unit_type not in target_array:
print("Base didn't find event type {}".format(unit_type)) print("Base didn't find event type {}".format(unit_type))
continue continue
target_array[unit_type] = max(target_array[unit_type] - count, 0) target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0: if target_array[unit_type] == 0:
del target_array[unit_type] del target_array[unit_type]
@ -166,12 +196,20 @@ class Base:
def scramble_count(self, multiplier: float, task: Task = None) -> int: def scramble_count(self, multiplier: float, task: Task = None) -> int:
if task: if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) count = sum(
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
)
else: else:
count = self.total_aircraft count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) return min(
min(
max(count, PLANES_SCRAMBLE_MIN_BASE),
int(PLANES_SCRAMBLE_MAX_BASE * multiplier),
),
count,
)
def assemble_count(self): def assemble_count(self):
return int(self.total_armor * 0.5) return int(self.total_armor * 0.5)
@ -202,4 +240,8 @@ class Base:
return self._find_best_armor(PinpointStrike, count) return self._find_best_armor(PinpointStrike, count)
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]: def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count()) return self._find_best_unit(
self.aa,
AirDefence,
count and min(count, self.total_aa) or self.assemble_aa_count(),
)

View File

@ -158,7 +158,8 @@ class MizCampaignLoader:
def country(self, blue: bool) -> Country: def country(self, blue: bool) -> Country:
country = self.mission.country( country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name) self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
)
# Should be guaranteed because we initialized them. # Should be guaranteed because we initialized them.
assert country assert country
return country return country
@ -255,22 +256,23 @@ class MizCampaignLoader:
for blue in (False, True): for blue in (False, True):
for group in self.off_map_spawns(blue): for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(next(self.control_point_id), control_point = OffMapSpawn(
str(group.name), group.position) next(self.control_point_id), str(group.name), group.position
)
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for group in self.carriers(blue): for group in self.carriers(blue):
# TODO: Name the carrier. # TODO: Name the carrier.
control_point = Carrier( control_point = Carrier(
"carrier", group.position, next(self.control_point_id)) "carrier", group.position, next(self.control_point_id)
)
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for group in self.lhas(blue): for group in self.lhas(blue):
# TODO: Name the LHA. # TODO: Name the LHA.
control_point = Lha( control_point = Lha("lha", group.position, next(self.control_point_id))
"lha", group.position, next(self.control_point_id))
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
@ -302,21 +304,21 @@ class MizCampaignLoader:
origin = self.theater.closest_control_point(waypoints[0]) origin = self.theater.closest_control_point(waypoints[0])
if origin is None: if origin is None:
raise RuntimeError( raise RuntimeError(
f"No control point near the first waypoint of {group.name}") f"No control point near the first waypoint of {group.name}"
)
destination = self.theater.closest_control_point(waypoints[-1]) destination = self.theater.closest_control_point(waypoints[-1])
if destination is None: if destination is None:
raise RuntimeError( raise RuntimeError(
f"No control point near the final waypoint of {group.name}") f"No control point near the final waypoint of {group.name}"
)
# Snap the begin and end points to the control points. # Snap the begin and end points to the control points.
waypoints[0] = origin.position waypoints[0] = origin.position
waypoints[-1] = destination.position waypoints[-1] = destination.position
front_line_id = f"{origin.id}|{destination.id}" front_line_id = f"{origin.id}|{destination.id}"
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints) front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
self.control_points[origin.id].connect( self.control_points[origin.id].connect(self.control_points[destination.id])
self.control_points[destination.id]) self.control_points[destination.id].connect(self.control_points[origin.id])
self.control_points[destination.id].connect(
self.control_points[origin.id])
return front_lines return front_lines
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
@ -329,52 +331,63 @@ class MizCampaignLoader:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS: if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append( closest.preset_locations.base_garrisons.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
else: else:
logging.warning( logging.warning(f"Found garrison unit too far from base: {group.name}")
f"Found garrison unit too far from base: {group.name}")
for group in self.sams: for group in self.sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS: if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append( closest.preset_locations.base_air_defense.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
else: else:
closest.preset_locations.strike_locations.append( closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ewrs: for group in self.ewrs:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append(PointWithHeading.from_point(group.position, group.units[0].heading)) closest.preset_locations.ewrs.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.offshore_strike_targets: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( closest.preset_locations.offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ships: for group in self.ships:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(PointWithHeading.from_point(group.position, group.units[0].heading)) closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.missile_sites: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append( closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.coastal_defenses: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append( closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_long_range_sams: for group in self.required_long_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append( closest.preset_locations.required_long_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.required_medium_range_sams: for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append( closest.preset_locations.required_medium_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)) PointWithHeading.from_point(group.position, group.units[0].heading)
)
def populate_theater(self) -> None: def populate_theater(self) -> None:
for control_point in self.control_points.values(): for control_point in self.control_points.values():
@ -428,8 +441,9 @@ class ConflictTheater:
logging.warning("Replacing existing frontline data") logging.warning("Replacing existing frontline data")
self._frontline_data = data self._frontline_data = data
def add_controlpoint(self, point: ControlPoint, def add_controlpoint(
connected_to: Optional[List[ControlPoint]] = None): self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
):
if connected_to is None: if connected_to is None:
connected_to = [] connected_to = []
for connected_point in connected_to: for connected_point in connected_to:
@ -503,7 +517,7 @@ class ConflictTheater:
nearest_point = Point(nearest_point.x, nearest_point.y) nearest_point = Point(nearest_point.x, nearest_point.y)
new_point = point.point_from_heading( new_point = point.point_from_heading(
point.heading_between_point(nearest_point), point.heading_between_point(nearest_point),
point.distance_to_point(nearest_point) + extend_dist point.distance_to_point(nearest_point) + extend_dist,
) )
return new_point return new_point
@ -517,7 +531,9 @@ class ConflictTheater:
def conflicts(self, from_player=True) -> Iterator[FrontLine]: def conflicts(self, from_player=True) -> Iterator[FrontLine]:
for cp in [x for x in self.controlpoints if x.captured == from_player]: for cp in [x for x in self.controlpoints if x.captured == from_player]:
for connected_point in [x for x in cp.connected_points if x.captured != from_player]: for connected_point in [
x for x in cp.connected_points if x.captured != from_player
]:
yield FrontLine(cp, connected_point, self) yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
@ -572,17 +588,22 @@ class ConflictTheater:
distances[cp.id] = dist distances[cp.id] = dist
closest_cp_id = min(distances, key=distances.get) # type: ignore closest_cp_id = min(distances, key=distances.get) # type: ignore
all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id] all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[
closest_cp_id
]
closest_opposing_cps = [ closest_opposing_cps = [
self.find_control_point_by_id(i) self.find_control_point_by_id(i)
for i for i in min(
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore all_cp_min_distances, key=all_cp_min_distances.get
) # type: ignore
] # type: List[ControlPoint] ] # type: List[ControlPoint]
assert len(closest_opposing_cps) == 2 assert len(closest_opposing_cps) == 2
if closest_opposing_cps[0].captured: if closest_opposing_cps[0].captured:
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps)) return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
else: else:
return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))) return cast(
Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))
)
def find_control_point_by_id(self, id: int) -> ControlPoint: def find_control_point_by_id(self, id: int) -> ControlPoint:
for i in self.controlpoints: for i in self.controlpoints:
@ -677,8 +698,7 @@ class PersianGulfTheater(ConflictTheater):
terrain = persiangulf.PersianGulf() terrain = persiangulf.PersianGulf()
overview_image = "persiangulf.gif" overview_image = "persiangulf.gif"
reference_points = ( reference_points = (
ReferencePoint(persiangulf.Jiroft_Airport.position, ReferencePoint(persiangulf.Jiroft_Airport.position, Point(1692, 1343)),
Point(1692, 1343)),
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)), ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
) )
landmap = load_landmap("resources\\gulflandmap.p") landmap = load_landmap("resources\\gulflandmap.p")
@ -727,7 +747,7 @@ class TheChannelTheater(ConflictTheater):
overview_image = "thechannel.gif" overview_image = "thechannel.gif"
reference_points = ( reference_points = (
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)), ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
ReferencePoint(thechannel.Detling.position, Point(706, 382)) ReferencePoint(thechannel.Detling.position, Point(706, 382)),
) )
landmap = load_landmap("resources\\channellandmap.p") landmap = load_landmap("resources\\channellandmap.p")
daytime_map = { daytime_map = {
@ -793,10 +813,10 @@ class FrontLine(MissionTarget):
""" """
def __init__( def __init__(
self, self,
control_point_a: ControlPoint, control_point_a: ControlPoint,
control_point_b: ControlPoint, control_point_b: ControlPoint,
theater: ConflictTheater theater: ConflictTheater,
) -> None: ) -> None:
self.control_point_a = control_point_a self.control_point_a = control_point_a
self.control_point_b = control_point_b self.control_point_b = control_point_b
@ -882,7 +902,7 @@ class FrontLine(MissionTarget):
according to the current strength of each control point according to the current strength of each control point
""" """
total_strength = ( total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength self.control_point_a.base.strength + self.control_point_b.base.strength
) )
if self.control_point_a.base.strength == 0: if self.control_point_a.base.strength == 0:
return self._adjust_for_min_dist(0) return self._adjust_for_min_dist(0)
@ -897,11 +917,11 @@ class FrontLine(MissionTarget):
constant of either end control point. constant of either end control point.
""" """
if (distance > self.attack_distance / 2) and ( if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
): ):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and ( elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE distance < FRONTLINE_MIN_CP_DISTANCE
): ):
distance = FRONTLINE_MIN_CP_DISTANCE distance = FRONTLINE_MIN_CP_DISTANCE
return distance return distance
@ -916,8 +936,8 @@ class FrontLine(MissionTarget):
) )
complex_frontlines = self.theater.frontline_data complex_frontlines = self.theater.frontline_data
if (complex_frontlines) and ( if (complex_frontlines) and (
(control_point_ids in complex_frontlines) (control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines) or (reversed_cp_ids in complex_frontlines)
): ):
# The frontline segments must be stored in the correct order for the distance algorithms to work. # The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after. # The points in the frontline are ordered from the id before the | to the id after.
@ -943,7 +963,7 @@ class FrontLine(MissionTarget):
@staticmethod @staticmethod
def load_json_frontlines( def load_json_frontlines(
theater: ConflictTheater theater: ConflictTheater,
) -> Optional[Dict[str, ComplexFrontLine]]: ) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json""" """Load complex frontlines from json"""
try: try:

View File

@ -71,6 +71,7 @@ class LocationType(Enum):
Shorad = "SHORAD" Shorad = "SHORAD"
StrikeTarget = "strike target" StrikeTarget = "strike target"
@dataclass @dataclass
class PresetLocations: class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file.""" """Defines the preset locations loaded from the campaign mission file."""
@ -230,10 +231,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Only airbases have IDs. # TODO: Only airbases have IDs.
# TODO: has_frontline is only reasonable for airbases. # TODO: has_frontline is only reasonable for airbases.
# TODO: cptype is obsolete. # TODO: cptype is obsolete.
def __init__(self, cp_id: int, name: str, position: Point, def __init__(
at: db.StartingPosition, size: int, self,
importance: float, has_frontline=True, cp_id: int,
cptype=ControlPointType.AIRBASE): name: str,
position: Point,
at: db.StartingPosition,
size: int,
importance: float,
has_frontline=True,
cptype=ControlPointType.AIRBASE,
):
super().__init__(name, position) super().__init__(name, position)
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.id = cp_id self.id = cp_id
@ -256,17 +264,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {} self.stances: Dict[int, CombatStance] = {}
from ..event import UnitsDeliveryEvent from ..event import UnitsDeliveryEvent
self.pending_unit_deliveries = UnitsDeliveryEvent(self) self.pending_unit_deliveries = UnitsDeliveryEvent(self)
self.target_position: Optional[Point] = None self.target_position: Optional[Point] = None
def __repr__(self): def __repr__(self):
return f"<{__class__}: {self.name}>" return f"<{__class__}: {self.name}>"
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: def ground_objects(self) -> List[TheaterGroundObject]:
return list( return list(itertools.chain(self.connected_objectives, self.base_defenses))
itertools.chain(self.connected_objectives, self.base_defenses))
@property @property
@abstractmethod @abstractmethod
@ -341,15 +349,18 @@ class ControlPoint(MissionTarget, ABC):
Get the carrier group name if the airbase is a carrier Get the carrier group name if the airbase is a carrier
:return: Carrier group name :return: Carrier group name
""" """
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, if self.cptype in [
ControlPointType.LHA_GROUP]: ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]:
for g in self.ground_objects: for g in self.ground_objects:
if g.dcs_identifier == "CARRIER": if g.dcs_identifier == "CARRIER":
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov]: CV_1143_5_Admiral_Kuznetsov,
]:
return group.name return group.name
elif g.dcs_identifier == "LHA": elif g.dcs_identifier == "LHA":
for group in g.groups: for group in g.groups:
@ -385,7 +396,8 @@ class ControlPoint(MissionTarget, ABC):
else: else:
logging.error( logging.error(
"Could not determine preset location type for " "Could not determine preset location type for "
f"{base_defense}. Assuming garrison type.") f"{base_defense}. Assuming garrison type."
)
self.preset_locations.base_garrisons.append(p) self.preset_locations.base_garrisons.append(p)
self.base_defenses = [] self.base_defenses = []
@ -395,15 +407,18 @@ class ControlPoint(MissionTarget, ABC):
game.adjust_budget(total, player=not self.captured) game.adjust_budget(total, player=not self.captured)
game.message( game.message(
f"{self.name} is not connected to any friendly points. Ground " f"{self.name} is not connected to any friendly points. Ground "
f"vehicles have been captured and sold for ${total}M.") f"vehicles have been captured and sold for ${total}M."
)
def retreat_ground_units(self, game: Game): def retreat_ground_units(self, game: Game):
# When there are multiple valid destinations, deliver units to whichever # When there are multiple valid destinations, deliver units to whichever
# base is least defended first. The closest approximation of unit # base is least defended first. The closest approximation of unit
# strength we have is price # strength we have is price
destinations = [GroundUnitDestination(cp) destinations = [
for cp in self.connected_points GroundUnitDestination(cp)
if cp.captured == self.captured] for cp in self.connected_points
if cp.captured == self.captured
]
if not destinations: if not destinations:
self.capture_equipment(game) self.capture_equipment(game)
return return
@ -416,8 +431,9 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commision_units({unit_type: 1}) destination.control_point.base.commision_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination) destination = heapq.heappushpop(destinations, destination)
def capture_aircraft(self, game: Game, airframe: Type[FlyingType], def capture_aircraft(
count: int) -> None: self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
try: try:
value = PRICES[airframe] * count value = PRICES[airframe] * count
except KeyError: except KeyError:
@ -428,11 +444,12 @@ class ControlPoint(MissionTarget, ABC):
game.message( game.message(
f"No valid retreat destination in range of {self.name} for " f"No valid retreat destination in range of {self.name} for "
f"{airframe.id}. {count} aircraft have been captured and sold for " f"{airframe.id}. {count} aircraft have been captured and sold for "
f"${value}M.") f"${value}M."
)
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, game: Game, self, game: Game, airframe: Type[FlyingType]
airframe: Type[FlyingType]) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent. # TODO: Should be airframe dependent.
max_retreat_distance = nautical_miles(200) max_retreat_distance = nautical_miles(200)
@ -448,8 +465,9 @@ class ControlPoint(MissionTarget, ABC):
return airbase return airbase
return None return None
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType], def _retreat_air_units(
count: int) -> None: self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
while count: while count:
logging.debug(f"Retreating {count} {airframe.id} from {self.name}") logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe) destination = self.aircraft_retreat_destination(game, airframe)
@ -482,6 +500,7 @@ class ControlPoint(MissionTarget, ABC):
self.clear_base_defenses() self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate() BaseDefenseGenerator(game, self).generate()
@abstractmethod @abstractmethod
@ -511,16 +530,19 @@ class ControlPoint(MissionTarget, ABC):
if issubclass(unit_bought, FlyingType): if issubclass(unit_bought, FlyingType):
on_order += self.pending_unit_deliveries.units[unit_bought] on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_aircraft, on_order, return PendingOccupancy(
self.aircraft_transferring(game)) self.base.total_aircraft, on_order, self.aircraft_transferring(game)
)
def unclaimed_parking(self, game: Game) -> int: def unclaimed_parking(self, game: Game) -> int:
return (self.total_aircraft_parking - return (
self.expected_aircraft_next_turn(game).total) self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
@abstractmethod @abstractmethod
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
... ...
@property @property
@ -571,7 +593,13 @@ class ControlPoint(MissionTarget, ABC):
Get number of pending frontline aa units Get number of pending frontline aa units
""" """
if self.pending_unit_deliveries: if self.pending_unit_deliveries:
return sum([v for k,v in self.pending_unit_deliveries.units.items() if k in TYPE_SHORAD]) return sum(
[
v
for k, v in self.pending_unit_deliveries.units.items()
if k in TYPE_SHORAD
]
)
else: else:
return 0 return 0
@ -595,9 +623,12 @@ class ControlPoint(MissionTarget, ABC):
continue continue
on_order += self.pending_unit_deliveries.units[unit_bought] on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_armor, on_order, return PendingOccupancy(
# Ground unit transfers not yet implemented. self.base.total_armor,
transferring=0) on_order,
# Ground unit transfers not yet implemented.
transferring=0,
)
@property @property
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
@ -605,6 +636,7 @@ class ControlPoint(MissionTarget, ABC):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.AEWC, FlightType.AEWC,
@ -613,17 +645,23 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def has_active_frontline(self) -> bool: def has_active_frontline(self) -> bool:
return any( return any(not c.is_friendly(self.captured) for c in self.connected_points)
not c.is_friendly(self.captured) for c in self.connected_points)
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__(
def __init__(self, airport: Airport, size: int, self, airport: Airport, size: int, importance: float, has_frontline=True
importance: float, has_frontline=True): ):
super().__init__(airport.id, airport.name, airport.position, airport, super().__init__(
size, importance, has_frontline, airport.id,
cptype=ControlPointType.AIRBASE) airport.name,
airport.position,
airport,
size,
importance,
has_frontline,
cptype=ControlPointType.AIRBASE,
)
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
@ -637,6 +675,7 @@ class Airfield(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
@ -667,8 +706,9 @@ class Airfield(ControlPoint):
def damage_runway(self) -> None: def damage_runway(self) -> None:
self.runway_status.damage() self.runway_status.damage()
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
assigner = RunwayAssigner(conditions) assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport) return assigner.get_preferred_runway(self.airport)
@ -686,13 +726,13 @@ class Airfield(ControlPoint):
class NavalControlPoint(ControlPoint, ABC): class NavalControlPoint(ControlPoint, ABC):
@property @property
def is_fleet(self) -> bool: def is_fleet(self) -> bool:
return True return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.INTERCEPTION # TODO: FlightType.INTERCEPTION
@ -716,14 +756,17 @@ class NavalControlPoint(ControlPoint, ABC):
for group in g.groups: for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, LHA_1_Tarawa, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov, LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock]: CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
]:
return True return True
return False return False
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
# TODO: Assign TACAN and ICLS earlier so we don't need this. # TODO: Assign TACAN and ICLS earlier so we don't need this.
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="") fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
return dynamic_runways.get(self.name, fallback) return dynamic_runways.get(self.name, fallback)
@ -746,12 +789,19 @@ class NavalControlPoint(ControlPoint, ABC):
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=False,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")
@ -769,12 +819,19 @@ class Carrier(NavalControlPoint):
class Lha(NavalControlPoint): class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=False, cptype=ControlPointType.LHA_GROUP) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=False,
cptype=ControlPointType.LHA_GROUP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("LHAs cannot be captured") raise RuntimeError("LHAs cannot be captured")
@ -792,15 +849,22 @@ class Lha(NavalControlPoint):
class OffMapSpawn(ControlPoint): class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return True return True
def __init__(self, cp_id: int, name: str, position: Point): def __init__(self, cp_id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(cp_id, name, position, at=position,
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM, super().__init__(
has_frontline=False, cptype=ControlPointType.OFF_MAP) cp_id,
name,
position,
at=position,
size=SIZE_REGULAR,
importance=IMPORTANCE_MEDIUM,
has_frontline=False,
cptype=ControlPointType.OFF_MAP,
)
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Off map control points cannot be captured") raise RuntimeError("Off map control points cannot be captured")
@ -819,8 +883,9 @@ class OffMapSpawn(ControlPoint):
def heading(self) -> int: def heading(self) -> int:
return 0 return 0
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.") logging.warning("TODO: Off map spawns have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="") return RunwayData(self.full_name, runway_heading=0, runway_name="")
@ -834,19 +899,27 @@ class OffMapSpawn(ControlPoint):
class Fob(ControlPoint): class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1, super().__init__(
has_frontline=True, cptype=ControlPointType.FOB) cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=True,
cptype=ControlPointType.FOB,
)
self.name = name self.name = name
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return False return False
def active_runway(self, conditions: Conditions, def active_runway(
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: FOBs have no runways.") logging.warning("TODO: FOBs have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="") return RunwayData(self.full_name, runway_heading=0, runway_name="")
@ -856,6 +929,7 @@ class Fob(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
FlightType.BARCAP, FlightType.BARCAP,

View File

@ -46,4 +46,3 @@ def poly_centroid(poly) -> Tuple[float, float]:
x = sum(x_list) / len(poly) x = sum(x_list) / len(poly)
y = sum(y_list) / len(poly) y = sum(y_list) / len(poly)
return (x, y) return (x, y)

View File

@ -29,6 +29,7 @@ class MissionTarget:
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield FlightType.BARCAP yield FlightType.BARCAP
else: else:

View File

@ -22,7 +22,8 @@ from game.theater.theatergroundobject import (
MissileSiteGroundObject, MissileSiteGroundObject,
SamGroundObject, SamGroundObject,
ShipGroundObject, ShipGroundObject,
VehicleGroupGroundObject, CoastalSiteGroundObject, VehicleGroupGroundObject,
CoastalSiteGroundObject,
) )
from game.version import VERSION from game.version import VERSION
from gen import namegen from gen import namegen
@ -77,9 +78,14 @@ class GeneratorSettings:
class GameGenerator: class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater, def __init__(
settings: Settings, self,
generator_settings: GeneratorSettings) -> None: player: str,
enemy: str,
theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings,
) -> None:
self.player = player self.player = player
self.enemy = enemy self.enemy = enemy
self.theater = theater self.theater = theater
@ -97,7 +103,7 @@ class GameGenerator:
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
settings=self.settings, settings=self.settings,
player_budget=self.generator_settings.player_budget, player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget enemy_budget=self.generator_settings.enemy_budget,
) )
GroundObjectGenerator(game, self.generator_settings).generate() GroundObjectGenerator(game, self.generator_settings).generate()
@ -109,7 +115,7 @@ class GameGenerator:
# Auto-capture half the bases if midgame. # Auto-capture half the bases if midgame.
if self.generator_settings.midgame: if self.generator_settings.midgame:
control_points = self.theater.controlpoints control_points = self.theater.controlpoints
for control_point in control_points[:len(control_points) // 2]: for control_point in control_points[: len(control_points) // 2]:
control_point.captured = True control_point.captured = True
# Remove carrier and lha, invert situation if needed # Remove carrier and lha, invert situation if needed
@ -141,28 +147,37 @@ class LocationFinder:
self.game = game self.game = game
self.control_point = control_point self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations( self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name) game.theater.terrain.name, control_point.full_name
)
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]: def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
position = self.control_point.preset_locations.random_for(location_type) position = self.control_point.preset_locations.random_for(location_type)
if position is not None: if position is not None:
return position return position
logging.warning(f"No campaign location for %s Mat %s", logging.warning(
location_type.value, self.control_point) f"No campaign location for %s Mat %s",
location_type.value,
self.control_point,
)
position = self.random_from_miz_data( position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget) location_type == LocationType.OffshoreStrikeTarget
)
if position is not None: if position is not None:
return position return position
logging.debug(f"No mizdata location for %s at %s", location_type.value, logging.debug(
self.control_point) f"No mizdata location for %s at %s", location_type.value, self.control_point
)
position = self.random_position(location_type) position = self.random_position(location_type)
if position is not None: if position is not None:
return position return position
logging.error(f"Could not find position for %s at %s", logging.error(
location_type.value, self.control_point) f"Could not find position for %s at %s",
location_type.value,
self.control_point,
)
return None return None
def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]: def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
@ -176,15 +191,20 @@ class LocationFinder:
return PointWithHeading.from_point(preset.position, preset.heading) return PointWithHeading.from_point(preset.position, preset.heading)
return None return None
def random_position(self, location_type: LocationType) -> Optional[PointWithHeading]: def random_position(
self, location_type: LocationType
) -> Optional[PointWithHeading]:
# TODO: Flesh out preset locations so we never hit this case. # TODO: Flesh out preset locations so we never hit this case.
if location_type == LocationType.Coastal: if location_type == LocationType.Coastal:
# No coastal locations generated randomly # No coastal locations generated randomly
return None return None
logging.warning("Falling back to random location for %s at %s", logging.warning(
location_type.value, self.control_point) "Falling back to random location for %s at %s",
location_type.value,
self.control_point,
)
is_base_defense = location_type in { is_base_defense = location_type in {
LocationType.BaseAirDefense, LocationType.BaseAirDefense,
@ -218,23 +238,28 @@ class LocationFinder:
min_range = 10000 min_range = 10000
max_range = 40000 max_range = 40000
position = self._find_random_position(min_range, max_range, position = self._find_random_position(
on_land, is_base_defense, min_range, max_range, on_land, is_base_defense, avoid_others
avoid_others) )
# Retry once, searching a bit further (On some big airbases, 3200 is too # Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be # short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away # problematic, as some base defense units would end up very far away
# from small airfields. # from small airfields.
if position is None and is_base_defense: if position is None and is_base_defense:
position = self._find_random_position(3200, 4800, position = self._find_random_position(
on_land, is_base_defense, 3200, 4800, on_land, is_base_defense, avoid_others
avoid_others) )
return position return position
def _find_random_position(self, min_range: int, max_range: int, def _find_random_position(
on_ground: bool, is_base_defense: bool, self,
avoid_others: bool) -> Optional[PointWithHeading]: min_range: int,
max_range: int,
on_ground: bool,
is_base_defense: bool,
avoid_others: bool,
) -> Optional[PointWithHeading]:
""" """
Find a valid ground object location Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on :param on_ground: Whether it should be on ground or on sea (True = on
@ -278,15 +303,21 @@ class LocationFinder:
for _ in range(300): for _ in range(300):
# Check if on land or sea # Check if on land or sea
p = PointWithHeading.from_point(near.random_point_within(max_range, min_range), random.randint(0, 360)) p = PointWithHeading.from_point(
near.random_point_within(max_range, min_range), random.randint(0, 360)
)
if is_valid(p): if is_valid(p):
return p return p
return None return None
class ControlPointGroundObjectGenerator: class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, generator_settings: GeneratorSettings, def __init__(
control_point: ControlPoint) -> None: self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
) -> None:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.control_point = control_point self.control_point = control_point
@ -327,15 +358,15 @@ class ControlPointGroundObjectGenerator:
self.generate_ship() self.generate_ship()
def generate_ship(self) -> None: def generate_ship(self) -> None:
point = self.location_finder.location_for( point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
LocationType.OffshoreStrikeTarget)
if point is None: if point is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = ShipGroundObject(namegen.random_objective_name(), group_id, point, g = ShipGroundObject(
self.control_point) namegen.random_objective_name(), group_id, point, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name) group = generate_ship_group(self.game, g, self.faction_name)
g.groups = [] g.groups = []
@ -358,13 +389,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not carrier_names: if not carrier_names:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers") f"{self.faction_name} has no carriers"
)
return False return False
# Create ground object group # Create ground object group
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = CarrierGroundObject(namegen.random_objective_name(), group_id, g = CarrierGroundObject(
self.control_point) namegen.random_objective_name(), group_id, self.control_point
)
group = generate_carrier_group(self.faction_name, self.game, g) group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@ -383,13 +416,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not lha_names: if not lha_names:
logging.info( logging.info(
f"Skipping generation of {self.control_point.name} because " f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs") f"{self.faction_name} has no LHAs"
)
return False return False
# Create ground object group # Create ground object group
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = LhaGroundObject(namegen.random_objective_name(), group_id, g = LhaGroundObject(
self.control_point) namegen.random_objective_name(), group_id, self.control_point
)
group = generate_lha_group(self.faction_name, self.game, g) group = generate_lha_group(self.faction_name, self.game, g)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@ -428,8 +463,9 @@ class BaseDefenseGenerator:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = EwrGroundObject(namegen.random_objective_name(), group_id, g = EwrGroundObject(
position, self.control_point) namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ewr_group(self.game, g, self.faction) group = generate_ewr_group(self.game, g, self.faction)
if group is None: if group is None:
@ -460,28 +496,35 @@ class BaseDefenseGenerator:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id, g = VehicleGroupGroundObject(
position, self.control_point, namegen.random_objective_name(),
for_airbase=True) group_id,
position,
self.control_point,
for_airbase=True,
)
group = generate_armor_group(self.faction_name, self.game, g) group = generate_armor_group(self.faction_name, self.game, g)
if group is None: if group is None:
logging.error( logging.error(f"Could not generate garrison at {self.control_point}")
f"Could not generate garrison at {self.control_point}")
return return
g.groups.append(group) g.groups.append(group)
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_sam(self) -> None: def generate_sam(self) -> None:
position = self.location_finder.location_for( position = self.location_finder.location_for(LocationType.BaseAirDefense)
LocationType.BaseAirDefense)
if position is None: if position is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=True) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction) groups = generate_anti_air_group(self.game, g, self.faction)
if not groups: if not groups:
@ -491,21 +534,25 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None: def generate_shorad(self) -> None:
position = self.location_finder.location_for( position = self.location_finder.location_for(LocationType.BaseAirDefense)
LocationType.BaseAirDefense)
if position is None: if position is None:
return return
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=True) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction, groups = generate_anti_air_group(
ranges=[{AirDefenseRange.Short}]) self.game, g, self.faction, ranges=[{AirDefenseRange.Short}]
)
if not groups: if not groups:
logging.error( logging.error(f"Could not generate SHORAD group at {self.control_point}")
f"Could not generate SHORAD group at {self.control_point}")
return return
g.groups = groups g.groups = groups
self.control_point.base_defenses.append(g) self.control_point.base_defenses.append(g)
@ -515,7 +562,7 @@ class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None: def generate(self) -> None:
self.generate_garrison() self.generate_garrison()
self.generate_fob_defenses() self.generate_fob_defenses()
def generate_fob_defenses(self): def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD, # First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison. # and a 1/2 chance of a garrison.
@ -534,9 +581,13 @@ class FobDefenseGenerator(BaseDefenseGenerator):
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, generator_settings: GeneratorSettings, def __init__(
control_point: ControlPoint, self,
templates: GroundObjectTemplates) -> None: game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates,
) -> None:
super().__init__(game, generator_settings, control_point) super().__init__(game, generator_settings, control_point)
self.templates = templates self.templates = templates
@ -585,18 +636,25 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
""" """
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.required_long_range_sams: for position in presets.required_long_range_sams:
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
{AirDefenseRange.Long}, position,
{AirDefenseRange.Medium}, ranges=[
{AirDefenseRange.Short}, {AirDefenseRange.Long},
]) {AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
for position in presets.required_medium_range_sams: for position in presets.required_medium_range_sams:
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
{AirDefenseRange.Medium}, position,
{AirDefenseRange.Short}, ranges=[
]) {AirDefenseRange.Medium},
return (len(presets.required_long_range_sams) + {AirDefenseRange.Short},
len(presets.required_medium_range_sams)) ],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
)
def generate_ground_point(self) -> None: def generate_ground_point(self) -> None:
try: try:
@ -627,8 +685,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y) template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject( g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point, obj_name,
unit["heading"], self.control_point, unit["type"]) category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
)
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
@ -636,23 +701,34 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
position = self.location_finder.location_for(LocationType.Sam) position = self.location_finder.location_for(LocationType.Sam)
if position is None: if position is None:
return return
self.generate_aa_at(position, ranges=[ self.generate_aa_at(
# Prefer to use proper SAMs, but fall back to SHORADs if needed. position,
{AirDefenseRange.Long, AirDefenseRange.Medium}, ranges=[
{AirDefenseRange.Short}, # Prefer to use proper SAMs, but fall back to SHORADs if needed.
]) {AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
def generate_aa_at( def generate_aa_at(
self, position: Point, self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
ranges: Iterable[Set[AirDefenseRange]]) -> None: ) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(
position, self.control_point, for_airbase=False) namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges) groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups: if not groups:
logging.error("Could not generate air defense group for %s at %s", logging.error(
g.name, self.control_point) "Could not generate air defense group for %s at %s",
g.name,
self.control_point,
)
return return
g.groups = groups g.groups = groups
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
@ -668,8 +744,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id, g = MissileSiteGroundObject(
position, self.control_point) namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_missile_group(self.game, g, self.faction_name) group = generate_missile_group(self.game, g, self.faction_name)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@ -688,8 +765,13 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(namegen.random_objective_name(), group_id, g = CoastalSiteGroundObject(
position, self.control_point, position.heading) namegen.random_objective_name(),
group_id,
position,
self.control_point,
position.heading,
)
group = generate_coastal_group(self.game, g, self.faction_name) group = generate_coastal_group(self.game, g, self.faction_name)
g.groups = [] g.groups = []
if group is not None: if group is not None:
@ -707,7 +789,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate_fob(self) -> None: def generate_fob(self) -> None:
try: try:
category = self.faction.building_set[self.faction.building_set.index('fob')] category = self.faction.building_set[self.faction.building_set.index("fob")]
except IndexError: except IndexError:
logging.exception("Faction has no fob buildings defined") logging.exception("Faction has no fob buildings defined")
return return
@ -725,14 +807,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y) template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject( g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point, obj_name,
unit["heading"], self.control_point, unit["type"], airbase_group=True) category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
airbase_group=True,
)
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
class GroundObjectGenerator: class GroundObjectGenerator:
def __init__(self, game: Game, def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
generator_settings: GeneratorSettings) -> None:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
with open("resources/groundobject_templates.p", "rb") as f: with open("resources/groundobject_templates.p", "rb") as f:
@ -750,19 +839,22 @@ class GroundObjectGenerator:
generator: ControlPointGroundObjectGenerator generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator( generator = CarrierGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif control_point.cptype == ControlPointType.LHA_GROUP: elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator( generator = LhaGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif isinstance(control_point, OffMapSpawn): elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator( generator = NoOpGroundObjectGenerator(
self.game, self.generator_settings, control_point) self.game, self.generator_settings, control_point
)
elif isinstance(control_point, Fob): elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator( generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.game, self.generator_settings, control_point, self.templates
self.templates) )
else: else:
generator = AirbaseGroundObjectGenerator( generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point, self.game, self.generator_settings, control_point, self.templates
self.templates) )
return generator.generate() return generator.generate()

View File

@ -33,7 +33,7 @@ NAME_BY_CATEGORY = {
"ww2bunker": "Bunker", "ww2bunker": "Bunker",
"village": "Village", "village": "Village",
"allycamp": "Camp", "allycamp": "Camp",
"EWR":"EWR", "EWR": "EWR",
} }
ABBREV_NAME = { ABBREV_NAME = {
@ -54,34 +54,59 @@ ABBREV_NAME = {
} }
CATEGORY_MAP = { CATEGORY_MAP = {
# Special cases # Special cases
"CARRIER": ["CARRIER"], "CARRIER": ["CARRIER"],
"LHA": ["LHA"], "LHA": ["LHA"],
"aa": ["AA"], "aa": ["AA"],
# Buildings # Buildings
"power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"], "power": [
"Workshop A",
"Electric power box",
"Garage small A",
"Farm B",
"Repair workshop",
"Garage B",
],
"ware": ["Warehouse", "Hangar A"], "ware": ["Warehouse", "Hangar A"],
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"], "fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
"ammo": [".Ammunition depot", "Hangar B"], "ammo": [".Ammunition depot", "Hangar B"],
"farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"], "farp": [
"FARP Tent",
"FARP Ammo Dump Coating",
"FARP Fuel Depot",
"FARP Command Post",
"FARP CP Blindage",
],
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"], "fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
"factory": ["Tech combine", "Tech hangar A"], "factory": ["Tech combine", "Tech hangar A"],
"comms": ["TV tower", "Comms tower M"], "comms": ["TV tower", "Comms tower M"],
"oil": ["Oil platform"], "oil": ["Oil platform"],
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"], "derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
"ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"], "ww2bunker": [
"Siegfried Line",
"Fire Control Bunker",
"SK_C_28_naval_gun",
"Concertina Wire",
"Czech hedgehogs 1",
],
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"], "village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
"allycamp": [], "allycamp": [],
} }
class TheaterGroundObject(MissionTarget): class TheaterGroundObject(MissionTarget):
def __init__(
def __init__(self, name: str, category: str, group_id: int, position: Point, self,
heading: int, control_point: ControlPoint, dcs_identifier: str, name: str,
airbase_group: bool, sea_object: bool) -> None: category: str,
group_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group: bool,
sea_object: bool,
) -> None:
super().__init__(name, position) super().__init__(name, position)
self.category = category self.category = category
self.group_id = group_id self.group_id = group_id
@ -131,6 +156,7 @@ class TheaterGroundObject(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [ yield from [
# TODO: FlightType.LOGISTICS # TODO: FlightType.LOGISTICS
@ -193,9 +219,18 @@ class TheaterGroundObject(MissionTarget):
class BuildingGroundObject(TheaterGroundObject): class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(
position: Point, heading: int, control_point: ControlPoint, self,
dcs_identifier: str, airbase_group=False) -> None: name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group=False,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category=category, category=category,
@ -205,7 +240,7 @@ class BuildingGroundObject(TheaterGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=airbase_group, airbase_group=airbase_group,
sea_object=False sea_object=False,
) )
self.object_id = object_id self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but # Other TGOs track deadness based on the number of alive units, but
@ -234,6 +269,7 @@ class BuildingGroundObject(TheaterGroundObject):
class NavalGroundObject(TheaterGroundObject): class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.ANTISHIP yield FlightType.ANTISHIP
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@ -249,8 +285,7 @@ class GenericCarrierGroundObject(NavalGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject): class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
control_point: ControlPoint) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="CARRIER", category="CARRIER",
@ -260,7 +295,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="CARRIER", dcs_identifier="CARRIER",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True,
) )
@property @property
@ -272,8 +307,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject): class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
control_point: ControlPoint) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="LHA", category="LHA",
@ -283,7 +317,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="LHA", dcs_identifier="LHA",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True,
) )
@property @property
@ -294,8 +328,9 @@ class LhaGroundObject(GenericCarrierGroundObject):
class MissileSiteGroundObject(TheaterGroundObject): class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@ -305,13 +340,19 @@ class MissileSiteGroundObject(TheaterGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=False sea_object=False,
) )
class CoastalSiteGroundObject(TheaterGroundObject): class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint, heading) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
heading,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@ -321,11 +362,10 @@ class CoastalSiteGroundObject(TheaterGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=False sea_object=False,
) )
class BaseDefenseGroundObject(TheaterGroundObject): class BaseDefenseGroundObject(TheaterGroundObject):
"""Base type for all base defenses.""" """Base type for all base defenses."""
@ -334,8 +374,14 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types. # be split into their own types.
class SamGroundObject(BaseDefenseGroundObject): class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint, for_airbase: bool) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@ -345,7 +391,7 @@ class SamGroundObject(BaseDefenseGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase, airbase_group=for_airbase,
sea_object=False sea_object=False,
) )
# Set by the SAM unit generator if the generated group is compatible # Set by the SAM unit generator if the generated group is compatible
# with Skynet. # with Skynet.
@ -362,6 +408,7 @@ class SamGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.DEAD yield FlightType.DEAD
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@ -372,8 +419,14 @@ class SamGroundObject(BaseDefenseGroundObject):
class VehicleGroupGroundObject(BaseDefenseGroundObject): class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint, for_airbase: bool) -> None: self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@ -383,13 +436,14 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase, airbase_group=for_airbase,
sea_object=False sea_object=False,
) )
class EwrGroundObject(BaseDefenseGroundObject): class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="EWR", category="EWR",
@ -399,7 +453,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="EWR", dcs_identifier="EWR",
airbase_group=True, airbase_group=True,
sea_object=False sea_object=False,
) )
@property @property
@ -409,6 +463,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.DEAD yield FlightType.DEAD
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@ -419,8 +474,9 @@ class EwrGroundObject(BaseDefenseGroundObject):
class ShipGroundObject(NavalGroundObject): class ShipGroundObject(NavalGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(
control_point: ControlPoint) -> None: self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
@ -430,7 +486,7 @@ class ShipGroundObject(NavalGroundObject):
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=True sea_object=True,
) )
@property @property

View File

@ -32,8 +32,9 @@ class ThreatZones:
self.all = unary_union([airbases, air_defenses]) self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint: def closest_boundary(self, point: DcsPoint) -> DcsPoint:
boundary, _ = nearest_points(self.all.boundary, boundary, _ = nearest_points(
self.dcs_to_shapely_point(point)) self.all.boundary, self.dcs_to_shapely_point(point)
)
return DcsPoint(boundary.x, boundary.y) return DcsPoint(boundary.x, boundary.y)
@singledispatchmethod @singledispatchmethod
@ -49,8 +50,9 @@ class ThreatZones:
return self.all.intersects(self.dcs_to_shapely_point(position)) return self.all.intersects(self.dcs_to_shapely_point(position))
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool: def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
return self.threatened(LineString( return self.threatened(
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])) LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
)
@singledispatchmethod @singledispatchmethod
def threatened_by_aircraft(self, target) -> bool: def threatened_by_aircraft(self, target) -> bool:
@ -62,9 +64,9 @@ class ThreatZones:
@threatened_by_aircraft.register @threatened_by_aircraft.register
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool: def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
return self.threatened_by_aircraft(LineString(( return self.threatened_by_aircraft(
self.dcs_to_shapely_point(p.position) for p in flight.points LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
))) )
@singledispatchmethod @singledispatchmethod
def threatened_by_air_defense(self, target) -> bool: def threatened_by_air_defense(self, target) -> bool:
@ -76,13 +78,14 @@ class ThreatZones:
@threatened_by_air_defense.register @threatened_by_air_defense.register
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool: def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
return self.threatened_by_air_defense(LineString(( return self.threatened_by_air_defense(
self.dcs_to_shapely_point(p.position) for p in flight.points LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
))) )
@classmethod @classmethod
def closest_enemy_airbase(cls, location: ControlPoint, def closest_enemy_airbase(
max_distance: Distance) -> Optional[ControlPoint]: cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location) airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.airfields_within(max_distance): for airfield in airfields.airfields_within(max_distance):
if airfield.captured != location.captured: if airfield.captured != location.captured:
@ -90,13 +93,14 @@ class ThreatZones:
return None return None
@classmethod @classmethod
def barcap_threat_range(cls, game: Game, def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
control_point: ControlPoint) -> Distance:
doctrine = game.faction_for(control_point.captured).doctrine doctrine = game.faction_for(control_point.captured).doctrine
cap_threat_range = (doctrine.cap_max_distance_from_cp + cap_threat_range = (
doctrine.cap_engagement_range) doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
opposing_airfield = cls.closest_enemy_airbase(control_point, )
cap_threat_range * 2) opposing_airfield = cls.closest_enemy_airbase(
control_point, cap_threat_range * 2
)
if opposing_airfield is None: if opposing_airfield is None:
return cap_threat_range return cap_threat_range
@ -133,8 +137,7 @@ class ThreatZones:
if control_point.captured != player: if control_point.captured != player:
continue continue
if control_point.runway_is_operational(): if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x, point = ShapelyPoint(control_point.position.x, control_point.position.y)
control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point) cap_threat_range = cls.barcap_threat_range(game, control_point)
airbases.append(point.buffer(cap_threat_range.meters)) airbases.append(point.buffer(cap_threat_range.meters))
@ -149,10 +152,9 @@ class ThreatZones:
air_defenses.append(threat_zone) air_defenses.append(threat_zone)
return cls( return cls(
airbases=unary_union(airbases), airbases=unary_union(airbases), air_defenses=unary_union(air_defenses)
air_defenses=unary_union(air_defenses)
) )
@staticmethod @staticmethod
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint: def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
return ShapelyPoint(point.x, point.y) return ShapelyPoint(point.x, point.y)

View File

@ -70,15 +70,19 @@ class UnitMap:
raise RuntimeError(f"Unknown unit type: {unit.type}") raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType): if not issubclass(unit_type, VehicleType):
raise RuntimeError( raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType") f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.front_line_units[name] = FrontLineUnit(unit_type, origin) self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None) return self.front_line_units.get(name, None)
def add_ground_object_units(self, ground_object: TheaterGroundObject, def add_ground_object_units(
persistence_group: Group, self,
miz_group: Group) -> None: ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group,
) -> None:
"""Adds a group associated with a TGO to the unit map. """Adds a group associated with a TGO to the unit map.
Args: Args:
@ -103,13 +107,13 @@ class UnitMap:
if name in self.ground_object_units: if name in self.ground_object_units:
raise RuntimeError(f"Duplicate TGO unit: {name}") raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_object_units[name] = GroundObjectUnit( self.ground_object_units[name] = GroundObjectUnit(
ground_object, persistence_group, persistent_unit) ground_object, persistence_group, persistent_unit
)
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]: def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
group: Group) -> None:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object" # The name of the initiator in the DCS dead event will have " object"
@ -119,8 +123,9 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}") raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object) self.buildings[name] = Building(ground_object)
def add_fortification(self, ground_object: BuildingGroundObject, def add_fortification(
group: VehicleGroup) -> None: self, ground_object: BuildingGroundObject, group: VehicleGroup
) -> None:
if len(group.units) != 1: if len(group.units) != 1:
raise ValueError("Fortification groups must have exactly one unit.") raise ValueError("Fortification groups must have exactly one unit.")
unit = group.units[0] unit = group.units[0]

View File

@ -58,7 +58,7 @@ class Weather:
return None return None
return Fog( return Fog(
visibility=meters(random.randint(2500, 5000)), visibility=meters(random.randint(2500, 5000)),
thickness=random.randint(100, 500) thickness=random.randint(100, 500),
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@ -76,7 +76,7 @@ class Weather:
# Always some wind to make the smoke move a bit. # Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)), at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor), at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor) at_8000m=Wind(wind_direction, base_wind * at_8000m_factor),
) )
@staticmethod @staticmethod
@ -105,7 +105,7 @@ class Cloudy(Weather):
base=self.random_cloud_base(), base=self.random_cloud_base(),
density=random.randint(1, 8), density=random.randint(1, 8),
thickness=self.random_cloud_thickness(), thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.None_ precipitation=PydcsWeather.Preceptions.None_,
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@ -118,7 +118,7 @@ class Raining(Weather):
base=self.random_cloud_base(), base=self.random_cloud_base(),
density=random.randint(5, 8), density=random.randint(5, 8),
thickness=self.random_cloud_thickness(), thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Rain precipitation=PydcsWeather.Preceptions.Rain,
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@ -131,7 +131,7 @@ class Thunderstorm(Weather):
base=self.random_cloud_base(), base=self.random_cloud_base(),
density=random.randint(9, 10), density=random.randint(9, 10),
thickness=self.random_cloud_thickness(), thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Thunderstorm precipitation=PydcsWeather.Preceptions.Thunderstorm,
) )
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
@ -145,20 +145,29 @@ class Conditions:
weather: Weather weather: Weather
@classmethod @classmethod
def generate(cls, theater: ConflictTheater, day: datetime.date, def generate(
time_of_day: TimeOfDay, settings: Settings) -> Conditions: cls,
theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
settings: Settings,
) -> Conditions:
return cls( return cls(
time_of_day=time_of_day, time_of_day=time_of_day,
start_time=cls.generate_start_time( start_time=cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled theater, day, time_of_day, settings.night_disabled
), ),
weather=cls.generate_weather() weather=cls.generate_weather(),
) )
@classmethod @classmethod
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date, def generate_start_time(
time_of_day: TimeOfDay, cls,
night_disabled: bool) -> datetime.datetime: theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
night_disabled: bool,
) -> datetime.datetime:
if night_disabled: if night_disabled:
logging.info("Skip Night mission due to user settings") logging.info("Skip Night mission due to user settings")
time_range = { time_range = {
@ -181,6 +190,7 @@ class Conditions:
Cloudy: 60, Cloudy: 60,
ClearSkies: 20, ClearSkies: 20,
} }
weather_type = random.choices(list(chances.keys()), weather_type = random.choices(
weights=list(chances.values()))[0] list(chances.keys()), weights=list(chances.values())
)[0]
return weather_type() return weather_type()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,6 @@ from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
TANKER_DISTANCE = 15000 TANKER_DISTANCE = 15000
TANKER_ALT = 4572 TANKER_ALT = 4572
TANKER_HEADING_OFFSET = 45 TANKER_HEADING_OFFSET = 45
@ -35,6 +33,7 @@ AWACS_ALT = 13000
@dataclass @dataclass
class AwacsInfo: class AwacsInfo:
"""AWACS information for the kneeboard.""" """AWACS information for the kneeboard."""
dcsGroupName: str dcsGroupName: str
callsign: str callsign: str
freq: RadioFrequency freq: RadioFrequency
@ -43,6 +42,7 @@ class AwacsInfo:
@dataclass @dataclass
class TankerInfo: class TankerInfo:
"""Tanker information for the kneeboard.""" """Tanker information for the kneeboard."""
dcsGroupName: str dcsGroupName: str
callsign: str callsign: str
variant: str variant: str
@ -57,10 +57,14 @@ class AirSupport:
class AirSupportConflictGenerator: class AirSupportConflictGenerator:
def __init__(
def __init__(self, mission: Mission, conflict: Conflict, game, self,
radio_registry: RadioRegistry, mission: Mission,
tacan_registry: TacanRegistry) -> None: conflict: Conflict,
game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.game = game self.game = game
@ -81,22 +85,37 @@ class AirSupportConflictGenerator:
elif unit_type is KC135MPRS: elif unit_type is KC135MPRS:
return (TANKER_ALT + 500, 596) return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574) return (TANKER_ALT, 574)
def generate(self): def generate(self):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp player_cp = (
self.conflict.from_cp
if self.conflict.from_cp.captured
else self.conflict.to_cp
)
fallback_tanker_number = 0 fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)): for i, tanker_unit_type in enumerate(
db.find_unittype(Refueling, self.conflict.attackers_side)
):
alt, airspeed = self._get_tanker_params(tanker_unit_type) alt, airspeed = self._get_tanker_params(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type) variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i tanker_heading = (
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE) self.conflict.to_cp.position.heading_between_point(
self.conflict.from_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight( tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type), name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None, airport=None,
plane_type=tanker_unit_type, plane_type=tanker_unit_type,
position=tanker_position, position=tanker_position,
@ -127,14 +146,23 @@ class AirSupportConflictGenerator:
if tanker_unit_type != IL_78M: if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel. # Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop() tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(ActivateBeaconCommand( tanker_group.points[0].tasks.append(
tacan.number, tacan.band.value, tacan_callsign, True, ActivateBeaconCommand(
tanker_group.units[0].id, True)) tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)) self.air_support.tankers.append(
TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
)
if not self.game.settings.disable_legacy_aewc: if not self.game.settings.disable_legacy_aewc:
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side) possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
@ -145,11 +173,15 @@ class AirSupportConflictGenerator:
awacs_flight = self.mission.awacs_flight( awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)), name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
),
plane_type=awacs_unit, plane_type=awacs_unit,
altitude=AWACS_ALT, altitude=AWACS_ALT,
airport=None, airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE), position=self.conflict.position.random_point_within(
AWACS_DISTANCE, AWACS_DISTANCE
),
frequency=freq.mhz, frequency=freq.mhz,
start_type=StartType.Warm, start_type=StartType.Warm,
) )
@ -158,7 +190,12 @@ class AirSupportConflictGenerator:
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(AwacsInfo( self.air_support.awacs.append(
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq)) AwacsInfo(
str(awacs_flight.name),
callsign_for_support_unit(awacs_flight),
freq,
)
)
else: else:
logging.warning("No AWACS for faction") logging.warning("No AWACS for faction")

View File

@ -12,9 +12,17 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction from dcs.point import PointAction
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint, from dcs.task import (
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand, EPLRS,
SetInvisibleCommand) AttackGroup,
ControlledTask,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
@ -24,8 +32,11 @@ from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE, from gen.ground_forces.ai_ground_planner import (
CombatGroup, CombatGroupRole) DISTANCE_FROM_FRONTLINE,
CombatGroup,
CombatGroupRole,
)
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
@ -56,6 +67,7 @@ INFANTRY_GROUP_SIZE = 5
@dataclass(frozen=True) @dataclass(frozen=True)
class JtacInfo: class JtacInfo:
"""JTAC information.""" """JTAC information."""
dcsGroupName: str dcsGroupName: str
unit_name: str unit_name: str
callsign: str callsign: str
@ -65,16 +77,16 @@ class JtacInfo:
class GroundConflictGenerator: class GroundConflictGenerator:
def __init__( def __init__(
self, self,
mission: Mission, mission: Mission,
conflict: Conflict, conflict: Conflict,
game: Game, game: Game,
player_planned_combat_groups: List[CombatGroup], player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup], enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance, player_stance: CombatStance,
unit_map: UnitMap) -> None: unit_map: UnitMap,
) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups self.enemy_planned_combat_groups = enemy_planned_combat_groups
@ -87,14 +99,16 @@ class GroundConflictGenerator:
def _enemy_stance(self): def _enemy_stance(self):
"""Picks the enemy stance according to the number of planned groups on the frontline for each side""" """Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups): if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
):
return random.choice( return random.choice(
[ [
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION, CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH CombatStance.BREAKTHROUGH,
] ]
) )
else: else:
@ -104,31 +118,37 @@ class GroundConflictGenerator:
CombatStance.DEFENSIVE, CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE, CombatStance.DEFENSIVE,
CombatStance.AMBUSH, CombatStance.AMBUSH,
CombatStance.AGGRESSIVE CombatStance.AGGRESSIVE,
] ]
) )
@staticmethod @staticmethod
def _group_point(point: Point, base_distance) -> Point: def _group_point(point: Point, base_distance) -> Point:
distance = random.randint( distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]), int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]), int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
) )
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR) return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
def generate(self): def generate(self):
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) position = Conflict.frontline_position(
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
)
frontline_vector = Conflict.frontline_vector( frontline_vector = Conflict.frontline_vector(
self.conflict.from_cp, self.conflict.from_cp, self.conflict.to_cp, self.game.theater
self.conflict.to_cp, )
self.game.theater
)
# Create player groups at random position # Create player groups at random position
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True) player_groups = self._generate_groups(
self.player_planned_combat_groups, frontline_vector, True
)
# Create enemy groups at random position # Create enemy groups at random position
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False) enemy_groups = self._generate_groups(
self.enemy_planned_combat_groups, frontline_vector, False
)
# Plan combat actions for groups # Plan combat actions for groups
self.plan_action_for_groups( self.plan_action_for_groups(
@ -137,7 +157,7 @@ class GroundConflictGenerator:
enemy_groups, enemy_groups,
self.conflict.heading + 90, self.conflict.heading + 90,
self.conflict.from_cp, self.conflict.from_cp,
self.conflict.to_cp self.conflict.to_cp,
) )
self.plan_action_for_groups( self.plan_action_for_groups(
self.enemy_stance, self.enemy_stance,
@ -145,7 +165,7 @@ class GroundConflictGenerator:
player_groups, player_groups,
self.conflict.heading - 90, self.conflict.heading - 90,
self.conflict.to_cp, self.conflict.to_cp,
self.conflict.from_cp self.conflict.from_cp,
) )
# Add JTAC # Add JTAC
@ -157,34 +177,38 @@ class GroundConflictGenerator:
if self.game.player_faction.jtac_unit is not None: if self.game.player_faction.jtac_unit is not None:
utype = self.game.player_faction.jtac_unit utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country), jtac = self.mission.flight_group(
name=n, country=self.mission.country(self.game.player_country),
aircraft_type=utype, name=n,
position=position[0], aircraft_type=utype,
airport=None, position=position[0],
altitude=5000) airport=None,
altitude=5000,
)
jtac.points[0].tasks.append(SetInvisibleCommand(True)) jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True)) jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)) jtac.points[0].tasks.append(
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
)
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. # Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac) callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code))) self.jtacs.append(
JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
)
def gen_infantry_group_for_group( def gen_infantry_group_for_group(
self, self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
group: VehicleGroup,
is_player: bool,
side: Country,
forward_heading: int
) -> None: ) -> None:
infantry_position = self.conflict.find_ground_position( infantry_position = self.conflict.find_ground_position(
group.points[0].position.random_point_within(250, 50), group.points[0].position.random_point_within(250, 50),
500, 500,
forward_heading, forward_heading,
self.conflict.theater self.conflict.theater,
) )
if not infantry_position: if not infantry_position:
logging.warning("Could not find infantry position") logging.warning("Could not find infantry position")
return return
@ -208,44 +232,50 @@ class GroundConflictGenerator:
u = random.choice(manpads) u = random.choice(manpads)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
u,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad,
)
return return
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads) possible_infantry_units = db.find_infantry(
faction, allow_manpad=self.game.settings.manpads
)
if len(possible_infantry_units) == 0: if len(possible_infantry_units) == 0:
return return
u = random.choice(possible_infantry_units) u = random.choice(possible_infantry_units)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
position=infantry_position, u,
group_size=1, position=infantry_position,
heading=forward_heading, group_size=1,
move_formation=PointAction.OffRoad) heading=forward_heading,
move_formation=PointAction.OffRoad,
)
for i in range(INFANTRY_GROUP_SIZE): for i in range(INFANTRY_GROUP_SIZE):
u = random.choice(possible_infantry_units) u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5) position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group( self.mission.vehicle_group(
side, side,
namegen.next_infantry_name(side, cp.id, u), u, namegen.next_infantry_name(side, cp.id, u),
u,
position=position, position=position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad,
)
def _set_reform_waypoint( def _set_reform_waypoint(
self, self, dcs_group: VehicleGroup, forward_heading: int
dcs_group: VehicleGroup,
forward_heading: int
) -> None: ) -> None:
"""Setting a waypoint close to the spawn position allows the group to reform gracefully """Setting a waypoint close to the spawn position allows the group to reform gracefully
rather than spin rather than spin
""" """
reform_point = dcs_group.position.point_from_heading(forward_heading, 50) reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
dcs_group.add_waypoint(reform_point) dcs_group.add_waypoint(reform_point)
@ -256,7 +286,7 @@ class GroundConflictGenerator:
gen_group: CombatGroup, gen_group: CombatGroup,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
forward_heading: int, forward_heading: int,
target: Point target: Point,
) -> bool: ) -> bool:
""" """
Handles adding the DCS tasks for artillery groups for all combat stances. Handles adding the DCS tasks for artillery groups for all combat stances.
@ -269,7 +299,9 @@ class GroundConflictGenerator:
dcs_group.add_trigger_action(hold_task) dcs_group.add_trigger_action(hold_task)
# Artillery strike random start # Artillery strike random start
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)) artillery_trigger = TriggerOnce(
Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id)
)
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60)) artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
# TODO: Update to fire at group instead of point # TODO: Update to fire at group instead of point
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100) fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
@ -283,12 +315,19 @@ class GroundConflictGenerator:
# Hold position # Hold position
dcs_group.points[1].tasks.append(Hold()) dcs_group.points[1].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3)) retreat = self.find_retreat_point(
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad) dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
)
dcs_group.add_waypoint(
dcs_group.position.point_from_heading(forward_heading, 1),
PointAction.OffRoad,
)
dcs_group.points[2].tasks.append(Hold()) dcs_group.points[2].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad) dcs_group.add_waypoint(retreat, PointAction.OffRoad)
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)) artillery_fallback = TriggerOnce(
Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)
)
for i, u in enumerate(dcs_group.units): for i, u in enumerate(dcs_group.units):
artillery_fallback.add_condition(UnitDamaged(u.id)) artillery_fallback.add_condition(UnitDamaged(u.id))
if i < len(dcs_group.units) - 1: if i < len(dcs_group.units) - 1:
@ -302,7 +341,9 @@ class GroundConflictGenerator:
retreat_task.number = 4 retreat_task.number = 4
dcs_group.add_trigger_action(retreat_task) dcs_group.add_trigger_action(retreat_task)
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks))) artillery_fallback.add_action(
AITaskPush(dcs_group.id, len(dcs_group.tasks))
)
self.mission.triggerrules.triggers.append(artillery_fallback) self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units: for u in dcs_group.units:
@ -330,12 +371,8 @@ class GroundConflictGenerator:
target = self.find_nearest_enemy_group(dcs_group, enemy_groups) target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
if target is not None: if target is not None:
rand_offset = Point( rand_offset = Point(
random.randint( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
),
random.randint(
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
)
) )
target_point = self.conflict.theater.nearest_land_pos( target_point = self.conflict.theater.nearest_land_pos(
target.points[0].position + rand_offset target.points[0].position + rand_offset
@ -345,8 +382,7 @@ class GroundConflictGenerator:
if ( if (
to_cp.position.distance_to_point(dcs_group.points[0].position) to_cp.position.distance_to_point(dcs_group.points[0].position)
<= <= AGGRESIVE_MOVE_DISTANCE
AGGRESIVE_MOVE_DISTANCE
): ):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
@ -358,16 +394,16 @@ class GroundConflictGenerator:
if offset_heading < 0: if offset_heading < 0:
offset_heading = 358 offset_heading = 358
attack_point = self.find_offensive_point( attack_point = self.find_offensive_point(
dcs_group, dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
offset_heading,
AGGRESIVE_MOVE_DISTANCE
) )
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
elif stance == CombatStance.BREAKTHROUGH: elif stance == CombatStance.BREAKTHROUGH:
# In breakthrough mode, the units will move forward # In breakthrough mode, the units will move forward
# If the enemy base is close enough, the units will attack the base # If the enemy base is close enough, the units will attack the base
if to_cp.position.distance_to_point( if (
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE: to_cp.position.distance_to_point(dcs_group.points[0].position)
<= BREAKTHROUGH_OFFENSIVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
) )
@ -377,27 +413,27 @@ class GroundConflictGenerator:
offset_heading = forward_heading - 1 offset_heading = forward_heading - 1
if offset_heading < 0: if offset_heading < 0:
offset_heading = 359 offset_heading = 359
attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) attack_point = self.find_offensive_point(
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
elif stance == CombatStance.ELIMINATION: elif stance == CombatStance.ELIMINATION:
# In elimination mode, the units focus on destroying as much enemy groups as possible # In elimination mode, the units focus on destroying as much enemy groups as possible
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3) targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
for i, target in enumerate(targets, start=1): for i, target in enumerate(targets, start=1):
rand_offset = Point( rand_offset = Point(
random.randint( random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
),
random.randint(
-RANDOM_OFFSET_ATTACK,
RANDOM_OFFSET_ATTACK
)
) )
target_point = self.conflict.theater.nearest_land_pos( target_point = self.conflict.theater.nearest_land_pos(
target.points[0].position+rand_offset target.points[0].position + rand_offset
) )
dcs_group.add_waypoint(target_point, PointAction.OffRoad) dcs_group.add_waypoint(target_point, PointAction.OffRoad)
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id)) dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if (
to_cp.position.distance_to_point(dcs_group.points[0].position)
<= AGGRESIVE_MOVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos( attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0) to_cp.position.random_point_within(500, 0)
) )
@ -420,12 +456,23 @@ class GroundConflictGenerator:
Returns True if tasking was added, returns False if the stance was not a combat stance. Returns True if tasking was added, returns False if the stance was not a combat stance.
""" """
self._set_reform_waypoint(dcs_group, forward_heading) self._set_reform_waypoint(dcs_group, forward_heading)
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]: if stance in [
CombatStance.AGGRESSIVE,
CombatStance.BREAKTHROUGH,
CombatStance.ELIMINATION,
]:
# APC & ATGM will never move too much forward, but will follow along any offensive # APC & ATGM will never move too much forward, but will follow along any offensive
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: if (
attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0)) to_cp.position.distance_to_point(dcs_group.points[0].position)
<= AGGRESIVE_MOVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
else: else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE) attack_point = self.find_offensive_point(
dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE
)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad) dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
if stance != CombatStance.RETREAT: if stance != CombatStance.RETREAT:
@ -434,29 +481,36 @@ class GroundConflictGenerator:
return False return False
def plan_action_for_groups( def plan_action_for_groups(
self, stance: CombatStance, self,
stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]], ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int, forward_heading: int,
from_cp: ControlPoint, from_cp: ControlPoint,
to_cp: ControlPoint to_cp: ControlPoint,
) -> None: ) -> None:
if not self.game.settings.perf_moving_units: if not self.game.settings.perf_moving_units:
return return
for dcs_group, group in ally_groups: for dcs_group, group in ally_groups:
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs: if hasattr(group.units[0], "eplrs") and group.units[0].eplrs:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id)) dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
if self.game.settings.perf_artillery: if self.game.settings.perf_artillery:
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups) target = self.get_artillery_target_in_range(
dcs_group, group, enemy_groups
)
if target is not None: if target is not None:
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target) self._plan_artillery_action(
stance, group, dcs_group, forward_heading, target
)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]: elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp) self._plan_tank_ifv_action(
stance, enemy_groups, dcs_group, forward_heading, to_cp
)
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]: elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp) self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
@ -464,11 +518,16 @@ class GroundConflictGenerator:
if stance == CombatStance.RETREAT: if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back # In retreat mode, the units will fall back
# If the ally base is close enough, the units will even regroup there # If the ally base is close enough, the units will even regroup there
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE: if (
from_cp.position.distance_to_point(dcs_group.points[0].position)
<= RETREAT_DISTANCE
):
retreat_point = from_cp.position.random_point_within(500, 250) retreat_point = from_cp.position.random_point_within(500, 250)
else: else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading) retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy reposition_point = retreat_point.point_from_heading(
forward_heading, 10
) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad) dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad) dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
@ -490,8 +549,10 @@ class GroundConflictGenerator:
# We add a new retreat waypoint # We add a new retreat waypoint
dcs_group.add_waypoint( dcs_group.add_waypoint(
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), self.find_retreat_point(
PointAction.OffRoad dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
),
PointAction.OffRoad,
) )
# Fallback task # Fallback task
@ -515,7 +576,7 @@ class GroundConflictGenerator:
self, self,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
frontline_heading: int, frontline_heading: int,
distance: int = RETREAT_DISTANCE distance: int = RETREAT_DISTANCE,
) -> Point: ) -> Point:
""" """
Find a point to retreat to Find a point to retreat to
@ -523,17 +584,15 @@ class GroundConflictGenerator:
:param frontline_heading: Heading of the frontline :param frontline_heading: Heading of the frontline
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance) desired_point = dcs_group.points[0].position.point_from_heading(
heading_sum(frontline_heading, +180), distance
)
if self.conflict.theater.is_on_land(desired_point): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
return self.conflict.theater.nearest_land_pos(desired_point) return self.conflict.theater.nearest_land_pos(desired_point)
def find_offensive_point( def find_offensive_point(
self, self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int
) -> Point: ) -> Point:
""" """
Find a point to attack Find a point to attack
@ -542,7 +601,9 @@ class GroundConflictGenerator:
:param distance: Distance of the offensive (how far unit should move) :param distance: Distance of the offensive (how far unit should move)
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance) desired_point = dcs_group.points[0].position.point_from_heading(
frontline_heading, distance
)
if self.conflict.theater.is_on_land(desired_point): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
return self.conflict.theater.nearest_land_pos(desired_point) return self.conflict.theater.nearest_land_pos(desired_point)
@ -551,7 +612,7 @@ class GroundConflictGenerator:
def find_n_nearest_enemy_groups( def find_n_nearest_enemy_groups(
player_group: VehicleGroup, player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
n: int n: int,
) -> List[VehicleGroup]: ) -> List[VehicleGroup]:
""" """
Return the nearest enemy group for the player group Return the nearest enemy group for the player group
@ -562,7 +623,9 @@ class GroundConflictGenerator:
targets = [] # type: List[Optional[VehicleGroup]] targets = [] # type: List[Optional[VehicleGroup]]
sorted_list = sorted( sorted_list = sorted(
enemy_groups, enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position) key=lambda group: player_group.points[0].position.distance_to_point(
group[0].points[0].position
),
) )
for i in range(n): for i in range(n):
# TODO: Is this supposed to return no groups if enemy_groups is less than n? # TODO: Is this supposed to return no groups if enemy_groups is less than n?
@ -574,8 +637,7 @@ class GroundConflictGenerator:
@staticmethod @staticmethod
def find_nearest_enemy_group( def find_nearest_enemy_group(
player_group: VehicleGroup, player_group: VehicleGroup, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
) -> Optional[VehicleGroup]: ) -> Optional[VehicleGroup]:
""" """
Search the enemy groups for a potential target suitable to armored assault Search the enemy groups for a potential target suitable to armored assault
@ -585,7 +647,9 @@ class GroundConflictGenerator:
min_distance = 99999999 min_distance = 99999999
target = None target = None
for dcs_group, _ in enemy_groups: for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position) dist = player_group.points[0].position.distance_to_point(
dcs_group.points[0].position
)
if dist < min_distance: if dist < min_distance:
min_distance = dist min_distance = dist
target = dcs_group target = dcs_group
@ -595,7 +659,7 @@ class GroundConflictGenerator:
def get_artillery_target_in_range( def get_artillery_target_in_range(
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
group: CombatGroup, group: CombatGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]] enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
) -> Optional[Point]: ) -> Optional[Point]:
""" """
Search the enemy groups for a potential target suitable to an artillery unit Search the enemy groups for a potential target suitable to an artillery unit
@ -606,7 +670,9 @@ class GroundConflictGenerator:
return None return None
for _ in range(10): for _ in range(10):
potential_target = random.choice(enemy_groups)[0] potential_target = random.choice(enemy_groups)[0]
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position) distance_to_target = dcs_group.points[0].position.distance_to_point(
potential_target.points[0].position
)
if distance_to_target < rng: if distance_to_target < rng:
return potential_target.points[0].position return potential_target.points[0].position
return None return None
@ -620,12 +686,12 @@ class GroundConflictGenerator:
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint( rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0], DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1] DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1],
) )
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint( rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0], DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1] DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1],
) )
return rg return rg
@ -635,42 +701,46 @@ class GroundConflictGenerator:
combat_width: int, combat_width: int,
distance_from_frontline: int, distance_from_frontline: int,
heading: int, heading: int,
spawn_heading: int spawn_heading: int,
): ):
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) shifted = conflict_position.point_from_heading(
desired_point = shifted.point_from_heading( heading, random.randint(0, combat_width)
spawn_heading, )
distance_from_frontline desired_point = shifted.point_from_heading(
spawn_heading, distance_from_frontline
)
return Conflict.find_ground_position(
desired_point, combat_width, heading, self.conflict.theater
) )
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
def _generate_groups( def _generate_groups(
self, self,
groups: List[CombatGroup], groups: List[CombatGroup],
frontline_vector: Tuple[Point, int, int], frontline_vector: Tuple[Point, int, int],
is_player: bool is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]: ) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them""" """Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = [] positioned_groups = []
position, heading, combat_width = frontline_vector position, heading, combat_width = frontline_vector
spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90)) spawn_heading = (
int(heading_sum(heading, -90))
if is_player
else int(heading_sum(heading, 90))
)
country = self.game.player_country if is_player else self.game.enemy_country country = self.game.player_country if is_player else self.game.enemy_country
for group in groups: for group in groups:
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group) distance_from_frontline = (
self.get_artilery_group_distance_from_frontline(group)
)
else: else:
distance_from_frontline = random.randint( distance_from_frontline = random.randint(
DISTANCE_FROM_FRONTLINE[group.role][0], DISTANCE_FROM_FRONTLINE[group.role][0],
DISTANCE_FROM_FRONTLINE[group.role][1] DISTANCE_FROM_FRONTLINE[group.role][1],
) )
final_position = self.get_valid_position_for_group( final_position = self.get_valid_position_for_group(
position, position, combat_width, distance_from_frontline, heading, spawn_heading
combat_width,
distance_from_frontline,
heading,
spawn_heading
) )
if final_position is not None: if final_position is not None:
@ -693,7 +763,7 @@ class GroundConflictGenerator:
g, g,
is_player, is_player,
self.mission.country(country), self.mission.country(country),
opposite_heading(spawn_heading) opposite_heading(spawn_heading),
) )
else: else:
logging.warning(f"Unable to get valid position for {group}") logging.warning(f"Unable to get valid position for {group}")
@ -718,12 +788,14 @@ class GroundConflictGenerator:
logging.info("armorgen: {} for {}".format(unit, side.id)) logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
side, side,
namegen.next_unit_name(side, cp.id, unit), unit, namegen.next_unit_name(side, cp.id, unit),
position=at, unit,
group_size=count, position=at,
heading=heading, group_size=count,
move_formation=move_formation) heading=heading,
move_formation=move_formation,
)
self.unit_map.add_front_line_units(group, cp) self.unit_map.add_front_line_units(group, cp)

View File

@ -96,7 +96,8 @@ class Package:
if tot is None: if tot is None:
logging.error( logging.error(
f"{flight} requested escort at {waypoint} but that " f"{flight} requested escort at {waypoint} but that "
"waypoint has no TOT. It may not be escorted.") "waypoint has no TOT. It may not be escorted."
)
continue continue
times.append(tot) times.append(tot)
if times: if times:
@ -117,7 +118,8 @@ class Package:
logging.error( logging.error(
f"{flight} dismissed escort at {waypoint} but that " f"{flight} dismissed escort at {waypoint} but that "
"waypoint has no TOT or departure time. It may not be " "waypoint has no TOT or departure time. It may not be "
"escorted.") "escorted."
)
continue continue
times.append(tot) times.append(tot)
if times: if times:

View File

@ -23,9 +23,11 @@ from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@dataclass @dataclass
class CommInfo: class CommInfo:
"""Communications information for the kneeboard.""" """Communications information for the kneeboard."""
name: str name: str
freq: RadioFrequency freq: RadioFrequency
@ -37,10 +39,13 @@ class FrontLineInfo:
self.enemy_base: ControlPoint = front_line.control_point_b self.enemy_base: ControlPoint = front_line.control_point_b
self.player_zero: bool = self.player_base.base.total_armor == 0 self.player_zero: bool = self.player_base.base.total_armor == 0
self.enemy_zero: bool = self.enemy_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.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.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
self.combat_stances = CombatStance self.combat_stances = CombatStance
class MissionInfoGenerator: class MissionInfoGenerator:
"""Base type for generators of mission information for the player. """Base type for generators of mission information for the player.
@ -131,7 +136,6 @@ def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
class BriefingGenerator(MissionInfoGenerator): class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, game: Game): def __init__(self, mission: Mission, game: Game):
super().__init__(mission, game) super().__init__(mission, game)
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {} self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
@ -141,36 +145,36 @@ class BriefingGenerator(MissionInfoGenerator):
disabled_extensions=("",), disabled_extensions=("",),
default_for_string=True, default_for_string=True,
default=True, default=True,
), ),
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
) )
env.filters["waypoint_timing"] = format_waypoint_time env.filters["waypoint_timing"] = format_waypoint_time
self.template = env.get_template("briefingtemplate_EN.j2") self.template = env.get_template("briefingtemplate_EN.j2")
def generate(self) -> None: def generate(self) -> None:
"""Generate the mission briefing """Generate the mission briefing"""
"""
self._generate_frontline_info() self._generate_frontline_info()
self.generate_allied_flights_by_departure() self.generate_allied_flights_by_departure()
self.mission.set_description_text(self.template.render(vars(self))) self.mission.set_description_text(self.template.render(vars(self)))
self.mission.add_picture_blue(os.path.abspath( self.mission.add_picture_blue(
"./resources/ui/splash_screen.png")) os.path.abspath("./resources/ui/splash_screen.png")
)
def _generate_frontline_info(self) -> None: def _generate_frontline_info(self) -> None:
"""Build FrontLineInfo objects from FrontLine type and append to briefing. """Build FrontLineInfo objects from FrontLine type and append to briefing."""
"""
for front_line in self.game.theater.conflicts(from_player=True): for front_line in self.game.theater.conflicts(from_player=True):
self.add_frontline(FrontLineInfo(front_line)) self.add_frontline(FrontLineInfo(front_line))
# TODO: This should determine if runway is friendly through a method more robust than the existing string match # 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: def generate_allied_flights_by_departure(self) -> None:
"""Create iterable to display allied flights grouped by departure airfield. """Create iterable to display allied flights grouped by departure airfield."""
"""
for flight in self.flights: for flight in self.flights:
if not flight.client_units and flight.friendly: if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name name = flight.departure.airfield_name
if name in self.allied_flights_by_departure: # where else can we get this? if (
name in self.allied_flights_by_departure
): # where else can we get this?
self.allied_flights_by_departure[name].append(flight) self.allied_flights_by_departure[name].append(flight)
else: else:
self.allied_flights_by_departure[name] = [flight] self.allied_flights_by_departure[name] = [flight]

View File

@ -23,5 +23,9 @@ def generate_coastal_group(game, ground_object, faction_name: str):
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists") logging.info(
return None "Unable to generate missile group, generator : "
+ str(gen)
+ "does not exists"
)
return None

View File

@ -4,7 +4,6 @@ from gen.sam.group_generator import GroupGenerator
class SilkwormGenerator(GroupGenerator): class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction): def __init__(self, game, ground_object, faction):
super(SilkwormGenerator, self).__init__(game, ground_object) super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
@ -13,20 +12,47 @@ class SilkwormGenerator(GroupGenerator):
positions = self.get_circular_position(5, launcher_distance=120, coverage=180) positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
self.add_unit(MissilesSS.Silkworm_Radar, "SR#0", self.position.x, self.position.y, self.heading) self.add_unit(
MissilesSS.Silkworm_Radar,
"SR#0",
self.position.x,
self.position.y,
self.heading,
)
# Launchers # Launchers
for i, p in enumerate(positions): for i, p in enumerate(positions):
self.add_unit(MissilesSS.SS_N_2_Silkworm, "Missile#" + str(i), p[0], p[1], self.heading) self.add_unit(
MissilesSS.SS_N_2_Silkworm,
"Missile#" + str(i),
p[0],
p[1],
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Transport_KAMAZ_43101, "KAMAZ#0", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.Transport_KAMAZ_43101,
"KAMAZ#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad # Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38, self.add_unit(
self.heading) AirDefence.SPAAA_ZSU_23_4_Shilka,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
# Shorad 2 # Shorad 2
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0", self.add_unit(
self.position.x + 200, self.position.y + 15, 90) AirDefence.SAM_SA_9_Strela_1_9P31,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@ -12,19 +12,21 @@ from game.utils import heading_sum, opposite_heading
FRONTLINE_LENGTH = 80000 FRONTLINE_LENGTH = 80000
class Conflict: class Conflict:
def __init__(self, def __init__(
theater: ConflictTheater, self,
from_cp: ControlPoint, theater: ConflictTheater,
to_cp: ControlPoint, from_cp: ControlPoint,
attackers_side: str, to_cp: ControlPoint,
defenders_side: str, attackers_side: str,
attackers_country: Country, defenders_side: str,
defenders_country: Country, attackers_country: Country,
position: Point, defenders_country: Country,
heading: Optional[int] = None, position: Point,
size: Optional[int] = None heading: Optional[int] = None,
): size: Optional[int] = None,
):
self.attackers_side = attackers_side self.attackers_side = attackers_side
self.defenders_side = defenders_side self.defenders_side = defenders_side
@ -43,27 +45,49 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline return from_cp.has_frontline and to_cp.has_frontline
@classmethod @classmethod
def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]: def frontline_position(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp, theater) frontline = FrontLine(from_cp, to_cp, theater)
attack_heading = frontline.attack_heading attack_heading = frontline.attack_heading
position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater) position = cls.find_ground_position(
frontline.position,
FRONTLINE_LENGTH,
heading_sum(attack_heading, 90),
theater,
)
return position, opposite_heading(attack_heading) return position, opposite_heading(attack_heading)
@classmethod @classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: def frontline_vector(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int, int]:
""" """
Returns a vector for a valid frontline location avoiding exclusion zones. Returns a vector for a valid frontline location avoiding exclusion zones.
""" """
center_position, heading = cls.frontline_position(from_cp, to_cp, theater) center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
left_heading = heading_sum(heading, -90) left_heading = heading_sum(heading, -90)
right_heading = heading_sum(heading, 90) right_heading = heading_sum(heading, 90)
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater) left_position = cls.extend_ground_position(
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater) center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater
)
right_position = cls.extend_ground_position(
center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater
)
distance = int(left_position.distance_to_point(right_position)) distance = int(left_position.distance_to_point(right_position))
return left_position, right_heading, distance return left_position, right_heading, distance
@classmethod @classmethod
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): def frontline_cas_conflict(
cls,
attacker_name: str,
defender_name: str,
attacker: Country,
defender: Country,
from_cp: ControlPoint,
to_cp: ControlPoint,
theater: ConflictTheater,
):
assert cls.has_frontline_between(from_cp, to_cp) assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls( conflict = cls(
@ -76,12 +100,14 @@ class Conflict:
defenders_side=defender_name, defenders_side=defender_name,
attackers_country=attacker, attackers_country=attacker,
defenders_country=defender, defenders_country=defender,
size=distance size=distance,
) )
return conflict return conflict
@classmethod @classmethod
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: def extend_ground_position(
cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater
) -> Point:
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
extended = initial.point_from_heading(heading, max_distance) extended = initial.point_from_heading(heading, max_distance)
if theater.landmap is None: if theater.landmap is None:
@ -92,8 +118,7 @@ class Conflict:
p1 = ShapelyPoint(extended.x, extended.y) p1 = ShapelyPoint(extended.x, extended.y)
line = LineString([p0, p1]) line = LineString([p0, p1])
intersection = line.intersection( intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary)
theater.landmap.inclusion_zone_only.boundary)
if intersection.is_empty: if intersection.is_empty:
# Max extent does not intersect with the boundary of the inclusion # Max extent does not intersect with the boundary of the inclusion
# zone, so the full front line is usable. This does assume that the # zone, so the full front line is usable. This does assume that the
@ -104,7 +129,14 @@ class Conflict:
return initial.point_from_heading(heading, p0.distance(intersection)) return initial.point_from_heading(heading, p0.distance(intersection))
@classmethod @classmethod
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]: def find_ground_position(
cls,
initial: Point,
max_distance: int,
heading: int,
theater: ConflictTheater,
coerce=True,
) -> Optional[Point]:
""" """
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance. Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
`coerce=True` will return the closest land position to `initial` regardless of heading or distance `coerce=True` will return the closest land position to `initial` regardless of heading or distance
@ -123,4 +155,3 @@ class Conflict:
return pos return pos
logging.error("Didn't find ground position ({})!".format(initial)) logging.error("Didn't find ground position ({})!".format(initial))
return None return None

View File

@ -3,15 +3,20 @@ import random
from dcs.vehicles import Armor from dcs.vehicles import Armor
from game import db from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator from gen.defenses.armored_group_generator import (
ArmoredGroupGenerator,
FixedSizeArmorGroupGenerator,
)
def generate_armor_group(faction:str, game, ground_object): def generate_armor_group(faction: str, game, ground_object):
""" """
This generate a group of ground units This generate a group of ground units
:return: Generated group :return: Generated group
""" """
possible_unit = [u for u in db.FACTIONS[faction].frontline_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: if len(possible_unit) > 0:
unit_type = random.choice(possible_unit) unit_type = random.choice(possible_unit)
return generate_armor_group_of_type(game, ground_object, unit_type) return generate_armor_group_of_type(game, ground_object, unit_type)
@ -36,4 +41,3 @@ def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size:
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size) generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()

View File

@ -4,7 +4,6 @@ from gen.sam.group_generator import GroupGenerator
class ArmoredGroupGenerator(GroupGenerator): class ArmoredGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type): def __init__(self, game, ground_object, unit_type):
super(ArmoredGroupGenerator, self).__init__(game, ground_object) super(ArmoredGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
@ -20,13 +19,16 @@ class ArmoredGroupGenerator(GroupGenerator):
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index + 1 index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index), self.add_unit(
self.position.x + spacing * i, self.unit_type,
self.position.y + spacing * j, self.heading) "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
class FixedSizeArmorGroupGenerator(GroupGenerator): class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size): def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object) super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type self.unit_type = unit_type
@ -38,7 +40,10 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
index = 0 index = 0
for i in range(self.size): for i in range(self.size):
index = index + 1 index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index), self.add_unit(
self.position.x + spacing * i, self.unit_type,
self.position.y, self.heading) "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y,
self.heading,
)

View File

@ -2,54 +2,122 @@ import random
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.ships import ( from dcs.ships import USS_Arleigh_Burke_IIa, Ticonderoga_class
USS_Arleigh_Burke_IIa,
Ticonderoga_class
)
class CarrierGroupGenerator(ShipGroupGenerator): class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
#Carrier Strike Group 8 # Carrier Strike Group 8
if self.faction.carrier_names[0] == "Carrier Strike Group 8": if self.faction.carrier_names[0] == "Carrier Strike Group 8":
carrier_type = random.choice(self.faction.aircraft_carrier) carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "CVN-75 Harry S. Truman", self.position.x, self.position.y, self.heading) self.add_unit(
carrier_type,
"CVN-75 Harry S. Truman",
self.position.x,
self.position.y,
self.heading,
)
# Add Arleigh Burke escort # Add Arleigh Burke escort
self.add_unit(USS_Arleigh_Burke_IIa, "USS Ramage", self.position.x + 6482, self.position.y + 6667, self.heading) self.add_unit(
USS_Arleigh_Burke_IIa,
"USS Ramage",
self.position.x + 6482,
self.position.y + 6667,
self.heading,
)
self.add_unit(USS_Arleigh_Burke_IIa, "USS Mitscher", self.position.x - 7963, self.position.y + 7037, self.heading) self.add_unit(
USS_Arleigh_Burke_IIa,
"USS Mitscher",
self.position.x - 7963,
self.position.y + 7037,
self.heading,
)
self.add_unit(USS_Arleigh_Burke_IIa, "USS Forrest Sherman", self.position.x - 7408, self.position.y - 7408, self.heading) self.add_unit(
USS_Arleigh_Burke_IIa,
"USS Forrest Sherman",
self.position.x - 7408,
self.position.y - 7408,
self.heading,
)
self.add_unit(USS_Arleigh_Burke_IIa, "USS Lassen", self.position.x + 8704, self.position.y - 6296, self.heading) self.add_unit(
USS_Arleigh_Burke_IIa,
"USS Lassen",
self.position.x + 8704,
self.position.y - 6296,
self.heading,
)
# Add Ticonderoga escort # Add Ticonderoga escort
if self.heading >= 180: if self.heading >= 180:
self.add_unit(Ticonderoga_class, "USS Hué City", self.position.x + 2222, self.position.y - 3333, self.heading) self.add_unit(
Ticonderoga_class,
"USS Hué City",
self.position.x + 2222,
self.position.y - 3333,
self.heading,
)
else: else:
self.add_unit(Ticonderoga_class, "USS Hué City", self.position.x - 3333, self.position.y + 2222, self.heading) self.add_unit(
Ticonderoga_class,
"USS Hué City",
self.position.x - 3333,
self.position.y + 2222,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
################################################################################################## ##################################################################################################
# Add carrier for normal generation # Add carrier for normal generation
else: else:
if len(self.faction.aircraft_carrier) > 0: if len(self.faction.aircraft_carrier) > 0:
carrier_type = random.choice(self.faction.aircraft_carrier) carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading) self.add_unit(
carrier_type,
"Carrier",
self.position.x,
self.position.y,
self.heading,
)
else: else:
return return
# Add destroyers escort # Add destroyers escort
if len(self.faction.destroyers) > 0: if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers) 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(
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading) 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(
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading) 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 self.get_generated_group().points[0].speed = 20

View File

@ -20,7 +20,6 @@ if TYPE_CHECKING:
class ChineseNavyGroupGenerator(ShipGroupGenerator): class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
@ -30,17 +29,45 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(Type_054A_Frigate, "FF2", self.position.x + 1200, self.position.y - 900, self.heading) Type_054A_Frigate,
"FF1",
self.position.x + 1200,
self.position.y + 900,
self.heading,
)
self.add_unit(
Type_054A_Frigate,
"FF2",
self.position.x + 1200,
self.position.y - 900,
self.heading,
)
if include_dd: if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer]) dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) dd_type,
"DD1",
self.position.x + 2400,
self.position.y + 900,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 2400,
self.position.y - 900,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class Type54GroupGenerator(DDGroupGenerator): class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A_Frigate
)

View File

@ -13,22 +13,47 @@ if TYPE_CHECKING:
class DDGroupGenerator(ShipGroupGenerator): class DDGroupGenerator(ShipGroupGenerator):
def __init__(
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType): self,
game: Game,
ground_object: TheaterGroundObject,
faction: Faction,
ddtype: ShipType,
):
super(DDGroupGenerator, self).__init__(game, ground_object, faction) super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype self.ddtype = ddtype
def generate(self): def generate(self):
self.add_unit(self.ddtype, "DD1", self.position.x + 500, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(self.ddtype, "DD2", self.position.x + 500, self.position.y - 900, self.heading) self.ddtype,
"DD1",
self.position.x + 500,
self.position.y + 900,
self.heading,
)
self.add_unit(
self.ddtype,
"DD2",
self.position.x + 500,
self.position.y - 900,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class OliverHazardPerryGroupGenerator(DDGroupGenerator): class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, Oliver_Hazzard_Perry_class
)
class ArleighBurkeGroupGenerator(DDGroupGenerator): class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, USS_Arleigh_Burke_IIa
)

View File

@ -4,18 +4,31 @@ from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator): class LHAGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Add carrier # Add carrier
if len(self.faction.helicopter_carrier) > 0: if len(self.faction.helicopter_carrier) > 0:
carrier_type = random.choice(self.faction.helicopter_carrier) carrier_type = random.choice(self.faction.helicopter_carrier)
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading) self.add_unit(
carrier_type, "LHA", self.position.x, self.position.y, self.heading
)
# Add destroyers escort # Add destroyers escort
if len(self.faction.destroyers) > 0: if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers) 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(
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading) 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 self.get_generated_group().points[0].speed = 20

View File

@ -9,7 +9,7 @@ from dcs.ships import (
FF_1135M_Rezky, FF_1135M_Rezky,
CG_1164_Moskva, CG_1164_Moskva,
SSK_877, SSK_877,
SSK_641B SSK_641B,
) )
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
@ -23,7 +23,6 @@ if TYPE_CHECKING:
class RussianNavyGroupGenerator(ShipGroupGenerator): class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
include_frigate = random.choice([True, True, False]) include_frigate = random.choice([True, True, False])
@ -39,37 +38,79 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
if include_frigate: if include_frigate:
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya]) frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading) frigate_type,
"FF1",
self.position.x + 1200,
self.position.y + 900,
self.heading,
)
self.add_unit(
frigate_type,
"FF2",
self.position.x + 1200,
self.position.y - 900,
self.heading,
)
if include_dd: if include_dd:
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky]) dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading) self.add_unit(
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading) dd_type,
"DD1",
self.position.x + 2400,
self.position.y + 900,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 2400,
self.position.y - 900,
self.heading,
)
if include_cc: if include_cc:
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster. # Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# See https://github.com/Khopa/dcs_liberation/issues/567 # See https://github.com/Khopa/dcs_liberation/issues/567
self.add_unit(CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading) self.add_unit(
CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
class GrishaGroupGenerator(DDGroupGenerator): class GrishaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, FFL_1124_4_Grisha
)
class MolniyaGroupGenerator(DDGroupGenerator): class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, FSG_1241_1MP_Molniya
)
class KiloSubGroupGenerator(DDGroupGenerator): class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877
)
class TangoSubGroupGenerator(DDGroupGenerator): class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B) self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(TangoSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_641B
)

View File

@ -6,10 +6,15 @@ from gen.sam.group_generator import ShipGroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator): class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
for i in range(random.randint(2, 4)): 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.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 self.get_generated_group().points[0].speed = 20

View File

@ -4,10 +4,18 @@ import random
from game import db from game import db
from gen.fleet.carrier_group import CarrierGroupGenerator from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import ArleighBurkeGroupGenerator, OliverHazardPerryGroupGenerator from gen.fleet.dd_group import (
ArleighBurkeGroupGenerator,
OliverHazardPerryGroupGenerator,
)
from gen.fleet.lha_group import LHAGroupGenerator from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \ from gen.fleet.ru_dd_group import (
KiloSubGroupGenerator, TangoSubGroupGenerator RussianNavyGroupGenerator,
GrishaGroupGenerator,
MolniyaGroupGenerator,
KiloSubGroupGenerator,
TangoSubGroupGenerator,
)
from gen.fleet.schnellboot import SchnellbootGroupGenerator from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator from gen.fleet.ww2lst import WW2LSTGroupGenerator
@ -25,7 +33,7 @@ SHIP_MAP = {
"MolniyaGroupGenerator": MolniyaGroupGenerator, "MolniyaGroupGenerator": MolniyaGroupGenerator,
"KiloSubGroupGenerator": KiloSubGroupGenerator, "KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator, "TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator "Type54GroupGenerator": Type54GroupGenerator,
} }
@ -43,7 +51,11 @@ def generate_ship_group(game, ground_object, faction_name: str):
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists") logging.info(
"Unable to generate ship group, generator : "
+ str(gen)
+ "does not exists"
)
return None return None

View File

@ -6,10 +6,15 @@ from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator): class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
self.add_unit(Uboat_VIIC_U_flak, "Uboat" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading) self.add_unit(
Uboat_VIIC_U_flak,
"Uboat" + 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 self.get_generated_group().points[0].speed = 20

View File

@ -6,13 +6,24 @@ from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator): class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Add LS Samuel Chase # Add LS Samuel Chase
self.add_unit(LS_Samuel_Chase, "SamuelChase", self.position.x, self.position.y, self.heading) self.add_unit(
LS_Samuel_Chase,
"SamuelChase",
self.position.x,
self.position.y,
self.heading,
)
for i in range(1, random.randint(3, 4)): for i in range(1, random.randint(3, 4)):
self.add_unit(LST_Mk_II, "LST" + str(i), self.position.x + i * random.randint(800, 1200), self.position.y, self.heading) self.add_unit(
LST_Mk_II,
"LST" + str(i),
self.position.x + i * random.randint(800, 1200),
self.position.y,
self.heading,
)
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@ -109,22 +109,25 @@ class ProposedMission:
flights: List[ProposedFlight] flights: List[ProposedFlight]
def __str__(self) -> str: def __str__(self) -> str:
flights = ', '.join([str(f) for f in self.flights]) flights = ", ".join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}" return f"{self.location.name}: {flights}"
class AircraftAllocator: class AircraftAllocator:
"""Finds suitable aircraft for proposed missions.""" """Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields, def __init__(
global_inventory: GlobalAircraftInventory, self,
is_player: bool) -> None: closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
) -> None:
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.is_player = is_player self.is_player = is_player
def find_aircraft_for_flight( def find_aircraft_for_flight(
self, flight: ProposedFlight self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
"""Finds aircraft suitable for the given mission. """Finds aircraft suitable for the given mission.
@ -144,12 +147,12 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory. responsible for returning them to the inventory.
""" """
return self.find_aircraft_of_type( return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
flight, aircraft_for_task(flight.task)
)
def find_aircraft_of_type( def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]], self,
flight: ProposedFlight,
types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within( airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance flight.max_distance
@ -171,18 +174,22 @@ class AircraftAllocator:
class PackageBuilder: class PackageBuilder:
"""Builds a Package for the flights it receives.""" """Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget, def __init__(
closest_airfields: ClosestAirfields, self,
global_inventory: GlobalAircraftInventory, location: MissionTarget,
is_player: bool, closest_airfields: ClosestAirfields,
package_country: str, global_inventory: GlobalAircraftInventory,
start_type: str) -> None: is_player: bool,
package_country: str,
start_type: str,
) -> None:
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.is_player = is_player self.is_player = is_player
self.package_country = package_country self.package_country = package_country
self.package = Package(location) self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory, self.allocator = AircraftAllocator(
is_player) closest_airfields, global_inventory, is_player
)
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.start_type = start_type self.start_type = start_type
@ -203,14 +210,23 @@ class PackageBuilder:
else: else:
start_type = self.start_type start_type = self.start_type
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task, flight = Flight(
start_type, departure=airfield, arrival=airfield, self.package,
divert=self.find_divert_field(aircraft, airfield)) self.package_country,
aircraft,
plan.num_aircraft,
plan.task,
start_type,
departure=airfield,
arrival=airfield,
divert=self.find_divert_field(aircraft, airfield),
)
self.package.add_flight(flight) self.package.add_flight(flight)
return True return True
def find_divert_field(self, aircraft: Type[FlyingType], def find_divert_field(
arrival: ControlPoint) -> Optional[ControlPoint]: self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.airfields_within(divert_limit): for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player: if airfield.captured != self.is_player:
@ -323,8 +339,8 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships()) return self._targets_by_range(self.enemy_ships())
def _targets_by_range( def _targets_by_range(
self, self, targets: Iterable[MissionTarget]
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]: ) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = [] target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets: for target in targets:
ranges: List[int] = [] ranges: List[int] = []
@ -430,8 +446,9 @@ class ObjectiveFinder:
def friendly_control_points(self) -> Iterator[ControlPoint]: def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points.""" """Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if return (
c.is_friendly(self.is_player)) c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
)
def farthest_friendly_control_point(self) -> ControlPoint: def farthest_friendly_control_point(self) -> ControlPoint:
""" """
@ -451,8 +468,11 @@ class ObjectiveFinder:
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if return (
not c.is_friendly(self.is_player)) c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
)
def all_possible_targets(self) -> Iterator[MissionTarget]: def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater. """Iterates over all possible mission targets in the theater.
@ -524,33 +544,46 @@ class CoalitionMissionPlanner:
eliminated this turn. eliminated this turn.
""" """
#Find farthest, friendly CP for AEWC # Find farthest, friendly CP for AEWC
cp = self.objective_finder.farthest_friendly_control_point() cp = self.objective_finder.farthest_friendly_control_point()
yield ProposedMission(cp, [ yield ProposedMission(
ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE) cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
]) )
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing # Plan three rounds of CAP to give ~90 minutes coverage. Spacing
# these out appropriately is done in stagger_missions. # these out appropriately is done in stagger_missions.
yield ProposedMission(cp, [ yield ProposedMission(
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), cp,
]) [
yield ProposedMission(cp, [ ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), ],
]) )
yield ProposedMission(cp, [ yield ProposedMission(
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), cp,
]) [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
# Find front lines, plan CAS. # Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines(): for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [ yield ProposedMission(
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), front_line,
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE, [
EscortType.AirToAir), ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
]) ProposedFlight(
FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir
),
],
)
def propose_missions(self) -> Iterator[ProposedMission]: def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order.""" """Identifies and iterates over potential mission in priority order."""
@ -561,30 +594,46 @@ class CoalitionMissionPlanner:
# Find enemy SAM sites with ranges that extend to within 50 nmi of # Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD. # friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams(): for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [ yield ProposedMission(
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), sam,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
]) ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
),
],
)
for group in self.objective_finder.threatening_ships(): for group in self.objective_finder.threatening_ships():
yield ProposedMission(group, [ yield ProposedMission(
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE), group,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE, ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
]) ProposedFlight(
FlightType.ESCORT,
2,
self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir,
),
],
)
for group in self.objective_finder.threatening_vehicle_groups(): for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(group, [ yield ProposedMission(
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE), group,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE, ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, ProposedFlight(
EscortType.Sead), FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
]) ),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
],
)
for target in self.objective_finder.oca_targets(min_aircraft=20): for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = [ flights = [
@ -593,27 +642,37 @@ class CoalitionMissionPlanner:
if self.game.settings.default_start_type == "Cold": if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player # Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit. # has set anything else there are no targets to hit.
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2, flights.append(
self.MAX_OCA_RANGE)) ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
flights.extend([ )
# TODO: Max escort range. flights.extend(
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE, [
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE, ProposedFlight(
EscortType.Sead), FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
]) ),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
]
)
yield ProposedMission(target, flights) yield ProposedMission(target, flights)
# Plan strike missions. # Plan strike missions.
for target in self.objective_finder.strike_targets(): for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [ yield ProposedMission(
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), target,
# TODO: Max escort range. [
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
EscortType.AirToAir), # TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, ProposedFlight(
EscortType.Sead), FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
]) ),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
),
],
)
def plan_missions(self) -> None: def plan_missions(self) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
@ -628,19 +687,23 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points(): for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft: for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft", self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight, def plan_flight(
builder: PackageBuilder, missing_types: Set[FlightType], self,
for_reserves: bool) -> None: mission: ProposedMission,
flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType],
for_reserves: bool,
) -> None:
if not builder.plan_flight(flight): if not builder.plan_flight(flight):
missing_types.add(flight.task) missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest( purchase_order = AircraftProcurementRequest(
near=mission.location, near=mission.location,
range=flight.max_distance, range=flight.max_distance,
task_capability=flight.task, task_capability=flight.task,
number=flight.num_aircraft number=flight.num_aircraft,
) )
if for_reserves: if for_reserves:
# Reserves are planned for critical missions, so prioritize # Reserves are planned for critical missions, so prioritize
@ -650,26 +713,28 @@ class CoalitionMissionPlanner:
self.procurement_requests.append(purchase_order) self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft( def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder, self,
missing_types: Set[FlightType], mission: ProposedMission,
not_attempted: Iterable[ProposedFlight], builder: PackageBuilder,
reserves: bool) -> None: missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool,
) -> None:
# Try to plan the rest of the mission just so we can count the missing # Try to plan the rest of the mission just so we can count the missing
# types to buy. # types to buy.
for flight in not_attempted: for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types, reserves) self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join( missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
sorted([t.name for t in missing_types]))
builder.release_planned_aircraft() builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft" desc = "reserve aircraft" if reserves else "aircraft"
self.message( self.message(
"Insufficient aircraft", "Insufficient aircraft",
f"Not enough {desc} in range for {mission.location.name} " f"Not enough {desc} in range for {mission.location.name} "
f"capable of: {missing_types_str}") f"capable of: {missing_types_str}",
)
def check_needed_escorts( def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool) threats = defaultdict(bool)
for flight in builder.package.flights: for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight): if self.threat_zones.threatened_by_aircraft(flight):
@ -678,8 +743,7 @@ class CoalitionMissionPlanner:
threats[EscortType.Sead] = True threats[EscortType.Sead] = True
return threats return threats
def plan_mission(self, mission: ProposedMission, def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
reserves: bool = False) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player: if self.is_player:
@ -693,7 +757,7 @@ class CoalitionMissionPlanner:
self.game.aircraft_inventory, self.game.aircraft_inventory,
self.is_player, self.is_player,
package_country, package_country,
self.game.settings.default_start_type self.game.settings.default_start_type,
) )
# Attempt to plan all the main elements of the mission first. Escorts # Attempt to plan all the main elements of the mission first. Escorts
@ -707,12 +771,12 @@ class CoalitionMissionPlanner:
# If the package does not need escorts they may be pruned. # If the package does not need escorts they may be pruned.
escorts.append(proposed_flight) escorts.append(proposed_flight)
continue continue
self.plan_flight(mission, proposed_flight, builder, missing_types, self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
reserves)
if missing_types: if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types, self.scrub_mission_missing_aircraft(
escorts, reserves) mission, builder, missing_types, escorts, reserves
)
return return
# Create flight plans for the main flights of the package so we can # Create flight plans for the main flights of the package so we can
@ -721,8 +785,9 @@ class CoalitionMissionPlanner:
# flights that will rendezvous with their package will be affected by # flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to # the other flights in the package. Escorts will not be able to
# contribute to this. # contribute to this.
flight_plan_builder = FlightPlanBuilder(self.game, builder.package, flight_plan_builder = FlightPlanBuilder(
self.is_player) self.game, builder.package, self.is_player
)
for flight in builder.package.flights: for flight in builder.package.flights:
flight_plan_builder.populate_flight_plan(flight) flight_plan_builder.populate_flight_plan(flight)
@ -732,14 +797,14 @@ class CoalitionMissionPlanner:
# impossible. # impossible.
assert escort.escort_type is not None assert escort.escort_type is not None
if needed_escorts[escort.escort_type]: if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder, missing_types, self.plan_flight(mission, escort, builder, missing_types, reserves)
reserves)
# Check again for unavailable aircraft. If the escort was required and # Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission. # none were found, scrub the mission.
if missing_types: if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types, self.scrub_mission_missing_aircraft(
escorts, reserves) mission, builder, missing_types, escorts, reserves
)
return return
if reserves: if reserves:
@ -756,8 +821,9 @@ class CoalitionMissionPlanner:
self.ato.add_package(package) self.ato.add_package(package)
def stagger_missions(self) -> None: def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int, def start_time_generator(
margin: int) -> Iterator[timedelta]: count: int, earliest: int, latest: int, margin: int
) -> Iterator[timedelta]:
interval = (latest - earliest) // count interval = (latest - earliest) // count
for time in range(earliest, latest, interval): for time in range(earliest, latest, interval):
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
@ -768,17 +834,13 @@ class CoalitionMissionPlanner:
FlightType.TARCAP, FlightType.TARCAP,
} }
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict( previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
timedelta non_dca_packages = [
) p for p in self.ato.packages if p.primary_task not in dca_types
non_dca_packages = [p for p in self.ato.packages if ]
p.primary_task not in dca_types]
start_time = start_time_generator( start_time = start_time_generator(
count=len(non_dca_packages), count=len(non_dca_packages), earliest=5, latest=90, margin=5
earliest=5,
latest=90,
margin=5
) )
for package in self.ato.packages: for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot() tot = TotEstimator(package).earliest_tot()
@ -795,8 +857,7 @@ class CoalitionMissionPlanner:
departure_time = package.mission_departure_time departure_time = package.mission_departure_time
# Should be impossible for CAPs # Should be impossible for CAPs
if departure_time is None: if departure_time is None:
logging.error( logging.error(f"Could not determine mission end time for {package}")
f"Could not determine mission end time for {package}")
continue continue
previous_cap_end_time[package.target] = departure_time previous_cap_end_time[package.target] = departure_time
else: else:
@ -815,8 +876,6 @@ class CoalitionMissionPlanner:
message to the info panel. message to the info panel.
""" """
if self.is_player: if self.is_player:
self.game.informations.append( self.game.informations.append(Information(title, text, self.game.turn))
Information(title, text, self.game.turn)
)
else: else:
logging.info(f"{title}: {text}") logging.info(f"{title}: {text}")

View File

@ -370,11 +370,7 @@ TRANSPORT_CAPABLE = [
UH_1H, UH_1H,
] ]
DRONES = [ DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
MQ_9_Reaper,
RQ_1A_Predator,
WingLoong_I
]
AEWC_CAPABLE = [ AEWC_CAPABLE = [
E_3A, E_3A,

View File

@ -12,8 +12,9 @@ if TYPE_CHECKING:
class ClosestAirfields: class ClosestAirfields:
"""Precalculates which control points are closes to the given target.""" """Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget, def __init__(
all_control_points: List[ControlPoint]) -> None: self, target: MissionTarget, all_control_points: List[ControlPoint]
) -> None:
self.target = target self.target = target
# This cache is configured once on load, so it's important that it is # This cache is configured once on load, so it's important that it is
# complete and deterministic to avoid different behaviors across loads. # complete and deterministic to avoid different behaviors across loads.
@ -52,9 +53,7 @@ class ObjectiveDistanceCache:
@classmethod @classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields: def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None: if cls.theater is None:
raise RuntimeError( raise RuntimeError("Call ObjectiveDistanceCache.set_theater before using")
"Call ObjectiveDistanceCache.set_theater before using"
)
if location.name not in cls.closest_airfields: if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields( cls.closest_airfields[location.name] = ClosestAirfields(

View File

@ -28,6 +28,7 @@ class FlightType(Enum):
each flight and thus a part of the ATO, so changing these values will break each flight and thus a part of the ATO, so changing these values will break
save compat. save compat.
""" """
TARCAP = "TARCAP" TARCAP = "TARCAP"
BARCAP = "BARCAP" BARCAP = "BARCAP"
CAS = "CAS" CAS = "CAS"
@ -48,22 +49,22 @@ class FlightType(Enum):
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points) INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points) INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task) INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour) CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16 JOIN = 16
SPLIT = 17 SPLIT = 17
LOITER = 18 LOITER = 18
@ -77,9 +78,13 @@ class FlightWaypointType(Enum):
class FlightWaypoint: class FlightWaypoint:
def __init__(
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float, self,
alt: Distance = meters(0)) -> None: waypoint_type: FlightWaypointType,
x: float,
y: float,
alt: Distance = meters(0),
) -> None:
"""Creates a flight waypoint. """Creates a flight waypoint.
Args: Args:
@ -117,10 +122,13 @@ class FlightWaypoint:
return Point(self.x, self.y) return Point(self.x, self.y)
@classmethod @classmethod
def from_pydcs(cls, point: MovingPoint, def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
from_cp: ControlPoint) -> "FlightWaypoint": waypoint = FlightWaypoint(
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x, FlightWaypointType.NAV,
point.position.y, meters(point.alt)) point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first # Other actions exist... but none of them *should* be the first
# waypoint for a flight. # waypoint for a flight.
@ -144,12 +152,19 @@ class FlightWaypoint:
class Flight: class Flight:
def __init__(
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType], self,
count: int, flight_type: FlightType, start_type: str, package: Package,
departure: ControlPoint, arrival: ControlPoint, country: str,
divert: Optional[ControlPoint], unit_type: Type[FlyingType],
custom_name: Optional[str] = None) -> None: count: int,
flight_type: FlightType,
start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None,
) -> None:
self.package = package self.package = package
self.country = country self.country = country
self.unit_type = unit_type self.unit_type = unit_type
@ -170,10 +185,9 @@ class Flight:
# FlightPlanBuilder, but an empty flight plan the flight begins with an # FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan. # empty flight plan.
from gen.flights.flightplan import CustomFlightPlan from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan( self.flight_plan: FlightPlan = CustomFlightPlan(
package=package, package=package, flight=self, custom_waypoints=[]
flight=self,
custom_waypoints=[]
) )
@property @property
@ -191,7 +205,7 @@ class Flight:
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {name}"
def __str__(self): def __str__(self):
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name') name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
if self.custom_name: if self.custom_name:
return f"{self.custom_name} {self.count} x {name}" return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {name}" return f"[{self.flight_type}] {self.count} x {name}"

View File

@ -15,12 +15,7 @@ from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.planes import ( from dcs.planes import E_3A, E_2C, A_50, KJ_2000
E_3A,
E_2C,
A_50,
KJ_2000
)
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
@ -82,7 +77,7 @@ class FlightPlan:
raise NotImplementedError raise NotImplementedError
def edges( def edges(
self, until: Optional[FlightWaypoint] = None self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]: ) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order.""" """A list of all paths between waypoints, in order."""
waypoints = self.waypoints waypoints = self.waypoints
@ -93,8 +88,9 @@ class FlightPlan:
return zip(self.waypoints[:last_index], self.waypoints[1:last_index]) return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
def best_speed_between_waypoints(self, a: FlightWaypoint, def best_speed_between_waypoints(
b: FlightWaypoint) -> Speed: self, a: FlightWaypoint, b: FlightWaypoint
) -> Speed:
"""Desired ground speed between points a and b.""" """Desired ground speed between points a and b."""
factor = 1.0 factor = 1.0
if b.waypoint_type == FlightWaypointType.ASCEND_POINT: if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
@ -115,8 +111,7 @@ class FlightPlan:
# near 2000 ft MSL. # near 2000 ft MSL.
return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor
def speed_between_waypoints(self, a: FlightWaypoint, def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
b: FlightWaypoint) -> Speed:
return self.best_speed_between_waypoints(a, b) return self.best_speed_between_waypoints(a, b)
@property @property
@ -131,8 +126,7 @@ class FlightPlan:
@cached_property @cached_property
def bingo_fuel(self) -> int: def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan """Bingo fuel value for the FlightPlan"""
"""
distance_to_arrival = self.max_distance_from(self.flight.arrival) distance_to_arrival = self.max_distance_from(self.flight.arrival)
bingo = 1000.0 # Minimum Emergency Fuel bingo = 1000.0 # Minimum Emergency Fuel
@ -149,8 +143,7 @@ class FlightPlan:
@cached_property @cached_property
def joker_fuel(self) -> int: def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan """Joker fuel value for the FlightPlan"""
"""
return self.bingo_fuel + 1000 return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance: def max_distance_from(self, cp: ControlPoint) -> Distance:
@ -159,8 +152,9 @@ class FlightPlan:
""" """
if not self.waypoints: if not self.waypoints:
return meters(0) return meters(0)
return max([meters(cp.position.distance_to_point(w.position)) for w in return max(
self.waypoints]) [meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
@property @property
def tot_offset(self) -> timedelta: def tot_offset(self) -> timedelta:
@ -171,30 +165,30 @@ class FlightPlan:
""" """
return timedelta() return timedelta()
def _travel_time_to_waypoint( def _travel_time_to_waypoint(self, destination: FlightWaypoint) -> timedelta:
self, destination: FlightWaypoint) -> timedelta:
total = timedelta() total = timedelta()
if destination not in self.waypoints: if destination not in self.waypoints:
raise PlanningError( raise PlanningError(
f"Did not find destination waypoint {destination} in " f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}") f"waypoints for {self.flight}"
)
for previous_waypoint, waypoint in self.edges(until=destination): for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint, total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
waypoint)
return total return total
def travel_time_between_waypoints(self, a: FlightWaypoint, def travel_time_between_waypoints(
b: FlightWaypoint) -> timedelta: self, a: FlightWaypoint, b: FlightWaypoint
return TravelTime.between_points(a.position, b.position, ) -> timedelta:
self.speed_between_waypoints(a, b)) return TravelTime.between_points(
a.position, b.position, self.speed_between_waypoints(a, b)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError raise NotImplementedError
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError raise NotImplementedError
def request_escort_at(self) -> Optional[FlightWaypoint]: def request_escort_at(self) -> Optional[FlightWaypoint]:
@ -219,8 +213,7 @@ class FlightPlan:
if takeoff_time is None: if takeoff_time is None:
return None return None
start_time = (takeoff_time - self.estimate_startup() - start_time = takeoff_time - self.estimate_startup() - self.estimate_ground_ops()
self.estimate_ground_ops())
# In case FP math has given us some barely below zero time, round to # In case FP math has given us some barely below zero time, round to
# zero. # zero.
@ -276,14 +269,14 @@ class LoiterFlightPlan(FlightPlan):
def push_time(self) -> timedelta: def push_time(self) -> timedelta:
raise NotImplementedError raise NotImplementedError
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold: if waypoint == self.hold:
return self.push_time return self.push_time
return None return None
def travel_time_between_waypoints(self, a: FlightWaypoint, def travel_time_between_waypoints(
b: FlightWaypoint) -> timedelta: self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b) travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold: if a != self.hold:
return travel_time return travel_time
@ -328,12 +321,12 @@ class FormationFlightPlan(LoiterFlightPlan):
speeds = [] speeds = []
for previous_waypoint, waypoint in self.edges(): for previous_waypoint, waypoint in self.edges():
if waypoint in self.package_speed_waypoints: if waypoint in self.package_speed_waypoints:
speeds.append(self.best_speed_between_waypoints( speeds.append(
previous_waypoint, waypoint)) self.best_speed_between_waypoints(previous_waypoint, waypoint)
)
return min(speeds) return min(speeds)
def speed_between_waypoints(self, a: FlightWaypoint, def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
b: FlightWaypoint) -> Speed:
if b in self.package_speed_waypoints: if b in self.package_speed_waypoints:
# Should be impossible, as any package with at least one # Should be impossible, as any package with at least one
# FormationFlightPlan flight needs a formation speed. # FormationFlightPlan flight needs a formation speed.
@ -366,7 +359,7 @@ class FormationFlightPlan(LoiterFlightPlan):
return self.join_time - TravelTime.between_points( return self.join_time - TravelTime.between_points(
self.hold.position, self.hold.position,
self.join.position, self.join.position,
GroundSpeed.for_flight(self.flight, self.hold.alt) GroundSpeed.for_flight(self.flight, self.hold.alt),
) )
@property @property
@ -406,8 +399,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_start_time return self.patrol_start_time
return None return None
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end: if waypoint == self.patrol_end:
return self.patrol_end_time return self.patrol_end_time
return None return None
@ -497,8 +489,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
def tot_offset(self) -> timedelta: def tot_offset(self) -> timedelta:
return -self.lead_time return -self.lead_time
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end: if waypoint == self.patrol_end:
return self.patrol_end_time return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint) return super().depart_time_for_waypoint(waypoint)
@ -549,13 +540,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> Set[FlightWaypoint]:
return { return {
self.ingress, self.ingress,
self.egress, self.egress,
self.split, self.split,
} | set(self.targets) } | set(self.targets)
def speed_between_waypoints(self, a: FlightWaypoint, def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
b: FlightWaypoint) -> Speed:
# FlightWaypoint is only comparable by identity, so adding # FlightWaypoint is only comparable by identity, so adding
# target_area_waypoint to package_speed_waypoints is useless. # target_area_waypoint to package_speed_waypoints is useless.
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC: if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
@ -571,10 +561,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def target_area_waypoint(self) -> FlightWaypoint: def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC, return FlightWaypoint(
self.package.target.position.x, FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.y, self.package.target.position.x,
meters(0)) self.package.target.position.y,
meters(0),
)
@property @property
def travel_time_to_target(self) -> timedelta: def travel_time_to_target(self) -> timedelta:
@ -588,14 +580,15 @@ class StrikeFlightPlan(FormationFlightPlan):
# package we need to use the travel time to the same position as # package we need to use the travel time to the same position as
# the others. # the others.
total += self.travel_time_between_waypoints( total += self.travel_time_between_waypoints(
previous_waypoint, self.target_area_waypoint) previous_waypoint, self.target_area_waypoint
)
break break
total += self.travel_time_between_waypoints(previous_waypoint, total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
waypoint)
else: else:
raise PlanningError( raise PlanningError(
f"Did not find destination waypoint {destination} in " f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}") f"waypoints for {self.flight}"
)
return total return total
@property @property
@ -604,28 +597,28 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def join_time(self) -> timedelta: def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
self.join, self.ingress)
return self.ingress_time - travel_time return self.ingress_time - travel_time
@property @property
def split_time(self) -> timedelta: def split_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(self.egress, self.split)
self.egress, self.split)
return self.egress_time + travel_time return self.egress_time + travel_time
@property @property
def ingress_time(self) -> timedelta: def ingress_time(self) -> timedelta:
tot = self.package.time_over_target tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint) self.ingress, self.target_area_waypoint
)
return tot - travel_time return tot - travel_time
@property @property
def egress_time(self) -> timedelta: def egress_time(self) -> timedelta:
tot = self.package.time_over_target tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.target_area_waypoint, self.egress) self.target_area_waypoint, self.egress
)
return tot + travel_time return tot + travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
@ -671,7 +664,8 @@ class SweepFlightPlan(LoiterFlightPlan):
@property @property
def sweep_start_time(self) -> timedelta: def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end) self.sweep_start, self.sweep_end
)
return self.sweep_end_time - travel_time return self.sweep_end_time - travel_time
@property @property
@ -685,8 +679,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time return self.sweep_end_time
return None return None
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold: if waypoint == self.hold:
return self.push_time return self.push_time
return None return None
@ -696,7 +689,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time - TravelTime.between_points( return self.sweep_end_time - TravelTime.between_points(
self.hold.position, self.hold.position,
self.sweep_end.position, self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt) GroundSpeed.for_flight(self.flight, self.hold.alt),
) )
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
@ -763,8 +756,7 @@ class CustomFlightPlan(FlightPlan):
return self.package.time_over_target return self.package.time_over_target
return None return None
def depart_time_for_waypoint( def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None return None
@property @property
@ -794,9 +786,11 @@ class FlightPlanBuilder:
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(not self.is_player)
def populate_flight_plan( def populate_flight_plan(
self, flight: Flight, self,
# TODO: Custom targets should be an attribute of the flight. flight: Flight,
custom_targets: Optional[List[Unit]] = None) -> None: # 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.""" """Creates a default flight plan for the given mission."""
if flight not in self.package.flights: if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package") raise RuntimeError("Flight must be a part of the package")
@ -805,8 +799,8 @@ class FlightPlanBuilder:
flight.flight_plan = self.generate_flight_plan(flight, custom_targets) flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
def generate_flight_plan( def generate_flight_plan(
self, flight: Flight, self, flight: Flight, custom_targets: Optional[List[Unit]]
custom_targets: Optional[List[Unit]]) -> FlightPlan: ) -> FlightPlan:
# TODO: Flesh out mission types. # TODO: Flesh out mission types.
task = flight.flight_type task = flight.flight_type
if task == FlightType.ANTISHIP: if task == FlightType.ANTISHIP:
@ -835,8 +829,7 @@ class FlightPlanBuilder:
return self.generate_tarcap(flight) return self.generate_tarcap(flight)
elif task == FlightType.AEWC: elif task == FlightType.AEWC:
return self.generate_aewc(flight) return self.generate_aewc(flight)
raise PlanningError( raise PlanningError(f"{task} flight plan generation not implemented")
f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
# The simple case is where the target is greater than the ingress # The simple case is where the target is greater than the ingress
@ -873,6 +866,7 @@ class FlightPlanBuilder:
# | | | | # | | | |
# +--------------+ +---------------+ # +--------------+ +---------------+
from gen.ato import PackageWaypoints from gen.ato import PackageWaypoints
target = self.package.target.position target = self.package.target.position
join_point = self.preferred_join_point() join_point = self.preferred_join_point()
@ -906,10 +900,9 @@ class FlightPlanBuilder:
def legacy_package_waypoints_impl(self) -> None: def legacy_package_waypoints_impl(self) -> None:
from gen.ato import PackageWaypoints from gen.ato import PackageWaypoints
ingress_point = self._ingress_point(
self._target_heading_to_package_airfield()) ingress_point = self._ingress_point(self._target_heading_to_package_airfield())
egress_point = self._egress_point( egress_point = self._egress_point(self._target_heading_to_package_airfield())
self._target_heading_to_package_airfield())
join_point = self._rendezvous_point(ingress_point) join_point = self._rendezvous_point(ingress_point)
split_point = self._rendezvous_point(egress_point) split_point = self._rendezvous_point(egress_point)
self.package.waypoints = PackageWaypoints( self.package.waypoints = PackageWaypoints(
@ -921,7 +914,8 @@ class FlightPlanBuilder:
def preferred_join_point(self) -> Optional[Point]: def preferred_join_point(self) -> Optional[Point]:
path = self.game.navmesh_for(self.is_player).shortest_path( path = self.game.navmesh_for(self.is_player).shortest_path(
self.package_airfield().position, self.package.target.position) self.package_airfield().position, self.package.target.position
)
for point in reversed(path): for point in reversed(path):
if not self.threat_zones.threatened(point): if not self.threat_zones.threatened(point):
return point return point
@ -961,16 +955,16 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(building.category, building)) targets.append(StrikeTarget(building.category, building))
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_STRIKE, flight, location, FlightWaypointType.INGRESS_STRIKE, targets
targets) )
def generate_aewc(self, flight: Flight) -> AwacsFlightPlan: def generate_aewc(self, flight: Flight) -> AwacsFlightPlan:
"""Generate a AWACS flight at a given location. """Generate a AWACS flight at a given location.
Args: Args:
flight: The flight to generate the flight plan for. flight: The flight to generate the flight plan for.
""" """
location = self.package.target location = self.package.target
start = self.aewc_orbit(location) start = self.aewc_orbit(location)
@ -994,10 +988,12 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position, nav_to=builder.nav_path(
patrol_alt), flight.departure.position, start.position, patrol_alt
nav_from=builder.nav_path(start.position, flight.arrival.position, ),
patrol_alt), nav_from=builder.nav_path(
start.position, flight.arrival.position, patrol_alt
),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
hold=start, hold=start,
@ -1017,11 +1013,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = [] targets: List[StrikeTarget] = []
for group in location.groups: for group in location.groups:
targets.append( targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_BAI, targets) flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan: def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan. """Generates an anti-ship flight plan.
@ -1043,11 +1039,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = [] targets: List[StrikeTarget] = []
for group in location.groups: for group in location.groups:
targets.append( targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_BAI, targets) flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan: def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
"""Generate a BARCAP flight at a given location. """Generate a BARCAP flight at a given location.
@ -1061,10 +1057,12 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location, barcap=True) start, end = self.racetrack_for_objective(location, barcap=True)
patrol_alt = meters(random.randint( patrol_alt = meters(
int(self.doctrine.min_patrol_altitude.meters), random.randint(
int(self.doctrine.max_patrol_altitude.meters) int(self.doctrine.min_patrol_altitude.meters),
)) int(self.doctrine.max_patrol_altitude.meters),
)
)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start, end, patrol_alt) start, end = builder.race_track(start, end, patrol_alt)
@ -1075,14 +1073,16 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range, engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position, nav_to=builder.nav_path(
patrol_alt), flight.departure.position, start.position, patrol_alt
nav_from=builder.nav_path(end.position, flight.arrival.position, ),
patrol_alt), nav_from=builder.nav_path(
end.position, flight.arrival.position, patrol_alt
),
patrol_start=start, patrol_start=start,
patrol_end=end, patrol_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
def generate_sweep(self, flight: Flight) -> SweepFlightPlan: def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@ -1095,12 +1095,10 @@ class FlightPlanBuilder:
target = self.package.target.position target = self.package.target.position
heading = self.package.waypoints.join.heading_between_point(target) heading = self.package.waypoints.join.heading_between_point(target)
start = target.point_from_heading(heading, start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
-self.doctrine.sweep_distance.meters)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start, target, start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight)) hold = builder.hold(self._hold_point(flight))
@ -1111,18 +1109,21 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=hold, hold=hold,
hold_duration=timedelta(minutes=5), hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, start.position, nav_to=builder.nav_path(
self.doctrine.ingress_altitude), hold.position, start.position, self.doctrine.ingress_altitude
nav_from=builder.nav_path(end.position, flight.arrival.position, ),
self.doctrine.ingress_altitude), nav_from=builder.nav_path(
end.position, flight.arrival.position, self.doctrine.ingress_altitude
),
sweep_start=start, sweep_start=start,
sweep_end=end, sweep_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
def racetrack_for_objective(self, location: MissionTarget, def racetrack_for_objective(
barcap: bool) -> Tuple[Point, Point]: self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.operational_airfields: for airfield in closest_cache.operational_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next* # If the mission is a BARCAP of an enemy airfield, find the *next*
@ -1135,20 +1136,21 @@ class FlightPlanBuilder:
else: else:
raise PlanningError("Could not find any enemy airfields") raise PlanningError("Could not find any enemy airfields")
heading = location.position.heading_between_point( heading = location.position.heading_between_point(closest_airfield.position)
closest_airfield.position
)
position = ShapelyPoint(self.package.target.position.x, position = ShapelyPoint(
self.package.target.position.y) self.package.target.position.x, self.package.target.position.y
)
if barcap: if barcap:
# BARCAPs should remain far enough back from the enemy that their # BARCAPs should remain far enough back from the enemy that their
# commit range does not enter the enemy's threat zone. Include a 5nm # commit range does not enter the enemy's threat zone. Include a 5nm
# buffer. # buffer.
distance_to_no_fly = meters( distance_to_no_fly = (
position.distance(self.threat_zones.all) meters(position.distance(self.threat_zones.all))
) - self.doctrine.cap_engagement_range - nautical_miles(5) - self.doctrine.cap_engagement_range
- nautical_miles(5)
)
else: else:
# Other race tracks (TARCAPs, currently) just try to keep some # Other race tracks (TARCAPs, currently) just try to keep some
# distance from the nearest enemy airbase, but since they are by # distance from the nearest enemy airbase, but since they are by
@ -1158,22 +1160,24 @@ class FlightPlanBuilder:
distance_to_airfield = meters( distance_to_airfield = meters(
closest_airfield.position.distance_to_point( closest_airfield.position.distance_to_point(
self.package.target.position self.package.target.position
)) )
)
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, min_cap_distance = min(
distance_to_no_fly) 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) max_cap_distance = min(
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
)
end = location.position.point_from_heading( end = location.position.point_from_heading(
heading, heading,
random.randint(int(min_cap_distance.meters), random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
int(max_cap_distance.meters))
) )
diameter = random.randint( diameter = random.randint(
int(self.doctrine.cap_min_track_length.meters), int(self.doctrine.cap_min_track_length.meters),
int(self.doctrine.cap_max_track_length.meters) int(self.doctrine.cap_max_track_length.meters),
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
return start, end return start, end
@ -1185,13 +1189,11 @@ class FlightPlanBuilder:
# Place this either over the target or as close as possible outside the # Place this either over the target or as close as possible outside the
# threat zone: https://github.com/Khopa/dcs_liberation/issues/842. # threat zone: https://github.com/Khopa/dcs_liberation/issues/842.
heading = location.position.heading_between_point(closest_airfield.position) heading = location.position.heading_between_point(closest_airfield.position)
return location.position.point_from_heading( return location.position.point_from_heading(heading, 5000)
heading,
5000
)
def racetrack_for_frontline(self, origin: Point, def racetrack_for_frontline(
front_line: FrontLine) -> Tuple[Point, Point]: self, origin: Point, front_line: FrontLine
) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points ally_cp, enemy_cp = front_line.control_points
# Find targets waypoints # Find targets waypoints
@ -1200,8 +1202,10 @@ class FlightPlanBuilder:
) )
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading( orbit_center = center.point_from_heading(
heading - 90, random.randint(int(nautical_miles(6).meters), heading - 90,
int(nautical_miles(15).meters)) random.randint(
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
),
) )
combat_width = distance / 2 combat_width = distance / 2
@ -1227,18 +1231,21 @@ class FlightPlanBuilder:
location = self.package.target location = self.package.target
patrol_alt = meters( patrol_alt = meters(
random.randint(int(self.doctrine.min_patrol_altitude.meters), random.randint(
int(self.doctrine.max_patrol_altitude.meters))) int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
# Create points # Create points
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline( orbit0p, orbit1p = self.racetrack_for_frontline(
flight.departure.position, location) flight.departure.position, location
)
else: else:
orbit0p, orbit1p = self.racetrack_for_objective(location, orbit0p, orbit1p = self.racetrack_for_objective(location, barcap=False)
barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return TarCapFlightPlan( return TarCapFlightPlan(
@ -1252,18 +1259,17 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range, engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p, nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
patrol_alt), nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
patrol_alt),
patrol_start=start, patrol_start=start,
patrol_end=end, patrol_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
def generate_dead(self, flight: Flight, def generate_dead(
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan: self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a DEAD flight at a given location. """Generate a DEAD flight at a given location.
Args: Args:
@ -1276,7 +1282,8 @@ class FlightPlanBuilder:
is_sam = isinstance(location, SamGroundObject) is_sam = isinstance(location, SamGroundObject)
if not is_ewr and not is_sam: if not is_ewr and not is_sam:
logging.exception( logging.exception(
f"Invalid Objective Location for DEAD flight {flight=} at {location=}") f"Invalid Objective Location for DEAD flight {flight=} at {location=}"
)
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these. # TODO: Unify these.
@ -1288,8 +1295,9 @@ class FlightPlanBuilder:
for target in custom_targets: for target in custom_targets:
targets.append(StrikeTarget(location.name, target)) targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_DEAD, targets) flight, location, FlightWaypointType.INGRESS_DEAD, targets
)
def generate_oca_strike(self, flight: Flight) -> StrikeFlightPlan: def generate_oca_strike(self, flight: Flight) -> StrikeFlightPlan:
"""Generate an OCA Strike flight plan at a given location. """Generate an OCA Strike flight plan at a given location.
@ -1302,11 +1310,13 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield): if not isinstance(location, Airfield):
logging.exception( logging.exception(
f"Invalid Objective Location for OCA Strike flight " f"Invalid Objective Location for OCA Strike flight "
f"{flight=} at {location=}.") f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_OCA_AIRCRAFT) flight, location, FlightWaypointType.INGRESS_OCA_AIRCRAFT
)
def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan: def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a runway attack flight plan at a given location. """Generate a runway attack flight plan at a given location.
@ -1319,14 +1329,17 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield): if not isinstance(location, Airfield):
logging.exception( logging.exception(
f"Invalid Objective Location for runway bombing flight " f"Invalid Objective Location for runway bombing flight "
f"{flight=} at {location=}.") f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_OCA_RUNWAY) flight, location, FlightWaypointType.INGRESS_OCA_RUNWAY
)
def generate_sead(self, flight: Flight, def generate_sead(
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan: self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a SEAD flight at a given location. """Generate a SEAD flight at a given location.
Args: Args:
@ -1344,16 +1357,19 @@ class FlightPlanBuilder:
for target in custom_targets: for target in custom_targets:
targets.append(StrikeTarget(location.name, target)) targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, return self.strike_flightplan(
FlightWaypointType.INGRESS_SEAD, targets) flight, location, FlightWaypointType.INGRESS_SEAD, targets
)
def generate_escort(self, flight: Flight) -> StrikeFlightPlan: def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
ingress, target, egress = builder.escort( ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.target, self.package.waypoints.ingress,
self.package.waypoints.egress) self.package.target,
self.package.waypoints.egress,
)
hold = builder.hold(self._hold_point(flight)) hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join) join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split) split = builder.split(self.package.waypoints.split)
@ -1364,17 +1380,19 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=hold, hold=hold,
hold_duration=timedelta(minutes=5), hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position, nav_to=builder.nav_path(
self.doctrine.ingress_altitude), hold.position, join.position, self.doctrine.ingress_altitude
),
join=join, join=join,
ingress=ingress, ingress=ingress,
targets=[target], targets=[target],
egress=egress, egress=egress,
split=split, split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position, nav_from=builder.nav_path(
self.doctrine.ingress_altitude), split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
def generate_cas(self, flight: Flight) -> CasFlightPlan: def generate_cas(self, flight: Flight) -> CasFlightPlan:
@ -1389,8 +1407,7 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1], location.control_points[0], location.control_points[1], self.game.theater
self.game.theater
) )
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
@ -1407,22 +1424,26 @@ class FlightPlanBuilder:
flight=flight, flight=flight,
patrol_duration=self.doctrine.cas_duration, patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, ingress, nav_to=builder.nav_path(
self.doctrine.ingress_altitude), flight.departure.position, ingress, self.doctrine.ingress_altitude
nav_from=builder.nav_path(egress, flight.arrival.position, ),
self.doctrine.ingress_altitude), nav_from=builder.nav_path(
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS, egress, flight.arrival.position, self.doctrine.ingress_altitude
ingress, location), ),
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
),
engagement_distance=meters(FRONTLINE_LENGTH) / 2, engagement_distance=meters(FRONTLINE_LENGTH) / 2,
target=builder.cas(center), target=builder.cas(center),
patrol_end=builder.egress(egress, location), patrol_end=builder.egress(egress, location),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
@staticmethod @staticmethod
def target_waypoint(flight: Flight, builder: WaypointBuilder, def target_waypoint(
target: StrikeTarget) -> FlightWaypoint: flight: Flight, builder: WaypointBuilder, target: StrikeTarget
) -> FlightWaypoint:
if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}: if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
return builder.bai_group(target) return builder.bai_group(target)
elif flight.flight_type == FlightType.DEAD: elif flight.flight_type == FlightType.DEAD:
@ -1433,8 +1454,9 @@ class FlightPlanBuilder:
return builder.strike_point(target) return builder.strike_point(target)
@staticmethod @staticmethod
def target_area_waypoint(flight: Flight, location: MissionTarget, def target_area_waypoint(
builder: WaypointBuilder) -> FlightWaypoint: flight: Flight, location: MissionTarget, builder: WaypointBuilder
) -> FlightWaypoint:
if flight.flight_type == FlightType.DEAD: if flight.flight_type == FlightType.DEAD:
return builder.dead_area(location) return builder.dead_area(location)
elif flight.flight_type == FlightType.SEAD: elif flight.flight_type == FlightType.SEAD:
@ -1455,12 +1477,14 @@ class FlightPlanBuilder:
# If the origin airfield is closer to the target than the join # If the origin airfield is closer to the target than the join
# point, plan the hold point such that it retreats from the origin # point, plan the hold point such that it retreats from the origin
# airfield. # airfield.
return join.point_from_heading(target.heading_between_point(origin), return join.point_from_heading(
self.doctrine.push_distance.meters) target.heading_between_point(origin), self.doctrine.push_distance.meters
)
heading_to_join = origin.heading_between_point(join) heading_to_join = origin.heading_between_point(join)
hold_point = origin.point_from_heading( hold_point = origin.point_from_heading(
heading_to_join, self.doctrine.push_distance.meters) heading_to_join, self.doctrine.push_distance.meters
)
hold_distance = meters(hold_point.distance_to_point(join)) hold_distance = meters(hold_point.distance_to_point(join))
if hold_distance >= self.doctrine.push_distance: if hold_distance >= self.doctrine.push_distance:
# Hold point is between the origin airfield and the join point and # Hold point is between the origin airfield and the join point and
@ -1474,26 +1498,27 @@ class FlightPlanBuilder:
# properly. # properly.
origin_to_join = origin.distance_to_point(join) origin_to_join = origin.distance_to_point(join)
cos_theta = ( cos_theta = (
(self.doctrine.hold_distance.meters ** 2 + self.doctrine.hold_distance.meters ** 2
origin_to_join ** 2 - + origin_to_join ** 2
self.doctrine.join_distance.meters ** 2) / - self.doctrine.join_distance.meters ** 2
(2 * self.doctrine.hold_distance.meters * origin_to_join) ) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
)
try: try:
theta = math.acos(cos_theta) theta = math.acos(cos_theta)
except ValueError: except ValueError:
# No solution that maintains hold and join distances. Extend the # No solution that maintains hold and join distances. Extend the
# hold point away from the target. # hold point away from the target.
return origin.point_from_heading( return origin.point_from_heading(
target.heading_between_point(origin), target.heading_between_point(origin), self.doctrine.hold_distance.meters
self.doctrine.hold_distance.meters) )
return origin.point_from_heading(heading_to_join - theta, return origin.point_from_heading(
self.doctrine.hold_distance.meters) heading_to_join - theta, self.doctrine.hold_distance.meters
)
# TODO: Make a model for the waypoint builder and use that in the UI. # TODO: Make a model for the waypoint builder and use that in the UI.
def generate_rtb_waypoint(self, flight: Flight, def generate_rtb_waypoint(
arrival: ControlPoint) -> FlightWaypoint: self, flight: Flight, arrival: ControlPoint
) -> FlightWaypoint:
"""Generate RTB landing point. """Generate RTB landing point.
Args: Args:
@ -1504,20 +1529,23 @@ class FlightPlanBuilder:
return builder.land(arrival) return builder.land(arrival)
def strike_flightplan( def strike_flightplan(
self, flight: Flight, location: MissionTarget, self,
ingress_type: FlightWaypointType, flight: Flight,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan: location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None,
) -> StrikeFlightPlan:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player, targets) builder = WaypointBuilder(flight, self.game, self.is_player, targets)
target_waypoints: List[FlightWaypoint] = [] target_waypoints: List[FlightWaypoint] = []
if targets is not None: if targets is not None:
for target in targets: for target in targets:
target_waypoints.append( target_waypoints.append(self.target_waypoint(flight, builder, target))
self.target_waypoint(flight, builder, target))
else: else:
target_waypoints.append( target_waypoints.append(
self.target_area_waypoint(flight, location, builder)) self.target_area_waypoint(flight, location, builder)
)
hold = builder.hold(self._hold_point(flight)) hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join) join = builder.join(self.package.waypoints.join)
@ -1529,32 +1557,38 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
hold=hold, hold=hold,
hold_duration=timedelta(minutes=5), hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position, nav_to=builder.nav_path(
self.doctrine.ingress_altitude), hold.position, join.position, self.doctrine.ingress_altitude
),
join=join, join=join,
ingress=builder.ingress(ingress_type, ingress=builder.ingress(
self.package.waypoints.ingress, location), ingress_type, self.package.waypoints.ingress, location
),
targets=target_waypoints, targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location), egress=builder.egress(self.package.waypoints.egress, location),
split=split, split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position, nav_from=builder.nav_path(
self.doctrine.ingress_altitude), split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert) divert=builder.divert(flight.divert),
) )
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that retreats from the origin airfield.""" """Creates a rendezvous point that retreats from the origin airfield."""
return attack_transition.point_from_heading( return attack_transition.point_from_heading(
self.package.target.position.heading_between_point( self.package.target.position.heading_between_point(
self.package_airfield().position), self.package_airfield().position
self.doctrine.join_distance.meters) ),
self.doctrine.join_distance.meters,
)
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target.""" """Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition) heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading( return attack_transition.point_from_heading(
heading, -self.doctrine.join_distance.meters) heading, -self.doctrine.join_distance.meters
)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point( transition_target_distance = attack_transition.distance_to_point(
@ -1609,13 +1643,9 @@ class FlightPlanBuilder:
# The package airfield is either the flight's airfield (when there is no # The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the # package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package. # departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields( cache = ObjectiveDistanceCache.get_closest_airfields(self.package.target)
self.package.target
)
for airfield in cache.operational_airfields: for airfield in cache.operational_airfields:
for flight in self.package.flights: for flight in self.package.flights:
if flight.departure == airfield: if flight.departure == airfield:
return airfield return airfield
raise RuntimeError( raise RuntimeError("Could not find any airfield assigned to this package")
"Could not find any airfield assigned to this package"
)

View File

@ -23,7 +23,6 @@ if TYPE_CHECKING:
class GroundSpeed: class GroundSpeed:
@classmethod @classmethod
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed: def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
if not issubclass(flight.unit_type, FlyingType): if not issubclass(flight.unit_type, FlyingType):
@ -55,13 +54,11 @@ class TravelTime:
def between_points(a: Point, b: Point, speed: Speed) -> timedelta: def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
error_factor = 1.1 error_factor = 1.1
distance = meters(a.distance_to_point(b)) distance = meters(a.distance_to_point(b))
return timedelta( return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
hours=distance.nautical_miles / speed.knots * error_factor)
# TODO: Most if not all of this should move into FlightPlan. # TODO: Most if not all of this should move into FlightPlan.
class TotEstimator: class TotEstimator:
def __init__(self, package: Package) -> None: def __init__(self, package: Package) -> None:
self.package = package self.package = package
@ -75,9 +72,9 @@ class TotEstimator:
return startup_time return startup_time
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
earliest_tot = max(( earliest_tot = max(
self.earliest_tot_for_flight(f) for f in self.package.flights (self.earliest_tot_for_flight(f) for f in self.package.flights)
)) )
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks, # Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we # and they're not interesting from a mission planning perspective so we

View File

@ -36,8 +36,13 @@ class StrikeTarget:
class WaypointBuilder: class WaypointBuilder:
def __init__(self, flight: Flight, game: Game, player: bool, def __init__(
targets: Optional[List[StrikeTarget]] = None) -> None: self,
flight: Flight,
game: Game,
player: bool,
targets: Optional[List[StrikeTarget]] = None,
) -> None:
self.flight = flight self.flight = flight
self.conditions = game.conditions self.conditions = game.conditions
self.doctrine = game.faction_for(player).doctrine self.doctrine = game.faction_for(player).doctrine
@ -65,9 +70,7 @@ class WaypointBuilder:
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.alt_type = "BARO" waypoint.alt_type = "BARO"
@ -75,10 +78,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Enter theater" waypoint.pretty_name = "Enter theater"
else: else:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF, FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
position.x,
position.y,
meters(0)
) )
waypoint.name = "TAKEOFF" waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -98,9 +98,7 @@ class WaypointBuilder:
FlightWaypointType.NAV, FlightWaypointType.NAV,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.alt_type = "BARO" waypoint.alt_type = "BARO"
@ -108,10 +106,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater" waypoint.pretty_name = "Exit theater"
else: else:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
position.x,
position.y,
meters(0)
) )
waypoint.name = "LANDING" waypoint.name = "LANDING"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -119,8 +114,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Land" waypoint.pretty_name = "Land"
return waypoint return waypoint
def divert(self, def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier. """Create divert waypoint for the given arrival airfield or carrier.
Args: Args:
@ -141,10 +135,7 @@ class WaypointBuilder:
altitude_type = "RADIO" altitude_type = "RADIO"
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.DIVERT, FlightWaypointType.DIVERT, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.alt_type = altitude_type waypoint.alt_type = altitude_type
waypoint.name = "DIVERT" waypoint.name = "DIVERT"
@ -158,9 +149,7 @@ class WaypointBuilder:
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position.x, position.x,
position.y, position.y,
meters( meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
500
) if self.is_helo else self.doctrine.rendezvous_altitude
) )
waypoint.pretty_name = "Hold" waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time" waypoint.description = "Wait until push time"
@ -172,9 +161,7 @@ class WaypointBuilder:
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position.x, position.x,
position.y, position.y,
meters( meters(80) if self.is_helo else self.doctrine.ingress_altitude,
80
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo: if self.is_helo:
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -188,9 +175,7 @@ class WaypointBuilder:
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position.x, position.x,
position.y, position.y,
meters( meters(80) if self.is_helo else self.doctrine.ingress_altitude,
80
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo: if self.is_helo:
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -199,15 +184,17 @@ class WaypointBuilder:
waypoint.name = "SPLIT" waypoint.name = "SPLIT"
return waypoint return waypoint
def ingress(self, ingress_type: FlightWaypointType, position: Point, def ingress(
objective: MissionTarget) -> FlightWaypoint: self,
ingress_type: FlightWaypointType,
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
ingress_type, ingress_type,
position.x, position.x,
position.y, position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
50
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo: if self.is_helo:
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -223,9 +210,7 @@ class WaypointBuilder:
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position.x, position.x,
position.y, position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
50
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo: if self.is_helo:
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -252,7 +237,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_POINT, FlightWaypointType.TARGET_POINT,
target.target.position.x, target.target.position.x,
target.target.position.y, target.target.position.y,
meters(0) meters(0),
) )
waypoint.description = description waypoint.description = description
waypoint.pretty_name = description waypoint.pretty_name = description
@ -277,13 +262,14 @@ class WaypointBuilder:
return self._target_area(f"ATTACK {target.name}", target, flyover=True) return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@staticmethod @staticmethod
def _target_area(name: str, location: MissionTarget, def _target_area(
flyover: bool = False) -> FlightWaypoint: name: str, location: MissionTarget, flyover: bool = False
) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
location.position.x, location.position.x,
location.position.y, location.position.y,
meters(0) meters(0),
) )
waypoint.description = name waypoint.description = name
waypoint.pretty_name = name waypoint.pretty_name = name
@ -308,7 +294,7 @@ class WaypointBuilder:
FlightWaypointType.CAS, FlightWaypointType.CAS,
position.x, position.x,
position.y, position.y,
meters(50) if self.is_helo else meters(1000) meters(50) if self.is_helo else meters(1000),
) )
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS" waypoint.description = "Provide CAS"
@ -325,10 +311,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK, FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "RACETRACK START" waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point" waypoint.description = "Orbit between this point and the next point"
@ -344,18 +327,16 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.PATROL, FlightWaypointType.PATROL, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "RACETRACK END" waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point" waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end" waypoint.pretty_name = "Race-track end"
return waypoint return waypoint
def race_track(self, start: Point, end: Point, def race_track(
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit. """Creates two waypoint for a racetrack orbit.
Args: Args:
@ -363,8 +344,10 @@ class WaypointBuilder:
end: The ending racetrack waypoint. end: The ending racetrack waypoint.
altitude: The racetrack altitude. altitude: The racetrack altitude.
""" """
return (self.race_track_start(start, altitude), return (
self.race_track_end(end, altitude)) self.race_track_start(start, altitude),
self.race_track_end(end, altitude),
)
@staticmethod @staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint: def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
@ -375,12 +358,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack. altitude: Altitude of the racetrack.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
FlightWaypointType.LOITER,
start.x,
start.y,
altitude
)
waypoint.name = "ORBIT" waypoint.name = "ORBIT"
waypoint.description = "Anchor and hold at this point" waypoint.description = "Anchor and hold at this point"
waypoint.pretty_name = "Orbit" waypoint.pretty_name = "Orbit"
@ -395,10 +373,7 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP, FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "SWEEP START" waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft" waypoint.description = "Proceed to the target and engage enemy aircraft"
@ -414,18 +389,16 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters. altitude: Altitude of the sweep in meters.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "SWEEP END" waypoint.name = "SWEEP END"
waypoint.description = "End of sweep" waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end" waypoint.pretty_name = "Sweep end"
return waypoint return waypoint
def sweep(self, start: Point, end: Point, def sweep(
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]: self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit. """Creates two waypoint for a racetrack orbit.
Args: Args:
@ -433,11 +406,11 @@ class WaypointBuilder:
end: The end of the sweep. end: The end of the sweep.
altitude: The sweep altitude. altitude: The sweep altitude.
""" """
return (self.sweep_start(start, altitude), return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
self.sweep_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \ def escort(
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]: self, ingress: Point, target: MissionTarget, egress: Point
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package. """Creates the waypoints needed to escort the package.
Args: Args:
@ -451,16 +424,13 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give # description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target # the escort flights a flight plan including the ingress point, target
# area, and egress point. # area, and egress point.
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
target)
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position.x, target.position.x,
target.position.y, target.position.y,
meters( meters(50) if self.is_helo else self.doctrine.ingress_altitude,
50
) if self.is_helo else self.doctrine.ingress_altitude
) )
if self.is_helo: if self.is_helo:
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -480,18 +450,14 @@ class WaypointBuilder:
altitude: Altitude of the waypoint. altitude: Altitude of the waypoint.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.NAV, FlightWaypointType.NAV, position.x, position.y, altitude
position.x,
position.y,
altitude
) )
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.description = "NAV" waypoint.description = "NAV"
waypoint.pretty_name = "Nav" waypoint.pretty_name = "Nav"
return waypoint return waypoint
def nav_path(self, a: Point, b: Point, def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
altitude: Distance) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b)) path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path] return [self.nav(self.perturb(p), altitude) for p in path]
@ -518,10 +484,8 @@ class WaypointBuilder:
previous = current previous = current
current = nxt current = nxt
def nav_point_prunable(self, previous: Point, current: Point, def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
nxt: Point) -> bool: previous_threatened = self.threat_zones.path_threatened(previous, current)
previous_threatened = self.threat_zones.path_threatened(previous,
current)
next_threatened = self.threat_zones.path_threatened(current, nxt) next_threatened = self.threat_zones.path_threatened(current, nxt)
pruned_threatened = self.threat_zones.path_threatened(previous, nxt) pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
previous_distance = meters(previous.distance_to_point(current)) previous_distance = meters(previous.distance_to_point(current))

View File

@ -15,11 +15,15 @@ class ForcedOptionsGenerator:
self.game = game self.game = game
def _set_options_view(self) -> None: def _set_options_view(self) -> None:
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility self.mission.forced_options.options_view = (
self.game.settings.map_coalition_visibility
)
def _set_external_views(self) -> None: def _set_external_views(self) -> None:
if not self.game.settings.external_views_allowed: if not self.game.settings.external_views_allowed:
self.mission.forced_options.external_views = self.game.settings.external_views_allowed self.mission.forced_options.external_views = (
self.game.settings.external_views_allowed
)
def _set_labels(self) -> None: def _set_labels(self) -> None:
# TODO: Fix settings to use the real type. # TODO: Fix settings to use the real type.

View File

@ -39,12 +39,11 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
CombatStance.RETREAT: [2, 4, 6, 8], CombatStance.RETREAT: [2, 4, 6, 8],
CombatStance.BREAKTHROUGH: [4, 6, 6, 8], CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
CombatStance.ELIMINATION: [2, 4, 4, 4, 6], CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4] CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4],
} }
class CombatGroup: class CombatGroup:
def __init__(self, role: CombatGroupRole): def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = [] self.units: List[VehicleType] = []
self.role = role self.role = role
@ -60,11 +59,12 @@ class CombatGroup:
class GroundPlanner: class GroundPlanner:
def __init__(self, cp: ControlPoint, game):
def __init__(self, cp:ControlPoint, game):
self.cp = cp self.cp = cp
self.game = game self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured] self.connected_enemy_cp = [
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
]
self.tank_groups: List[CombatGroup] = [] self.tank_groups: List[CombatGroup] = []
self.apc_group: List[CombatGroup] = [] self.apc_group: List[CombatGroup] = []
self.ifv_group: List[CombatGroup] = [] self.ifv_group: List[CombatGroup] = []
@ -80,7 +80,7 @@ class GroundPlanner:
def plan_groundwar(self): def plan_groundwar(self):
if hasattr(self.cp, 'stance'): if hasattr(self.cp, "stance"):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance] group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else: else:
self.cp.stance = CombatStance.DEFENSIVE self.cp.stance = CombatStance.DEFENSIVE
@ -152,12 +152,3 @@ class GroundPlanner:
print("For : #" + str(key)) print("For : #" + str(key))
for group in self.units_per_cp[key]: for group in self.units_per_cp[key]:
print(str(group)) print(str(group))

View File

@ -16,7 +16,6 @@ TYPE_TANKS = [
Armor.MBT_M60A3_Patton, Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4, Armor.MBT_Merkava_Mk__4,
Armor.ZTZ_96B, Armor.ZTZ_96B,
# WW2 # WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G, Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H, Armor.MT_Pz_Kpfw_IV_Ausf_H,
@ -29,36 +28,30 @@ TYPE_TANKS = [
Armor.CT_Cromwell_IV, Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII, Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch, Armor.LT_Mk_VII_Tetrarch,
# Mods # Mods
frenchpack.DIM__TOYOTA_BLUE, frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN, frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT, frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE, frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR, frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR, frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2, frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI, frenchpack.Leclerc_Serie_XXI,
] ]
TYPE_ATGM = [ TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW, Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker, Armor.ATGM_M1134_Stryker,
Armor.IFV_BMP_2, Armor.IFV_BMP_2,
# WW2 (Tank Destroyers) # WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier, Armor.M30_Cargo_Carrier,
Armor.TD_Jagdpanzer_IV, Armor.TD_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1, Armor.TD_Jagdpanther_G1,
Armor.TD_M10_GMC, Armor.TD_M10_GMC,
# Mods # Mods
frenchpack.VBAE_CRAB_MMP, frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO, frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA, frenchpack.TRM_2000_PAMELA,
] ]
TYPE_IFV = [ TYPE_IFV = [
@ -73,17 +66,14 @@ TYPE_IFV = [
Armor.IFV_M2A2_Bradley, Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1, Armor.IFV_BMD_1,
Armor.ZBD_04A, Armor.ZBD_04A,
# WW2 # WW2
Armor.AC_Sd_Kfz_234_2_Puma, Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound, Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car, Armor.Daimler_Armoured_Car,
# Mods # Mods
frenchpack.ERC_90, frenchpack.ERC_90,
frenchpack.VBAE_CRAB, frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13 frenchpack.VAB_T20_13,
] ]
TYPE_APC = [ TYPE_APC = [
@ -101,16 +91,13 @@ TYPE_APC = [
Armor.ARV_BRDM_2, Armor.ARV_BRDM_2,
Armor.ARV_BTR_RD, Armor.ARV_BTR_RD,
Armor.FDDM_Grad, Armor.FDDM_Grad,
# WW2 # WW2
Armor.APC_M2A1, Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251, Armor.APC_Sd_Kfz_251,
# Mods # Mods
frenchpack.VAB__50, frenchpack.VAB__50,
frenchpack.VBL__50, frenchpack.VBL__50,
frenchpack.VBL_AANF1, frenchpack.VBL_AANF1,
] ]
TYPE_ARTILLERY = [ TYPE_ARTILLERY = [
@ -125,10 +112,9 @@ TYPE_ARTILLERY = [
Artillery.SpGH_Dana, Artillery.SpGH_Dana,
Artillery.SPH_2S19_Msta, Artillery.SPH_2S19_Msta,
Artillery.MLRS_FDDM, Artillery.MLRS_FDDM,
# WW2 # WW2
Artillery.Sturmpanzer_IV_Brummbär, Artillery.Sturmpanzer_IV_Brummbär,
Artillery.M12_GMC Artillery.M12_GMC,
] ]
TYPE_LOGI = [ TYPE_LOGI = [
@ -147,11 +133,9 @@ TYPE_LOGI = [
Unarmed.Willys_MB, Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3, Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC, Unarmed.Land_Rover_101_FC,
# Mods # Mods
frenchpack.VBL, frenchpack.VBL,
frenchpack.VAB, frenchpack.VAB,
] ]
TYPE_INFANTRY = [ TYPE_INFANTRY = [
@ -179,7 +163,6 @@ TYPE_SHORAD = [
AirDefence.SAM_SA_13_Strela_10M3_9A35M3, AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SAM_SA_15_Tor_9A331, AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_19_Tunguska_2S6, AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163, AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6, AirDefence.SAM_Linebacker_M6,
@ -187,7 +170,6 @@ TYPE_SHORAD = [
AirDefence.SAM_Avenger_M1097, AirDefence.SAM_Avenger_M1097,
AirDefence.SAM_Roland_ADS, AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN, AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
@ -195,5 +177,4 @@ TYPE_SHORAD = [
AirDefence.AAA_Bofors_40mm, AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm, AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7, AirDefence.AA_gun_QF_3_7,
] ]

View File

@ -2,10 +2,11 @@ from enum import Enum
class CombatStance(Enum): class CombatStance(Enum):
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units AGGRESSIVE = (
RETREAT = 2 # Unit will retreat 1 # Unit will attempt to make progress with medium sized group of units
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively )
RETREAT = 2 # Unit will retreat
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller) AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)

View File

@ -18,7 +18,8 @@ from dcs.task import (
ActivateBeaconCommand, ActivateBeaconCommand,
ActivateICLSCommand, ActivateICLSCommand,
EPLRS, EPLRS,
OptAlarmState, FireAtPoint, OptAlarmState,
FireAtPoint,
) )
from dcs.unit import Ship, Unit, Vehicle from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
@ -30,9 +31,12 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from game.theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject, BuildingGroundObject,
CarrierGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject, LhaGroundObject,
ShipGroundObject,
MissileSiteGroundObject,
) )
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import knots, mps from game.utils import knots, mps
@ -53,8 +57,15 @@ class GenericGroundObjectGenerator:
Currently used only for SAM Currently used only for SAM
""" """
def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission, unit_map: UnitMap) -> None: def __init__(
self,
ground_object: TheaterGroundObject,
country: Country,
game: Game,
mission: Mission,
unit_map: UnitMap,
) -> None:
self.ground_object = ground_object self.ground_object = ground_object
self.country = country self.country = country
self.game = game self.game = game
@ -72,18 +83,22 @@ class GenericGroundObjectGenerator:
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group(self.country, group.name, unit_type, vg = self.m.vehicle_group(
position=group.position, self.country,
heading=group.units[0].heading) 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].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True vg.units[0].player_can_drive = True
for i, u in enumerate(group.units): for i, u in enumerate(group.units):
if i > 0: if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), vehicle = Vehicle(
self.m.string(u.name), u.type) self.m.next_unit_id(), self.m.string(u.name), u.type
)
vehicle.position.x = u.position.x vehicle.position.x = u.position.x
vehicle.position.y = u.position.y vehicle.position.y = u.position.y
vehicle.heading = u.heading vehicle.heading = u.heading
@ -96,7 +111,7 @@ class GenericGroundObjectGenerator:
@staticmethod @staticmethod
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None: def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, 'eplrs'): if hasattr(unit_type, "eplrs"):
if unit_type.eplrs: if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
@ -106,14 +121,13 @@ class GenericGroundObjectGenerator:
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group, def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
miz_group: Group) -> None: self.unit_map.add_ground_object_units(
self.unit_map.add_ground_object_units(self.ground_object, self.ground_object, persistence_group, miz_group
persistence_group, miz_group) )
class MissileSiteGenerator(GenericGroundObjectGenerator): class MissileSiteGenerator(GenericGroundObjectGenerator):
def generate(self) -> None: def generate(self) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
@ -125,13 +139,19 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
targets = self.possible_missile_targets(vg) targets = self.possible_missile_targets(vg)
if targets: if targets:
target = random.choice(targets) target = random.choice(targets)
real_target = target.point_from_heading(random.randint(0, 360), random.randint(0, 2500)) real_target = target.point_from_heading(
random.randint(0, 360), random.randint(0, 2500)
)
vg.points[0].add_task(FireAtPoint(real_target)) vg.points[0].add_task(FireAtPoint(real_target))
logging.info("Set up fire task for missile group.") logging.info("Set up fire task for missile group.")
else: else:
logging.info("Couldn't setup missile site to fire, no valid target in range.") logging.info(
"Couldn't setup missile site to fire, no valid target in range."
)
else: else:
logging.info("Couldn't setup missile site to fire, group was not generated.") logging.info(
"Couldn't setup missile site to fire, group was not generated."
)
def possible_missile_targets(self, vg: Group) -> List[Point]: def possible_missile_targets(self, vg: Group) -> List[Point]:
""" """
@ -190,7 +210,8 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
break break
else: else:
logging.error( logging.error(
f"{self.ground_object.dcs_identifier} not found in static maps") f"{self.ground_object.dcs_identifier} not found in static maps"
)
def generate_vehicle_group(self, unit_type: UnitType) -> None: def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
@ -228,11 +249,20 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
Used by both CV(N) groups and LHA groups. Used by both CV(N) groups and LHA groups.
""" """
def __init__(self, ground_object: GenericCarrierGroundObject,
control_point: ControlPoint, country: Country, game: Game, def __init__(
mission: Mission, radio_registry: RadioRegistry, self,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int], ground_object: GenericCarrierGroundObject,
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None: control_point: ControlPoint,
country: Country,
game: Game,
mission: Mission,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
icls_alloc: Iterator[int],
runways: Dict[str, RunwayData],
unit_map: UnitMap,
) -> None:
super().__init__(ground_object, country, game, mission, unit_map) super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object self.ground_object = ground_object
self.control_point = control_point self.control_point = control_point
@ -245,8 +275,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
# TODO: Require single group? # TODO: Require single group?
for group in self.ground_object.groups: for group in self.ground_object.groups:
if not group.units: if not group.units:
logging.warning( logging.warning(f"Found empty carrier group in {self.control_point}")
f"Found empty carrier group in {self.control_point}")
continue continue
atc = self.radio_registry.alloc_uhf() atc = self.radio_registry.alloc_uhf()
@ -270,25 +299,29 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
def get_carrier_type(self, group: Group) -> Type[UnitType]: def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
f"Unrecognized carrier name: {group.units[0].type}")
return unit_type return unit_type
def configure_carrier(self, group: Group, def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
atc_channel: RadioFrequency) -> ShipGroup:
unit_type = self.get_carrier_type(group) unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(self.country, group.name, unit_type, ship_group = self.m.ship_group(
position=group.position, self.country,
heading=group.units[0].heading) group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
ship_group.set_frequency(atc_channel.hertz) ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = self.m.string(group.units[0].name) ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship: def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(self.m.next_unit_id(), ship = Ship(
self.m.string(unit.name), self.m.next_unit_id(),
unit_type_from_name(unit.type)) self.m.string(unit.name),
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x ship.position.x = unit.position.x
ship.position.y = unit.position.y ship.position.y = unit.position.y
ship.heading = unit.heading ship.heading = unit.heading
@ -303,7 +336,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
carrier_speed = knots(25) - mps(wind.speed) carrier_speed = knots(25) - mps(wind.speed)
for attempt in range(5): for attempt in range(5):
point = group.points[0].position.point_from_heading( point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000) brc, 100000 - attempt * 20000
)
if self.game.theater.is_in_sea(point): if self.game.theater.is_in_sea(point):
group.points[0].speed = carrier_speed.meters_per_second group.points[0].speed = carrier_speed.meters_per_second
group.add_waypoint(point, carrier_speed.kph) group.add_waypoint(point, carrier_speed.kph)
@ -314,21 +348,30 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def activate_beacons(group: ShipGroup, tacan: TacanChannel, def activate_beacons(
callsign: str, icls: int) -> None: group: ShipGroup, tacan: TacanChannel, callsign: str, icls: int
group.points[0].tasks.append(ActivateBeaconCommand( ) -> None:
channel=tacan.number, group.points[0].tasks.append(
modechannel=tacan.band.value, ActivateBeaconCommand(
callsign=callsign, channel=tacan.number,
unit_id=group.units[0].id, modechannel=tacan.band.value,
aa=False callsign=callsign,
)) unit_id=group.units[0].id,
group.points[0].tasks.append(ActivateICLSCommand( aa=False,
icls, unit_id=group.units[0].id )
)) )
group.points[0].tasks.append(
ActivateICLSCommand(icls, unit_id=group.units[0].id)
)
def add_runway_data(self, brc: int, atc: RadioFrequency, def add_runway_data(
tacan: TacanChannel, callsign: str, icls: int) -> None: self,
brc: int,
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
icls: int,
) -> None:
# TODO: Make unit name usable. # TODO: Make unit name usable.
# This relies on one control point mapping exactly # This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway". # to one LHA, carrier, or other usable "runway".
@ -354,8 +397,7 @@ class CarrierGenerator(GenericCarrierGenerator):
def get_carrier_type(self, group: Group) -> UnitType: def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group) unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier: if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type, unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
self.control_point.name)
return unit_type return unit_type
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
@ -363,18 +405,20 @@ class CarrierGenerator(GenericCarrierGenerator):
if self.control_point.name == "Carrier Strike Group 8": if self.control_point.name == "Carrier Strike Group 8":
return "TRU" return "TRU"
else: else:
return random.choice([ return random.choice(
"STE", [
"CVN", "STE",
"CVH", "CVN",
"CCV", "CVH",
"ACC", "CCV",
"ARC", "ACC",
"GER", "ARC",
"ABR", "GER",
"LIN", "ABR",
"TRU", "LIN",
]) "TRU",
]
)
class LhaGenerator(GenericCarrierGenerator): class LhaGenerator(GenericCarrierGenerator):
@ -382,14 +426,16 @@ class LhaGenerator(GenericCarrierGenerator):
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
# TODO: Assign these properly. # TODO: Assign these properly.
return random.choice([ return random.choice(
"LHD", [
"LHA", "LHD",
"LHB", "LHA",
"LHC", "LHB",
"LHD", "LHC",
"LDS", "LHD",
]) "LDS",
]
)
class ShipObjectGenerator(GenericGroundObjectGenerator): class ShipObjectGenerator(GenericGroundObjectGenerator):
@ -406,22 +452,23 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type) self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
first_unit_type: Type[UnitType]) -> None: group = self.m.ship_group(
group = self.m.ship_group(self.country, group_def.name, first_unit_type, self.country,
position=group_def.position, group_def.name,
heading=group_def.units[0].heading) first_unit_type,
position=group_def.position,
heading=group_def.units[0].heading,
)
group.units[0].name = self.m.string(group_def.units[0].name) group.units[0].name = self.m.string(group_def.units[0].name)
# TODO: Skipping the first unit looks like copy pasta from the carrier. # TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]: for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type) unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(), ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x ship.position.x = unit.position.x
ship.position.y = unit.position.y ship.position.y = unit.position.y
ship.heading = unit.heading ship.heading = unit.heading
@ -439,9 +486,14 @@ class GroundObjectsGenerator:
the appropriate generators. the appropriate generators.
""" """
def __init__(self, mission: Mission, game: Game, def __init__(
radio_registry: RadioRegistry, tacan_registry: TacanRegistry, self,
unit_map: UnitMap) -> None: mission: Mission,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
@ -461,28 +513,44 @@ class GroundObjectsGenerator:
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject): if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator( generator = BuildingSiteGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
elif isinstance(ground_object, CarrierGroundObject): elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator( generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m, ground_object,
self.radio_registry, self.tacan_registry, cp,
self.icls_alloc, self.runways, self.unit_map) country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, LhaGroundObject): elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator( generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m, ground_object,
self.radio_registry, self.tacan_registry, cp,
self.icls_alloc, self.runways, self.unit_map) country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, ShipGroundObject): elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator( generator = ShipObjectGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
elif isinstance(ground_object, MissileSiteGroundObject): elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator( generator = MissileSiteGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
else: else:
generator = GenericGroundObjectGenerator( generator = GenericGroundObjectGenerator(
ground_object, country, self.game, self.m, ground_object, country, self.game, self.m, self.unit_map
self.unit_map) )
generator.generate() generator.generate()

View File

@ -49,7 +49,7 @@ class KneeboardPageWriter:
"""Creates kneeboard images.""" """Creates kneeboard images."""
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None: def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff)) self.image = Image.new("RGB", (768, 1024), (0xFF, 0xFF, 0xFF))
# These font sizes create a relatively full page for current sorties. If # These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including # we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should # more information in the comm ladder (the latter of which we should
@ -58,8 +58,7 @@ class KneeboardPageWriter:
self.title_font = ImageFont.truetype("arial.ttf", 32) self.title_font = ImageFont.truetype("arial.ttf", 32)
self.heading_font = ImageFont.truetype("arial.ttf", 24) self.heading_font = ImageFont.truetype("arial.ttf", 24)
self.content_font = ImageFont.truetype("arial.ttf", 20) self.content_font = ImageFont.truetype("arial.ttf", 20)
self.table_font = ImageFont.truetype( self.table_font = ImageFont.truetype("resources/fonts/Inconsolata.otf", 20)
"resources/fonts/Inconsolata.otf", 20)
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
self.x = page_margin self.x = page_margin
self.y = page_margin self.y = page_margin
@ -69,8 +68,9 @@ class KneeboardPageWriter:
def position(self) -> Tuple[int, int]: def position(self) -> Tuple[int, int]:
return self.x, self.y return self.x, self.y
def text(self, text: str, font=None, def text(
fill: Tuple[int, int, int] = (0, 0, 0)) -> None: self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
) -> None:
if font is None: if font is None:
font = self.content_font font = self.content_font
@ -84,8 +84,9 @@ class KneeboardPageWriter:
def heading(self, text: str) -> None: def heading(self, text: str) -> None:
self.text(text, font=self.heading_font) self.text(text, font=self.heading_font)
def table(self, cells: List[List[str]], def table(
headers: Optional[List[str]] = None) -> None: self, cells: List[List[str]], headers: Optional[List[str]] = None
) -> None:
if headers is None: if headers is None:
headers = [] headers = []
table = tabulate(cells, headers=headers, numalign="right") table = tabulate(cells, headers=headers, numalign="right")
@ -157,29 +158,34 @@ class FlightPlanBuilder:
first_waypoint_num = self.target_points[0].number first_waypoint_num = self.target_points[0].number
last_waypoint_num = self.target_points[-1].number last_waypoint_num = self.target_points[-1].number
self.rows.append([ self.rows.append(
f"{first_waypoint_num}-{last_waypoint_num}", [
"Target points", f"{first_waypoint_num}-{last_waypoint_num}",
"0", "Target points",
self._waypoint_distance(self.target_points[0].waypoint), "0",
self._ground_speed(self.target_points[0].waypoint), self._waypoint_distance(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.tot), self._ground_speed(self.target_points[0].waypoint),
self._format_time(self.target_points[0].waypoint.departure_time), 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 self.last_waypoint = self.target_points[-1].waypoint
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([ self.rows.append(
str(waypoint.number), [
KneeboardPageWriter.wrap_line( str(waypoint.number),
waypoint.waypoint.pretty_name, KneeboardPageWriter.wrap_line(
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN), waypoint.waypoint.pretty_name,
str(int(waypoint.waypoint.alt.feet)), FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN,
self._waypoint_distance(waypoint.waypoint), ),
self._ground_speed(waypoint.waypoint), str(int(waypoint.waypoint.alt.feet)),
self._format_time(waypoint.waypoint.tot), self._waypoint_distance(waypoint.waypoint),
self._format_time(waypoint.waypoint.departure_time), 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: def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None: if time is None:
@ -191,9 +197,9 @@ class FlightPlanBuilder:
if self.last_waypoint is None: if self.last_waypoint is None:
return "-" return "-"
distance = meters(self.last_waypoint.position.distance_to_point( distance = meters(
waypoint.position self.last_waypoint.position.distance_to_point(waypoint.position)
)) )
return f"{distance.nautical_miles:.1f} NM" return f"{distance.nautical_miles:.1f} NM"
def _ground_speed(self, waypoint: FlightWaypoint) -> str: def _ground_speed(self, waypoint: FlightWaypoint) -> str:
@ -210,9 +216,9 @@ class FlightPlanBuilder:
else: else:
return "-" return "-"
distance = meters(self.last_waypoint.position.distance_to_point( distance = meters(
waypoint.position self.last_waypoint.position.distance_to_point(waypoint.position)
)) )
duration = (waypoint.tot - last_time).total_seconds() / 3600 duration = (waypoint.tot - last_time).total_seconds() / 3600
return f"{int(distance.nautical_miles / duration)} kt" return f"{int(distance.nautical_miles / duration)} kt"
@ -222,9 +228,16 @@ class FlightPlanBuilder:
class BriefingPage(KneeboardPage): class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information.""" """A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo], def __init__(
jtacs: List[JtacInfo], start_time: datetime.datetime) -> None: self,
flight: FlightData,
comms: List[CommInfo],
awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
start_time: datetime.datetime,
) -> None:
self.flight = flight self.flight = flight
self.comms = list(comms) self.comms = list(comms)
self.awacs = awacs self.awacs = awacs
@ -239,49 +252,59 @@ class BriefingPage(KneeboardPage):
# TODO: Handle carriers. # TODO: Handle carriers.
writer.heading("Airfield Info") writer.heading("Airfield Info")
writer.table([ writer.table(
self.airfield_info_row("Departure", self.flight.departure), [
self.airfield_info_row("Arrival", self.flight.arrival), self.airfield_info_row("Departure", self.flight.departure),
self.airfield_info_row("Divert", self.flight.divert), self.airfield_info_row("Arrival", self.flight.arrival),
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"]) self.airfield_info_row("Divert", self.flight.divert),
],
headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"],
)
writer.heading("Flight Plan") writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder(self.start_time) flight_plan_builder = FlightPlanBuilder(self.start_time)
for num, waypoint in enumerate(self.flight.waypoints): for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint) flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(), headers=[ writer.table(
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure" flight_plan_builder.build(),
]) headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
)
flight_plan_builder flight_plan_builder
writer.table([ writer.table(
["{}lbs".format(self.flight.bingo_fuel), "{}lbs".format(self.flight.joker_fuel)] [
], ['Bingo', 'Joker']) [
"{}lbs".format(self.flight.bingo_fuel),
"{}lbs".format(self.flight.joker_fuel),
]
],
["Bingo", "Joker"],
)
# Package Section # Package Section
writer.heading("Comm ladder") writer.heading("Comm ladder")
comm_ladder = [] comm_ladder = []
for comm in self.comms: for comm in self.comms:
comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)]) comm_ladder.append(
[comm.name, "", "", "", self.format_frequency(comm.freq)]
)
for a in self.awacs: for a in self.awacs:
comm_ladder.append([ comm_ladder.append(
a.callsign, [a.callsign, "AWACS", "", "", self.format_frequency(a.freq)]
'AWACS', )
'',
'',
self.format_frequency(a.freq)
])
for tanker in self.tankers: for tanker in self.tankers:
comm_ladder.append([ comm_ladder.append(
tanker.callsign, [
"Tanker", tanker.callsign,
tanker.variant, "Tanker",
str(tanker.tacan), tanker.variant,
self.format_frequency(tanker.freq), str(tanker.tacan),
]) self.format_frequency(tanker.freq),
]
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"]) )
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
writer.heading("JTAC") writer.heading("JTAC")
jtacs = [] jtacs = []
@ -291,8 +314,9 @@ class BriefingPage(KneeboardPage):
writer.write(path) writer.write(path)
def airfield_info_row(self, row_title: str, def airfield_info_row(
runway: Optional[RunwayData]) -> List[str]: self, row_title: str, runway: Optional[RunwayData]
) -> List[str]:
"""Creates a table row for a given airfield. """Creates a table row for a given airfield.
Args: Args:
@ -372,7 +396,8 @@ class KneeboardGenerator(MissionInfoGenerator):
if not flight.client_units: if not flight.client_units:
continue continue
all_flights[flight.aircraft_type].extend( all_flights[flight.aircraft_type].extend(
self.generate_flight_kneeboard(flight)) self.generate_flight_kneeboard(flight)
)
return all_flights return all_flights
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
@ -384,6 +409,6 @@ class KneeboardGenerator(MissionInfoGenerator):
self.awacs, self.awacs,
self.tankers, self.tankers,
self.jtacs, self.jtacs,
self.mission.start_time self.mission.start_time,
), ),
] ]

View File

@ -9,9 +9,10 @@ from gen.locations.preset_locations import PresetLocation
class MizDataLocationFinder: class MizDataLocationFinder:
@staticmethod @staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations: def compute_possible_locations(
terrain_name: str, cp_name: str
) -> PresetControlPointLocations:
""" """
Extract the list of preset locations from miz data Extract the list of preset locations from miz data
:param terrain_name: Terrain/Map name :param terrain_name: Terrain/Map name
@ -32,28 +33,54 @@ class MizDataLocationFinder:
for vehicle_group in m.country("USA").vehicle_group: for vehicle_group in m.country("USA").vehicle_group:
if len(vehicle_group.units) > 0: if len(vehicle_group.units) > 0:
ashore_locations.append(PresetLocation(vehicle_group.position, ashore_locations.append(
vehicle_group.units[0].heading, PresetLocation(
vehicle_group.name)) vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
for ship_group in m.country("USA").ship_group: 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: if (
offshore_locations.append(PresetLocation(ship_group.position, len(ship_group.units) > 0
ship_group.units[0].heading, and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id
ship_group.name)) ):
offshore_locations.append(
PresetLocation(
ship_group.position,
ship_group.units[0].heading,
ship_group.name,
)
)
for static_group in m.country("USA").static_group: for static_group in m.country("USA").static_group:
if len(static_group.units) > 0: if len(static_group.units) > 0:
powerplants_locations.append(PresetLocation(static_group.position, powerplants_locations.append(
static_group.units[0].heading, PresetLocation(
static_group.name)) static_group.position,
static_group.units[0].heading,
static_group.name,
)
)
if m.country("Iran") is not None: if m.country("Iran") is not None:
for vehicle_group in m.country("Iran").vehicle_group: 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: if (
antiship_locations.append(PresetLocation(vehicle_group.position, len(vehicle_group.units) > 0
vehicle_group.units[0].heading, and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id
vehicle_group.name)) ):
antiship_locations.append(
PresetLocation(
vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
return PresetControlPointLocations(ashore_locations, offshore_locations, return PresetControlPointLocations(
antiship_locations, powerplants_locations) ashore_locations,
offshore_locations,
antiship_locations,
powerplants_locations,
)

View File

@ -6,10 +6,16 @@ from dcs import Point
@dataclass @dataclass
class PresetLocation: class PresetLocation:
"""A preset location""" """A preset location"""
position: Point position: Point
heading: int heading: int
id: str id: str
def __str__(self): def __str__(self):
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading, return (
self.id) + "-" * 10 "-" * 10
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
self.position.x, self.position.y, self.heading, self.id
)
+ "-" * 10
)

View File

@ -4,10 +4,7 @@ from game import db
from gen.missiles.scud_site import ScudGenerator from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = { MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
"V1GroupGenerator": V1GroupGenerator,
"ScudGenerator": ScudGenerator
}
def generate_missile_group(game, ground_object, faction_name: str): def generate_missile_group(game, ground_object, faction_name: str):
@ -25,5 +22,9 @@ def generate_missile_group(game, ground_object, faction_name: str):
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists") logging.info(
return None "Unable to generate missile group, generator : "
+ str(gen)
+ "does not exists"
)
return None

View File

@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
class ScudGenerator(GroupGenerator): class ScudGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction): def __init__(self, game, ground_object, faction):
super(ScudGenerator, self).__init__(game, ground_object) super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
@ -14,17 +13,50 @@ class ScudGenerator(GroupGenerator):
def generate(self): def generate(self):
# Scuds # 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(
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) MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
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) "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 # Commander
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.Transport_UAZ_469,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad # Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38, self.add_unit(
self.heading) 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.add_unit(
self.position.x + 200, self.position.y + 15, 90) AirDefence.SAM_SA_9_Strela_1_9P31,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@ -6,7 +6,6 @@ from gen.sam.group_generator import GroupGenerator
class V1GroupGenerator(GroupGenerator): class V1GroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction): def __init__(self, game, ground_object, faction):
super(V1GroupGenerator, self).__init__(game, ground_object) super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction self.faction = faction
@ -14,19 +13,54 @@ class V1GroupGenerator(GroupGenerator):
def generate(self): def generate(self):
# Ramps # Ramps
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading) self.add_unit(
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading) MissilesSS.V_1_ramp,
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading) "V1#0",
self.position.x,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.V_1_ramp,
"V1#1",
self.position.x + 50,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.V_1_ramp,
"V1#2",
self.position.x + 100,
self.position.y + random.randint(1, 8),
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.Kübelwagen_82,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Self defense flak # Self defense flak
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38]) flak_unit = random.choice(
[AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38]
)
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38, self.add_unit(
self.heading) flak_unit,
"FLAK#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0", self.add_unit(
self.position.x + 200, self.position.y + 15, 90) Unarmed.Blitz_3_6_6700A,
"Blitz#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@ -9,39 +9,242 @@ from game import db
from gen.flights.flight import Flight from gen.flights.flight import Flight
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", ALPHA_MILITARY = [
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "Alpha",
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Bravo",
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee", "Charlie",
"Zulu", "Zero"] "Delta",
"Echo",
"Foxtrot",
"Golf",
"Hotel",
"India",
"Juliet",
"Kilo",
"Lima",
"Mike",
"November",
"Oscar",
"Papa",
"Quebec",
"Romeo",
"Sierra",
"Tango",
"Uniform",
"Victor",
"Whisky",
"XRay",
"Yankee",
"Zulu",
"Zero",
]
ANIMALS = [ ANIMALS = [
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF", "SHARK",
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER", "TORTOISE",
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA", "BAT",
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX", "PANGOLIN",
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE", "AARDWOLF",
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA", "MONKEY",
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA", "BUFFALO",
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE", "DOG",
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA", "BOBCAT",
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON", "LYNX",
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON", "PANTHER",
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT", "TIGER",
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO", "LION",
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER", "OWL",
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD", "BUTTERFLY",
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER", "BISON",
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN", "DUCK",
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR", "COBRA",
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN", "MAMBA",
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH", "DOLPHIN",
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL", "PHEASANT",
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA", "ARMADILLLO",
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK", "RACOON",
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK", "ZEBRA",
"ANACONDA" "COW",
] "COYOTE",
"FOX",
"LIGHTFOOT",
"COTTONMOUTH",
"TAURUS",
"VIPER",
"CASTOR",
"GIRAFFE",
"SNAKE",
"MONSTER",
"ALBATROSS",
"HAWK",
"DOVE",
"MOCKINGBIRD",
"GECKO",
"ORYX",
"GORILLA",
"HARAMBE",
"GOOSE",
"MAVERICK",
"HARE",
"JACKAL",
"LEOPARD",
"CAT",
"MUSK",
"ORCA",
"OCELOT",
"BEAR",
"PANDA",
"GULL",
"PENGUIN",
"PYTHON",
"RAVEN",
"DEER",
"MOOSE",
"REINDEER",
"SHEEP",
"GAZELLE",
"INSECT",
"VULTURE",
"WALLABY",
"KANGAROO",
"KOALA",
"KIWI",
"WHALE",
"FISH",
"RHINO",
"HIPPO",
"RAT",
"WOODPECKER",
"WORM",
"BABOON",
"YAK",
"SCORPIO",
"HORSE",
"POODLE",
"CENTIPEDE",
"CHICKEN",
"CHEETAH",
"CHAMELEON",
"CATFISH",
"CATERPILLAR",
"CARACAL",
"CAMEL",
"CAIMAN",
"BARRACUDA",
"BANDICOOT",
"ALLIGATOR",
"BONGO",
"CORAL",
"ELEPHANT",
"ANTELOPE",
"CRAB",
"DACHSHUND",
"DODO",
"FLAMINGO",
"FERRET",
"FALCON",
"BULLDOG",
"DONKEY",
"IGUANA",
"TAMARIN",
"HARRIER",
"GRIZZLY",
"GREYHOUND",
"GRASSHOPPER",
"JAGUAR",
"LADYBUG",
"KOMODO",
"DRAGON",
"LIZARD",
"LLAMA",
"LOBSTER",
"OCTOPUS",
"MANATEE",
"MAGPIE",
"MACAW",
"OSTRICH",
"OYSTER",
"MOLE",
"MULE",
"MOTH",
"MONGOOSE",
"MOLLY",
"MEERKAT",
"MOUSE",
"PEACOCK",
"PIKE",
"ROBIN",
"RAGDOLL",
"PLATYPUS",
"PELICAN",
"PARROT",
"PORCUPINE",
"PIRANHA",
"PUMA",
"PUG",
"TAPIR",
"TERMITE",
"URCHIN",
"SHRIMP",
"TURKEY",
"TOUCAN",
"TETRA",
"HUSKY",
"STARFISH",
"SWAN",
"FROG",
"SQUIRREL",
"WALRUS",
"WARTHOG",
"CORGI",
"WEASEL",
"WOMBAT",
"WOLVERINE",
"MAMMOTH",
"TOAD",
"WOLF",
"ZEBU",
"SEAL",
"SKATE",
"JELLYFISH",
"MOSQUITO",
"LOCUST",
"SLUG",
"SNAIL",
"HEDGEHOG",
"PIGLET",
"FENNEC",
"BADGER",
"ALPACA",
"DINGO",
"COLT",
"SKUNK",
"BUNNY",
"IMPALA",
"GUANACO",
"CAPYBARA",
"ELK",
"MINK",
"PRONGHORN",
"CROW",
"BUMBLEBEE",
"FAWN",
"OTTER",
"WATERBUCK",
"JERBOA",
"KITTEN",
"ARGALI",
"OX",
"MARE",
"FINCH",
"BASILISK",
"GOPHER",
"HAMSTER",
"CANARY",
"WOODCHUCK",
"ANACONDA",
]
class NameGenerator: class NameGenerator:
number = 0 number = 0
@ -72,21 +275,36 @@ class NameGenerator:
name_str = flight.custom_name name_str = flight.custom_name
else: else:
name_str = "{} {}".format( name_str = "{} {}".format(
flight.package.target.name, flight.flight_type) flight.package.target.name, flight.flight_type
)
except AttributeError: # Here to maintain save compatibility with 2.3 except AttributeError: # Here to maintain save compatibility with 2.3
name_str = "{} {}".format( name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
flight.package.target.name, flight.flight_type) return "{}|{}|{}|{}|{}|".format(
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type)) name_str,
country.id,
cls.aircraft_number,
parent_base_id,
db.unit_type_name(flight.unit_type),
)
@classmethod @classmethod
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
cls.number += 1 cls.number += 1
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)) return "unit|{}|{}|{}|{}|".format(
country.id, cls.number, parent_base_id, db.unit_type_name(unit_type)
)
@classmethod @classmethod
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType): def next_infantry_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType
):
cls.infantry_number += 1 cls.infantry_number += 1
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type)) return "infantry|{}|{}|{}|{}|".format(
country.id,
cls.infantry_number,
parent_base_id,
db.unit_type_name(unit_type),
)
@staticmethod @staticmethod
def next_basedefense_name(): def next_basedefense_name():
@ -100,7 +318,9 @@ class NameGenerator:
@classmethod @classmethod
def next_tanker_name(cls, country: Country, unit_type: UnitType): def next_tanker_name(cls, country: Country, unit_type: UnitType):
cls.number += 1 cls.number += 1
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type)) return "tanker|{}|{}|0|{}".format(
country.id, cls.number, db.unit_type_name(unit_type)
)
@classmethod @classmethod
def next_carrier_name(cls, country: Country): def next_carrier_name(cls, country: Country):
@ -112,7 +332,11 @@ class NameGenerator:
if len(cls.ANIMALS) == 0: if len(cls.ANIMALS) == 0:
for i in range(10): for i in range(10):
new_name_generated = True new_name_generated = True
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100)) alpha_mil_name = (
random.choice(ALPHA_MILITARY).upper()
+ "#"
+ str(random.randint(0, 100))
)
for existing_name in cls.existing_alphas: for existing_name in cls.existing_alphas:
if existing_name == alpha_mil_name: if existing_name == alpha_mil_name:
new_name_generated = False new_name_generated = False

View File

@ -68,9 +68,10 @@ class Radio:
def range(self) -> Iterator[RadioFrequency]: def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio.""" """Returns an iterator over the usable frequencies of this radio."""
return (RadioFrequency(x) for x in range( return (
self.minimum.hertz, self.maximum.hertz, self.step.hertz RadioFrequency(x)
)) for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
)
@property @property
def last_channel(self) -> RadioFrequency: def last_channel(self) -> RadioFrequency:
@ -99,14 +100,12 @@ RADIOS: List[Radio] = [
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)), Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)), Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)), Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between # Note: The M2000C V/UHF can operate in both ranges, but has a gap between
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current # 150 MHz and 225 MHz. We can't allocate in that gap, and the current
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We # system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed. # can model gaps later if needed.
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)), Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)), Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
# Tomcat radios # Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio # # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)), Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
@ -114,31 +113,23 @@ RADIOS: List[Radio] = [
# to 400 MHz range, but we can't model gaps with the current implementation. # to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio # https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)), Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps. # Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)), Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
# P-51 / P-47 Radio # P-51 / P-47 Radio
# 4 preset channels (A/B/C/D) # 4 preset channels (A/B/C/D)
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)), Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)), Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)), Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
# MiG-15bis # MiG-15bis
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)), Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
# MiG-19P # MiG-19P
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis # MiG-21bis
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)), Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50 # Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps. # Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)), Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
Radio("R-828", MHz(20), MHz(60), step=kHz(25)), Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
# UH-1H # UH-1H
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)), 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-131", MHz(30), MHz(76), step=kHz(50)),
@ -218,7 +209,8 @@ class RadioRegistry:
# https://github.com/Khopa/dcs_liberation/issues/598 # https://github.com/Khopa/dcs_liberation/issues/598
channel = radio.last_channel channel = radio.last_channel
logging.warning( logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}.") f"No more free channels for {radio.name}. Reusing {channel}."
)
return channel return channel
def alloc_uhf(self) -> RadioFrequency: def alloc_uhf(self) -> RadioFrequency:

View File

@ -25,8 +25,9 @@ class RunwayData:
icls: Optional[int] = None icls: Optional[int] = None
@classmethod @classmethod
def for_airfield(cls, airport: Airport, runway_heading: int, def for_airfield(
runway_name: str) -> RunwayData: cls, airport: Airport, runway_heading: int, runway_name: str
) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield. """Creates RunwayData for the given runway of an airfield.
Args: Args:
@ -56,7 +57,7 @@ class RunwayData:
atc=atc, atc=atc,
tacan=tacan, tacan=tacan,
tacan_callsign=tacan_callsign, tacan_callsign=tacan_callsign,
ils=ils ils=ils,
) )
@classmethod @classmethod

View File

@ -20,15 +20,19 @@ class BoforsGenerator(AirDefenseGroupGenerator):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3) grid_y = random.randint(2, 3)
spacing = random.randint(10,40) spacing = random.randint(10, 40)
index = 0 index = 0
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i, AirDefence.AAA_Bofors_40mm,
self.position.y + spacing*j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -37,34 +37,64 @@ class FlakGenerator(AirDefenseGroupGenerator):
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(unit_type, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i + random.randint(1,5), unit_type,
self.position.y + spacing*j + random.randint(1,5), self.heading) "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
if(mixed): if mixed:
unit_type = random.choice(GFLAK) unit_type = random.choice(GFLAK)
# Search lights # Search lights
search_pos = self.get_circular_position(random.randint(2,3), 80) search_pos = self.get_circular_position(random.randint(2, 3), 80)
for index, pos in enumerate(search_pos): for index, pos in enumerate(search_pos):
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading) self.add_unit(
AirDefence.Flak_Searchlight_37,
"SearchLight#" + str(index),
pos[0],
pos[1],
self.heading,
)
# Support # Support
self.add_unit(AirDefence.Maschinensatz_33, "MC33#", self.position.x-20, self.position.y-20, self.heading) self.add_unit(
self.add_unit(AirDefence.AAA_Kdo_G_40, "KDO#", self.position.x - 25, self.position.y - 20, AirDefence.Maschinensatz_33,
self.heading) "MC33#",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
self.add_unit(
AirDefence.AAA_Kdo_G_40,
"KDO#",
self.position.x - 25,
self.position.y - 20,
self.heading,
)
# Commander # Commander
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#", self.position.x - 35, self.position.y - 20, self.add_unit(
self.heading) Unarmed.Kübelwagen_82,
"Kubel#",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Some Opel Blitz trucks # Some Opel Blitz trucks
for i in range(int(max(1,grid_x/2))): for i in range(int(max(1, grid_x / 2))):
for j in range(int(max(1,grid_x/2))): for j in range(int(max(1, grid_x / 2))):
self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index), self.add_unit(
self.position.x + 125 + 15*i + random.randint(1,5), Unarmed.Blitz_3_6_6700A,
self.position.y + 15*j + random.randint(1,5), 75) "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5),
75,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -24,12 +24,22 @@ class Flak18Generator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "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 # Add a commander truck
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading) self.add_unit(
Unarmed.Blitz_3_6_6700A,
"Blitz#",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -19,15 +19,25 @@ class KS19Generator(AirDefenseGroupGenerator):
spacing = random.randint(10, 40) spacing = random.randint(10, 40)
self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading) self.add_unit(
highdigitsams.AAA_SON_9_Fire_Can,
"TR",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
index = 0 index = 0
for i in range(3): for i in range(3):
for j in range(3): for j in range(3):
index = index + 1 index = index + 1
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i, highdigitsams.AAA_100mm_KS_19,
self.position.y + spacing * j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -20,21 +20,63 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
positions = self.get_circular_position(4, launcher_distance=30, coverage=360) positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2]) 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=60, coverage=360) positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2]) 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=90, coverage=360) positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_M45_Quadmount,
"AA#" + str(12 + i),
position[0],
position[1],
position[2],
)
# Add a commander truck # 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(
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360)) Unarmed.Willys_MB,
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360)) "CMD#1",
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360)) 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),
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -16,9 +16,17 @@ class ZSU57Generator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = 5 num_launchers = 5
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=360
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_ZSU_57_2, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.AAA_ZSU_57_2,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -20,15 +20,19 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3) grid_y = random.randint(2, 3)
spacing = random.randint(10,40) spacing = random.randint(10, 40)
index = 0 index = 0
for i in range(grid_x): for i in range(grid_x):
for j in range(grid_y): for j in range(grid_y):
index = index+1 index = index + 1
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index), self.add_unit(
self.position.x + spacing*i, AirDefence.AAA_ZU_23_Insurgent_Closed,
self.position.y + spacing*j, self.heading) "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -28,8 +28,9 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
self.auxiliary_groups: List[VehicleGroup] = [] self.auxiliary_groups: List[VehicleGroup] = []
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup: def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
group = VehicleGroup(self.game.next_group_id(), group = VehicleGroup(
"|".join([self.go.group_name, name_suffix])) self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
)
self.auxiliary_groups.append(group) self.auxiliary_groups.append(group)
return group return group
@ -37,7 +38,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
raise RuntimeError( raise RuntimeError(
"Deprecated call to AirDefenseGroupGenerator.get_generated_group " "Deprecated call to AirDefenseGroupGenerator.get_generated_group "
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups " "misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
"instead.") "instead."
)
@property @property
def groups(self) -> Iterator[VehicleGroup]: def groups(self) -> Iterator[VehicleGroup]:

View File

@ -29,18 +29,38 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "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 # Short range guns
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1", self.add_unit(
self.position.x - 40, self.position.y - 40, self.heading + 180), AirDefence.AAA_Bofors_40mm,
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2", "SHO#1",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x - 40,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_Bofors_40mm,
"SHO#2",
self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40,
self.heading,
),
# Add a truck # Add a truck
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading) self.add_unit(
Unarmed.Transport_KAMAZ_43101,
"Truck#",
self.position.x - 60,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
@ -66,18 +86,38 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
for i in range(3): for i in range(3):
for j in range(2): for j in range(2):
index = index + 1 index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index), self.add_unit(
self.position.x + spacing * i + random.randint(1, 5), AirDefence.AAA_8_8cm_Flak_18,
self.position.y + spacing * j + random.randint(1, 5), self.heading) "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 # Short range guns
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1", self.add_unit(
self.position.x - 40, self.position.y - 40, self.heading + 180), AirDefence.AAA_ZU_23_Closed,
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2", "SHO#1",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x - 40,
self.position.y - 40,
self.heading + 180,
),
self.add_unit(
AirDefence.AAA_ZU_23_Closed,
"SHO#2",
self.position.x + spacing * 2 + 40,
self.position.y + spacing + 40,
self.heading,
),
# Add a P19 Radar for EWR # 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) self.add_unit(
AirDefence.SAM_SR_P_19,
"SR#0",
self.position.x - 60,
self.position.y - 20,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -10,8 +10,9 @@ class EwrGenerator(GroupGenerator):
raise NotImplementedError raise NotImplementedError
def generate(self) -> None: def generate(self) -> None:
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y, self.add_unit(
self.heading) self.unit_type, "EWR", self.position.x, self.position.y, self.heading
)
class BoxSpringGenerator(EwrGenerator): class BoxSpringGenerator(EwrGenerator):

View File

@ -17,27 +17,93 @@ class FreyaGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
# TODO : would be better with the Concrete structure that is supposed to protect it # 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) 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) positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2]) 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) positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
for i, position in enumerate(positions): 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]) self.add_unit(
AirDefence.AAA_8_8cm_Flak_18,
"AA#" + str(4 + i),
position[0],
position[1],
position[2],
)
# Command/Logi # Command/Logi
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading) self.add_unit(
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading) Unarmed.Kübelwagen_82,
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading) "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 # 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(
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading) AirDefence.Maschinensatz_33,
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading) "Energy#1",
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading) self.position.x + 20,
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45) 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,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -23,14 +23,12 @@ if TYPE_CHECKING:
# care about in the format we want if we just generate our own group description # care about in the format we want if we just generate our own group description
# types rather than pydcs groups. # types rather than pydcs groups.
class GroupGenerator: class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None: def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0) wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True wp.ETA_locked = True
@ -40,16 +38,27 @@ class GroupGenerator:
def get_generated_group(self) -> unitgroup.VehicleGroup: def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg return self.vg
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float, def add_unit(
pos_y: float, heading: int) -> Vehicle: self,
return self.add_unit_to_group(self.vg, unit_type, name, unit_type: Type[VehicleType],
Point(pos_x, pos_y), heading) name: str,
pos_x: float,
pos_y: float,
heading: int,
) -> Vehicle:
return self.add_unit_to_group(
self.vg, unit_type, name, Point(pos_x, pos_y), heading
)
def add_unit_to_group(self, group: unitgroup.VehicleGroup, def add_unit_to_group(
unit_type: Type[VehicleType], name: str, self,
position: Point, heading: int) -> Vehicle: group: unitgroup.VehicleGroup,
unit = Vehicle(self.game.next_unit_id(), unit_type: Type[VehicleType],
f"{group.name}|{name}", unit_type.id) name: str,
position: Point,
heading: int,
) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
unit.position = position unit.position = position
unit.heading = heading unit.heading = heading
group.add_unit(unit) group.add_unit(unit)
@ -82,31 +91,36 @@ class GroupGenerator:
current_offset = self.heading current_offset = self.heading
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1) current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for x in range(1, num_units + 1): for x in range(1, num_units + 1):
positions.append(( positions.append(
self.position.x + launcher_distance * math.cos(math.radians(current_offset)), (
self.position.y + launcher_distance * math.sin(math.radians(current_offset)), self.position.x
current_offset, + launcher_distance * math.cos(math.radians(current_offset)),
)) self.position.y
+ launcher_distance * math.sin(math.radians(current_offset)),
current_offset,
)
)
current_offset += outer_offset current_offset += outer_offset
return positions return positions
class ShipGroupGenerator(GroupGenerator): class ShipGroupGenerator(GroupGenerator):
"""Abstract class for other ship generator classes""" """Abstract class for other ship generator classes"""
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.faction = faction self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0) wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship: def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
unit = Ship(self.game.next_unit_id(), unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x unit.position.x = pos_x
unit.position.y = pos_y unit.position.y = pos_y
unit.heading = heading unit.heading = heading

View File

@ -19,10 +19,24 @@ class AvengerGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = random.randint(2, 3) num_launchers = random.randint(2, 3)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading) self.add_unit(
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) Unarmed.Transport_M818,
"TRUCK",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Avenger_M1097,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -19,10 +19,24 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = random.randint(2, 4) num_launchers = random.randint(2, 4)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading) self.add_unit(
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) Unarmed.Transport_M818,
"TRUCK",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Chaparral_M48,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -17,10 +17,28 @@ class GepardGenerator(AirDefenseGroupGenerator):
price = 50 price = 50
def generate(self): def generate(self):
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading) self.add_unit(
AirDefence.SPAAA_Gepard,
"SPAAA",
self.position.x,
self.position.y,
self.heading,
)
if random.randint(0, 1) == 1: if random.randint(0, 1) == 1:
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA2", self.position.x, self.position.y, self.heading) self.add_unit(
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading) AirDefence.SPAAA_Gepard,
"SPAAA2",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
Unarmed.Transport_M818,
"TRUCK",
self.position.x + 80,
self.position.y,
self.heading,
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -49,7 +49,12 @@ from gen.sam.sam_roland import RolandGenerator
from gen.sam.sam_sa10 import ( from gen.sam.sam_sa10 import (
SA10Generator, SA10Generator,
Tier2SA10Generator, Tier2SA10Generator,
Tier3SA10Generator, SA10BGenerator, SA12Generator, SA20Generator, SA20BGenerator, SA23Generator, Tier3SA10Generator,
SA10BGenerator,
SA12Generator,
SA20Generator,
SA20BGenerator,
SA23Generator,
) )
from gen.sam.sam_sa11 import SA11Generator from gen.sam.sam_sa11 import SA11Generator
from gen.sam.sam_sa13 import SA13Generator from gen.sam.sam_sa13 import SA13Generator
@ -103,7 +108,6 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
"FreyaGenerator": FreyaGenerator, "FreyaGenerator": FreyaGenerator,
"AllyWW2FlakGenerator": AllyWW2FlakGenerator, "AllyWW2FlakGenerator": AllyWW2FlakGenerator,
"ZSU57Generator": ZSU57Generator, "ZSU57Generator": ZSU57Generator,
"KS19Generator": KS19Generator, "KS19Generator": KS19Generator,
"SA10BGenerator": SA10BGenerator, "SA10BGenerator": SA10BGenerator,
"SA12Generator": SA12Generator, "SA12Generator": SA12Generator,
@ -145,7 +149,7 @@ SAM_PRICES = {
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 30, AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 30,
AirDefence.SAM_SA_15_Tor_9A331: 40, AirDefence.SAM_SA_15_Tor_9A331: 40,
AirDefence.SAM_SA_19_Tunguska_2S6: 35, AirDefence.SAM_SA_19_Tunguska_2S6: 35,
AirDefence.HQ_7_Self_Propelled_LN: 35 AirDefence.HQ_7_Self_Propelled_LN: 35,
} }
EWR_MAP = { EWR_MAP = {
@ -163,7 +167,8 @@ EWR_MAP = {
def get_faction_possible_sams_generator( def get_faction_possible_sams_generator(
faction: Faction) -> List[Type[AirDefenseGroupGenerator]]: faction: Faction,
) -> List[Type[AirDefenseGroupGenerator]]:
""" """
Return the list of possible SAM generator for the given faction Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for :param faction: Faction name to search units for
@ -180,8 +185,10 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene
def _generate_anti_air_from( def _generate_anti_air_from(
generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game, generators: Sequence[Type[AirDefenseGroupGenerator]],
ground_object: SamGroundObject) -> List[VehicleGroup]: game: Game,
ground_object: SamGroundObject,
) -> List[VehicleGroup]:
if not generators: if not generators:
return [] return []
sam_generator_class = random.choice(generators) sam_generator_class = random.choice(generators)
@ -191,8 +198,10 @@ def _generate_anti_air_from(
def generate_anti_air_group( def generate_anti_air_group(
game: Game, ground_object: SamGroundObject, faction: Faction, game: Game,
ranges: Optional[Iterable[Set[AirDefenseRange]]] = None ground_object: SamGroundObject,
faction: Faction,
ranges: Optional[Iterable[Set[AirDefenseRange]]] = None,
) -> List[VehicleGroup]: ) -> List[VehicleGroup]:
""" """
This generate a SAM group This generate a SAM group
@ -213,24 +222,25 @@ def generate_anti_air_group(
""" """
generators = get_faction_possible_sams_generator(faction) generators = get_faction_possible_sams_generator(faction)
if ranges is None: if ranges is None:
ranges = [{ ranges = [
AirDefenseRange.Long, {
AirDefenseRange.Medium, AirDefenseRange.Long,
AirDefenseRange.Short, AirDefenseRange.Medium,
}] AirDefenseRange.Short,
}
]
for range_options in ranges: for range_options in ranges:
generators_for_range = [g for g in generators if generators_for_range = [g for g in generators if g.range() in range_options]
g.range() in range_options] groups = _generate_anti_air_from(generators_for_range, game, ground_object)
groups = _generate_anti_air_from(generators_for_range, game,
ground_object)
if groups: if groups:
return groups return groups
return [] return []
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject, def generate_ewr_group(
faction: Faction) -> Optional[VehicleGroup]: game: Game, ground_object: TheaterGroundObject, faction: Faction
) -> Optional[VehicleGroup]:
"""Generates an early warning radar group. """Generates an early warning radar group.
:param game: The Game. :param game: The Game.

View File

@ -18,20 +18,51 @@ class HawkGenerator(AirDefenseGroupGenerator):
price = 115 price = 115
def generate(self): def generate(self):
self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading) self.add_unit(
self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading) AirDefence.SAM_Hawk_SR_AN_MPQ_50,
self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading) "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 # Triple A for close range defense
aa_group = self.add_auxiliary_group("AA") aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA", self.add_unit_to_group(
self.position + Point(20, 30), self.heading) aa_group,
AirDefence.AAA_Vulcan_M163,
"AAA",
self.position + Point(20, 30),
self.heading,
)
num_launchers = random.randint(3, 6) num_launchers = random.randint(3, 6)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Hawk_LN_M192, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Hawk_LN_M192,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -18,23 +18,51 @@ class HQ7Generator(AirDefenseGroupGenerator):
price = 120 price = 120
def generate(self): def generate(self):
self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading) self.add_unit(
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading) AirDefence.HQ_7_Self_Propelled_STR,
"STR",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.HQ_7_Self_Propelled_LN,
"LN",
self.position.x + 20,
self.position.y,
self.heading,
)
# Triple A for close range defense # Triple A for close range defense
aa_group = self.add_auxiliary_group("AA") aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, self.add_unit_to_group(
"AAA1", self.position + Point(20, 30), aa_group,
self.heading) AirDefence.AAA_ZU_23_on_Ural_375,
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, "AAA1",
"AAA2", self.position - Point(20, 30), self.position + Point(20, 30),
self.heading) self.heading,
)
self.add_unit_to_group(
aa_group,
AirDefence.AAA_ZU_23_on_Ural_375,
"AAA2",
self.position - Point(20, 30),
self.heading,
)
num_launchers = random.randint(0, 3) num_launchers = random.randint(0, 3)
if num_launchers > 0: if num_launchers > 0:
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.HQ_7_Self_Propelled_LN,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -19,10 +19,24 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
num_launchers = random.randint(2, 4) num_launchers = random.randint(2, 4)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading) self.add_unit(
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) Unarmed.Transport_M818,
"TRUCK",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=180
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Linebacker_M6, "M6#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Linebacker_M6,
"M6#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -19,24 +19,65 @@ class PatriotGenerator(AirDefenseGroupGenerator):
def generate(self): def generate(self):
# Command Post # Command Post
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "STR", self.position.x + 30, self.position.y + 30, self.heading) self.add_unit(
self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading) AirDefence.SAM_Patriot_STR_AN_MPQ_53,
self.add_unit(AirDefence.SAM_Patriot_ECS_AN_MSQ_104, "MSQ", self.position.x + 30, self.position.y, self.heading) "STR",
self.add_unit(AirDefence.SAM_Patriot_ICC, "ICC", self.position.x + 60, self.position.y, self.heading) self.position.x + 30,
self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading) 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,
)
num_launchers = random.randint(3, 4) num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.SAM_Patriot_LN_M901,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
# Short range protection for high value site # Short range protection for high value site
aa_group = self.add_auxiliary_group("AA") aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(3, 4) num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=200, coverage=360
)
for i, (x, y, heading) in enumerate(positions): for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, self.add_unit_to_group(
f"SPAAA#{i}", Point(x, y), heading) aa_group, AirDefence.AAA_Vulcan_M163, f"SPAAA#{i}", Point(x, y), heading
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

View File

@ -17,14 +17,34 @@ class RapierGenerator(AirDefenseGroupGenerator):
price = 50 price = 50
def generate(self): def generate(self):
self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading) self.add_unit(
self.add_unit(AirDefence.Rapier_FSA_Optical_Tracker, "OT", self.position.x + 20, self.position.y, self.heading) AirDefence.Rapier_FSA_Blindfire_Tracker,
"BT",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.Rapier_FSA_Optical_Tracker,
"OT",
self.position.x + 20,
self.position.y,
self.heading,
)
num_launchers = random.randint(3, 6) num_launchers = random.randint(3, 6)
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=240) positions = self.get_circular_position(
num_launchers, launcher_distance=80, coverage=240
)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(
AirDefence.Rapier_FSA_Launcher,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:

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