Ran black reformater on 2.4x branch

This commit is contained in:
Khopa 2021-02-22 20:55:51 +01:00
parent c2310453d8
commit 523ef08697
204 changed files with 6744 additions and 4013 deletions

View File

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

View File

@ -1,17 +1,70 @@
import inspect
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_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
WW2_FREE = ["fuel", "factory", "ware", "fob"]
WW2_GERMANY_BUILDINGS = [
"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',
'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_BUILDINGS = [
"Siegfried Line",
"Concertina wire",
"Concertina Wire",
"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_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
FORTIFICATION_UNITS = [
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_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW
SpitfireLFMkIXCW,
)
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
"""
GUNFIGHTERS = [
# Cold War
MiG_15bis,
MiG_19P,
@ -34,11 +33,9 @@ GUNFIGHTERS = [
F_86F_Sabre,
A_4E_C,
F_5E_3,
# Trainers
C_101CC,
L_39ZA,
# WW2
P_51D_30_NA,
P_51D,
@ -51,5 +48,4 @@ GUNFIGHTERS = [
FW_190D9,
FW_190A8,
I_16,
]
]

View File

@ -23,7 +23,6 @@ from dcs.ships import (
from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [
# Radars
AirDefence.SAM_SA_15_Tor_9A331,
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_2_TR_SNR_75_Fan_Song,
AirDefence.HQ_7_Self_Propelled_STR,
# Ships
CVN_70_Carl_Vinson,
Oliver_Hazzard_Perry_class,
@ -70,5 +68,5 @@ UNITS_WITH_RADAR = [
LHA_1_Tarawa,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C_Destroyer
]
Type_052C_Destroyer,
]

View File

@ -28,7 +28,8 @@ class Weapon:
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
if introduction_year is None:
logging.warning(
f"No introduction year for {self}, assuming always available")
f"No introduction year for {self}, assuming always available"
)
return True
return date >= datetime.date(introduction_year, 1, 1)
@ -52,7 +53,7 @@ class Weapon:
return cls(
cast(str, weapon_data["clsid"]),
cast(str, weapon_data["name"]),
cast(int, weapon_data["weight"])
cast(int, weapon_data["weight"]),
)
@classmethod
@ -88,8 +89,11 @@ class Pylon:
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name.
pylons = [v for v in aircraft.__dict__.values() if
inspect.isclass(v) and v.__name__.startswith("Pylon")]
pylons = [
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
# values of (pylon number, allowed weapon).
@ -117,112 +121,92 @@ _WEAPON_FALLBACKS = [
(Weapons.ADM_141A_, None),
(Weapons.ADM_141A__, None),
(Weapons.ADM_141B, None),
# AGM-114K Hellfire
(Weapons.AGM114x2_OH_58, Weapons.M260_HYDRA), # assuming OH-58 and not MQ-9
(Weapons.AGM_114K, None), # Only for RQ-1
(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___4, Weapons.LAU_61___19_2_75__rockets_MK151_HE),
# AGM-119 Penguin
(Weapons.AGM_119B_Penguin, Weapons.Mk_82),
# AGM-122 Sidearm
(Weapons.AGM_122, None), # No known aircraft carries this
(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, None), # No known aircraft carries this
(Weapons.AGM_122_Sidearm, Weapons.GBU_12), # outer pylons harrier
(Weapons.AGM_122_Sidearm_, Weapons.LAU_117_AGM_65E), # internal pylons harrier
# AGM-154 JSOW
(Weapons.AGM_154A, Weapons.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_154C, Weapons.GBU_12),
(Weapons.AGM_154C_4, Weapons.GBU_31_8),
(Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12),
# AGM-45 Shrike
(Weapons.AGM_45A, None),
(Weapons.AGM_45B, Weapons.AGM_45A),
(Weapons.AGM_45B_, Weapons.AGM_45A),
# AGM-62 Walleye
(Weapons.AGM_62, Weapons.Mk_84),
# AGM-65 Maverick
(Weapons.AGM_65D, None), # doesn't exist
(Weapons.AGM_65E, None), # doesn't exist
(Weapons.AGM_65K, 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_65D, Weapons.AGM_62), # Walleye is the predecessor to the maverick
(Weapons.AGM_65D, None), # doesn't exist
(Weapons.AGM_65E, None), # doesn't exist
(Weapons.AGM_65K, 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_65D,
Weapons.AGM_62,
), # Walleye is the predecessor to the maverick
(Weapons.LAU_117_AGM_65E, None),
(Weapons.LAU_117_AGM_65F, 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_65K, Weapons.LAU_117_AGM_65D),
(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_3, 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_3, Weapons.LAU_88_AGM_65D_3),
(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_R, Weapons.LAU_88_AGM_65D_2_),
(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_3, Weapons.LAU_88_AGM_65D_3),
# 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_8, Weapons._27_Mk_82),
(Weapons.AGM_84D, Weapons.AGM_62),
(Weapons.AGM_84E, Weapons.LAU_117_AGM_65F),
(Weapons.AGM_84H, Weapons.AGM_84E),
# AGM-86 ALCM
(Weapons.AGM_86C, Weapons._27_Mk_82),
(Weapons.AGM_86C_20, Weapons._27_Mk_82),
(Weapons.AGM_86C_8, Weapons._27_Mk_82),
(Weapons.MER_6_AGM_86C, Weapons.MER_12_Mk_82),
# AGM-88 HARM
(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.LAU_115___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.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),
# AIM-54 Phoenix
(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_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
(Weapons.AIM_7E, None),
(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.LAU_115C_AIM_7E, None),
(Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E),
(Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F),
(Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M),
# AIM-9 Sidewinder
(Weapons.AIM_9L_Sidewinder_IR_AAM, None),
(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_9P_Sidewinder_IR_AAM, Weapons.AIM_9L_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_R, None),
(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_2_AIM_9L, None),
(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_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_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_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_9X, Weapons.LAU_115_LAU_127_AIM_9M),
(Weapons.LAU_127_AIM_9L, None),
(Weapons.LAU_127_AIM_9M, Weapons.LAU_127_AIM_9L),
(Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M),
(Weapons.LAU_138_AIM_9L, None),
(Weapons.LAU_138_AIM_9M, Weapons.LAU_138_AIM_9L),
(Weapons.LAU_7_AIM_9L, None),
(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_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_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_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_9P_Sidewinder_IR_AAM, Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM),
(
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_9P_Sidewinder_IR_AAM,
Weapons.LAU_7___2_AIM_9L_Sidewinder_IR_AAM,
),
# ALQ ECM Pods
(Weapons.ALQ_131, None),
(Weapons.ALQ_184, Weapons.ALQ_131),
(Weapons.AN_ALQ_164_DECM_Pod, None),
# TGP Pods
(Weapons.AN_AAQ_28_LITENING_, None),
(Weapons.AN_AAQ_28_LITENING, Weapons.Lantirn_F_16),
@ -300,17 +283,14 @@ _WEAPON_FALLBACKS = [
(Weapons.Lantirn_F_16, None),
(Weapons.Lantirn_Target_Pod, None),
(Weapons.Pavetack_F_111, None),
# BLU-107
(Weapons.BLU_107, None),
(Weapons.MER_6_BLU_107, Weapons.MER_6_Mk_82),
# GBU-10 LGB
(Weapons.DIS_GBU_10, Weapons.Mk_84),
(Weapons.GBU_10, Weapons.Mk_84),
(Weapons.GBU_10_, Weapons.Mk_84),
(Weapons.GBU_10_2, Weapons.Mk_84),
# GBU-12 LGB
(Weapons.AUF2_GBU_12_x_2, None),
(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._3_GBU_12, Weapons._3_Mk_82_),
# GBU-15 LGB
(Weapons.GBU_15, Weapons.Mk_84),
# GBU-16 LGB
(Weapons.BRU_33___2_x_GBU_16, None),
(Weapons.DIS_GBU_16, Weapons.Mk_83),
(Weapons.GBU_16, Weapons.Mk_83),
(Weapons.GBU_16_, Weapons.Mk_83_),
(Weapons.DIS_GBU_16, Weapons.Mk_83),
(Weapons.GBU_16, Weapons.Mk_83),
(Weapons.GBU_16_, Weapons.Mk_83_),
# GBU-24 LGB
(Weapons.GBU_24, Weapons.GBU_10),
(Weapons.GBU_24_, Weapons.GBU_10_),
(Weapons.GBU_24__, Weapons.GBU_10_),
# GBU-27 LGB
(Weapons.GBU_24, Weapons.GBU_10),
(Weapons.GBU_24_, Weapons.GBU_10_),
(Weapons.GBU_24__, Weapons.GBU_10_),
# GBU-28 LGB
(Weapons.GBU_28, Weapons.GBU_15),
# GBU-31 JDAM
(Weapons.GBU_31V3B_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_3_B, Weapons.GBU_24_),
(Weapons.GBU_31_V_4_B, Weapons.GBU_24_),
# GBU-32 JDAM
(Weapons.GBU_32_V_2_B, Weapons.GBU_16),
# GBU-32 JDAM
(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_16, Weapons.MK_82_28),
(Weapons._2_GBU_38, Weapons._2_Mk_82),
(Weapons._2_GBU_38_, Weapons._2_Mk_82_),
(Weapons._3_GBU_38, Weapons._3_Mk_82_),
# GBU-54 LJDAM
(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._3_GBU_54_V_1_B, Weapons._3_GBU_38),
# CBU-52
(Weapons.CBU_52B, None),
# CBU-87 CEM
(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___3_x_CBU_87, Weapons.TER_9A___3_x_Mk_82),
# CBU-97
(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___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-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),
# 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.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_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_),
(
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_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
# KAB-1500
(Weapons.KAB_1500Kr, None),
(Weapons.KAB_1500LG_Pr, Weapons.KAB_1500Kr),
(Weapons.KAB_1500L, Weapons.KAB_1500LG_Pr),
# KAB-500
(Weapons.KAB_500kr, Weapons.FAB_500_M62),
(Weapons.KAB_500L, Weapons.KAB_500kr),
(Weapons.KAB_500S, Weapons.KAB_500L),
# KH Series
(Weapons.Kh_22N, None),
(Weapons.Kh_23L, None),
(Weapons.Kh_25ML, None),
(Weapons.Kh_25ML_, None),
(Weapons.Kh_25ML__, None),
(Weapons.Kh_25MP, None),
(Weapons.Kh_25MPU, Weapons.Kh_25MP),
(Weapons.Kh_25MR, None),
(Weapons.Kh_25MR_, 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_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_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_6, None),
(Weapons.Kh_41, None),
(Weapons.Kh_58U, Weapons.Kh_31P),
(Weapons.Kh_58U_, Weapons.Kh_31P_),
(Weapons.Kh_59M, Weapons.Kh_31A),
(Weapons.Kh_65, None),
(Weapons.Kh_65_6, None),
(Weapons.Kh_65_8, None),
(Weapons.Kh_66_Grom__21__APU_68, None),
# ECM
(Weapons.L175V_Khibiny_ECM_pod, None),
# R-13
(Weapons.R_13M, None),
(Weapons.R_13M1, Weapons.R_13M),
# R-24
(Weapons.R_24R, None),
(Weapons.R_24T, None),
# R-27
(Weapons.R_27T, Weapons.R_24T),
(Weapons.R_27R, Weapons.R_24R),
(Weapons.R_27ER, Weapons.R_27R),
(Weapons.R_27ET, Weapons.R_27T),
# R-33
(Weapons.R_33, None),
# R-3
(Weapons.R_3S, Weapons.R_13M),
(Weapons.R_3R, Weapons.R_3S),
# R-40
(Weapons.R_40R, None),
(Weapons.R_40T, None),
# R-55
(Weapons.R_55, None),
(Weapons.RS2US, None),
# R-60
(Weapons.R_60, 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.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_x_2, Weapons.R_60M),
(Weapons.R_60M_x_2_, Weapons.R_60M),
# R-73
(Weapons.R_73, Weapons.R_60M),
(Weapons.R_73_, None),
# R-77
(Weapons.R_77, Weapons.R_27ER),
(Weapons.R_77_, None),
# UK
# ALARM
(Weapons.ALARM, None),
(Weapons.ALARM_2, None),
# France
# BLG-66 Belouga
(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),
# HOT-3
(Weapons.HOT3, None),
(Weapons.HOT3_, None),
# Magic 2
(Weapons.Matra_Magic_II, None),
(Weapons.R_550_Magic_2, None),
# Super 530D
(Weapons.Matra_Super_530D, Weapons.Matra_Magic_II),
(Weapons.Super_530D, None)
(Weapons.Super_530D, None),
]
WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict(
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 = {
@ -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_141B): 1987,
# AGM-114K Hellfire
Weapon.from_pydcs(Weapons.AGM114x2_OH_58): 1993,
Weapon.from_pydcs(Weapons.AGM_114K): 1993,
Weapon.from_pydcs(Weapons.AGM_114K___4): 1993,
# AGM-119 Penguin
Weapon.from_pydcs(Weapons.AGM_119B_Penguin): 1972,
# AGM-122 Sidearm
Weapon.from_pydcs(Weapons.AGM_122): 1986,
Weapon.from_pydcs(Weapons.AGM_122_Sidearm): 1986,
Weapon.from_pydcs(Weapons.AGM_122_Sidearm_): 1986,
# AGM-154 JSOW
Weapon.from_pydcs(Weapons.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.AGM_154B): 2005,
Weapon.from_pydcs(Weapons.AGM_154C): 2005,
Weapon.from_pydcs(Weapons.AGM_154C_4): 2005,
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005,
# AGM-45 Shrike
Weapon.from_pydcs(Weapons.AGM_45A): 1965,
Weapon.from_pydcs(Weapons.AGM_45B): 1970,
Weapon.from_pydcs(Weapons.AGM_45B_): 1970,
# AGM-62 Walleye
Weapon.from_pydcs(Weapons.AGM_62): 1972,
# AGM-65 Maverick
Weapon.from_pydcs(Weapons.AGM_65D): 1983,
Weapon.from_pydcs(Weapons.AGM_65E): 1985,
Weapon.from_pydcs(Weapons.AGM_65K): 2007,
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_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_65K): 2002,
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_3): 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_3): 1985,
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_R): 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_3): 2007,
# AGM-84 Harpoon
Weapon.from_pydcs(Weapons.AGM_84): 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_SLAM): 1990,
Weapon.from_pydcs(Weapons.AGM_84H): 1998,
# AGM-86 ALCM
Weapon.from_pydcs(Weapons.AGM_86C): 1986,
Weapon.from_pydcs(Weapons.AGM_86C_20): 1986,
Weapon.from_pydcs(Weapons.AGM_86C_8): 1986,
Weapon.from_pydcs(Weapons.MER_6_AGM_86C): 1986,
# AGM-88 HARM
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.
# AIM-120 AMRAAM
Weapon.from_pydcs(Weapons.AIM_120B): 1994,
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___AIM_120B): 1994,
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996,
Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996,
# 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_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_Mk47): 1974,
Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1974,
Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1974,
# AIM-7 Sparrow
Weapon.from_pydcs(Weapons.AIM_7E): 1963,
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.LAU_115C_AIM_7E): 1963,
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976,
Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982,
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987,
# AIM-9 Sidewinder
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_9P5_Sidewinder_IR_AAM): 1980,
Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978,
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_R): 1977,
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_2_AIM_9L): 1977,
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_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_9M): 1982,
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_9M): 1982,
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_9M): 1982,
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_9M): 1982,
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_Sidewinder_IR_AAM): 1982,
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_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_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_9P_Sidewinder_IR_AAM): 1978,
# ALQ ECM Pods
Weapon.from_pydcs(Weapons.ALQ_131): 1970,
Weapon.from_pydcs(Weapons.ALQ_184): 1989,
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
# TGP Pods
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_Target_Pod): 1985,
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
# BLU-107
Weapon.from_pydcs(Weapons.BLU_107): 1983,
Weapon.from_pydcs(Weapons.MER_6_BLU_107): 1983,
# GBU-10 LGB
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_2): 1976,
# GBU-12 LGB
Weapon.from_pydcs(Weapons.AUF2_GBU_12_x_2): 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._3_GBU_12): 1976,
# GBU-15 LGB
Weapon.from_pydcs(Weapons.GBU_15): 1975,
# GBU-16 LGB
Weapon.from_pydcs(Weapons.BRU_33___2_x_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._3_GBU_16): 1976,
Weapon.from_pydcs(Weapons._3_GBU_16_): 1976,
# GBU-24 LGB
Weapon.from_pydcs(Weapons.GBU_24): 1986,
Weapon.from_pydcs(Weapons.GBU_24_): 1986,
Weapon.from_pydcs(Weapons.GBU_24__): 1986,
# GBU-27 LGB
Weapon.from_pydcs(Weapons.GBU_27): 1991,
Weapon.from_pydcs(Weapons.GBU_27_2): 1991,
Weapon.from_pydcs(Weapons.GBU_27_4): 1991,
# GBU-28
Weapon.from_pydcs(Weapons.GBU_28): 1991,
# GBU-31 JDAM
Weapon.from_pydcs(Weapons.GBU_31V3B_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_3_B): 2001,
Weapon.from_pydcs(Weapons.GBU_31_V_4_B): 2001,
# GBU-32 JDAM
Weapon.from_pydcs(Weapons.GBU_32_V_2_B): 2002,
# GBU-38 JDAM
Weapon.from_pydcs(Weapons.BRU_55___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._3_GBU_38): 2005,
# GBU-54 LJDAM
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._3_GBU_54_V_1_B): 2008,
# CBU-52
Weapon.from_pydcs(Weapons.CBU_52B): 1970,
# CBU-87 CEM
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___3_x_CBU_87): 1986,
# CBU-97
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___3_x_CBU_97): 1992,
# CBU-99
Weapon.from_pydcs(Weapons.BRU_33___2_x_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.DIS_MK_20): 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.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_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.Mk_20): 1968,
Weapon.from_pydcs(Weapons.Mk_20_): 1968,
Weapon.from_pydcs(Weapons.Mk_20_18): 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,
@ -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._3_Mk_20_Rockeye): 1968,
Weapon.from_pydcs(Weapons._3_Mk_20_Rockeye_): 1968,
# CBU-103
Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_103): 2000,
Weapon.from_pydcs(Weapons.CBU_103): 2000,
# CBU-105
Weapon.from_pydcs(Weapons.BRU_57___2_x_CBU_105): 2000,
Weapon.from_pydcs(Weapons.CBU_105): 2000,
# APKWS
Weapon.from_pydcs(Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_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,
Weapon.from_pydcs(
Weapons.LAU_131_pod___7_x_2_75__Hydra___Laser_Guided_Rkts_M151___HE_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
# KAB-1500
Weapon.from_pydcs(Weapons.KAB_1500Kr): 1985,
Weapon.from_pydcs(Weapons.KAB_1500L): 1995,
Weapon.from_pydcs(Weapons.KAB_1500LG_Pr): 1990,
# KAB-500
Weapon.from_pydcs(Weapons.KAB_500kr): 1980,
Weapon.from_pydcs(Weapons.KAB_500L): 1995,
Weapon.from_pydcs(Weapons.KAB_500S): 2000,
# Kh Series
Weapon.from_pydcs(Weapons.Kh_22N): 1962,
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_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_25MR): 1975,
Weapon.from_pydcs(Weapons.Kh_25MR_): 1975,
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_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_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_6): 2003,
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_59M): 1990,
Weapon.from_pydcs(Weapons.Kh_65): 1992,
Weapon.from_pydcs(Weapons.Kh_65_6): 1992,
Weapon.from_pydcs(Weapons.Kh_65_8): 1992,
Weapon.from_pydcs(Weapons.Kh_66_Grom__21__APU_68): 1968,
# ECM
Weapon.from_pydcs(Weapons.L175V_Khibiny_ECM_pod): 1982,
# R-13
Weapon.from_pydcs(Weapons.R_13M): 1961,
Weapon.from_pydcs(Weapons.R_13M1): 1965,
# R-24
Weapon.from_pydcs(Weapons.R_24R): 1981,
Weapon.from_pydcs(Weapons.R_24T): 1981,
# R-27
Weapon.from_pydcs(Weapons.R_27ER): 1983,
Weapon.from_pydcs(Weapons.R_27ET): 1986,
Weapon.from_pydcs(Weapons.R_27R): 1983,
Weapon.from_pydcs(Weapons.R_27T): 1983,
# R-33
Weapon.from_pydcs(Weapons.R_33): 1981,
# R-3
Weapon.from_pydcs(Weapons.R_3R): 1966,
Weapon.from_pydcs(Weapons.R_3S): 1962,
# R-40
Weapon.from_pydcs(Weapons.R_40R): 1976,
Weapon.from_pydcs(Weapons.R_40T): 1976,
# R-55
Weapon.from_pydcs(Weapons.R_55): 1957,
Weapon.from_pydcs(Weapons.RS2US): 1957,
# R-60
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.APU_60_1_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_x_2): 1982,
Weapon.from_pydcs(Weapons.R_60M_x_2_): 1982,
# R-73
Weapon.from_pydcs(Weapons.R_73): 1984,
Weapon.from_pydcs(Weapons.R_73_): 1984,
# R-77
Weapon.from_pydcs(Weapons.R_77): 2002,
Weapon.from_pydcs(Weapons.R_77_): 2002,
# UK
# ALARM
Weapon.from_pydcs(Weapons.ALARM): 1990,
Weapon.from_pydcs(Weapons.ALARM_2): 1990,
# France
# BLG-66 Belouga
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,
# HOT-3
Weapon.from_pydcs(Weapons.HOT3): 1998,
Weapon.from_pydcs(Weapons.HOT3_): 1998,
# Magic 2
Weapon.from_pydcs(Weapons.Matra_Magic_II): 1986,
Weapon.from_pydcs(Weapons.R_550_Magic_2): 1986,
# Super 530D
Weapon.from_pydcs(Weapons.Matra_Super_530D): 1988,
Weapon.from_pydcs(Weapons.Super_530D): 1988,
}

View File

@ -25,6 +25,7 @@ from dcs.helicopters import (
helicopter_map,
)
from dcs.mapping import Point
# mypy can't resolve these if they're wildcard imports for some reason.
from dcs.planes import (
AJS37,
@ -112,7 +113,7 @@ from dcs.planes import (
WingLoong_I,
Yak_40,
plane_map,
I_16
I_16,
)
from dcs.ships import (
Armed_speedboat,
@ -166,6 +167,7 @@ from dcs.vehicles import (
import pydcs_extensions.frenchpack.frenchpack as frenchpack
import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams
# PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader
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_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[highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
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_SA_10B_S_300PS_54K6_CP.id
] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id
] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id
] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
vehicle_map[
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id
] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE
vehicle_map[
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id
] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id
] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id
] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id
] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
vehicle_map[
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id
] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id
] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
vehicle_map[
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id
] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
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_SA_3__V_601P__LN_5P73.id] = 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.SAM_SA_3__V_601P__LN_5P73.id
] = 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._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
@ -308,7 +386,6 @@ PRICES = {
JF_17: 20,
Su_30: 24,
Su_57: 40,
SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14,
I_16: 10,
@ -317,7 +394,6 @@ PRICES = {
FW_190A8: 14,
A_20G: 22,
Ju_88A4: 24,
F_5E_3: 8,
MiG_15bis: 4,
MiG_19P: 6,
@ -328,7 +404,6 @@ PRICES = {
C_101CC: 6,
A_4E_C: 8,
MB_339PAN: 6,
AV8BNA: 14,
M_2000C: 16,
Mirage_2000_5: 20,
@ -342,7 +417,6 @@ PRICES = {
F_22A: 40,
Tornado_IDS: 20,
Tornado_GR4: 20,
# bomber
Su_17M4: 10,
Su_25: 15,
@ -352,12 +426,10 @@ PRICES = {
Su_24M: 20,
Su_24MR: 24,
MiG_27K: 20,
A_10A: 16,
A_10C: 22,
A_10C_2: 24,
S_3B: 10,
# heli
Ka_50: 13,
SA342M: 8,
@ -373,7 +445,6 @@ PRICES = {
AH_64D: 30,
OH_58D: 6,
SH_60B: 6,
# Bombers
B_52H: 35,
B_1B: 50,
@ -382,7 +453,6 @@ PRICES = {
Tu_22M3: 40,
Tu_95MS: 35,
F_111F: 21,
# special
IL_76MD: 30,
An_26B: 25,
@ -393,14 +463,12 @@ PRICES = {
KC_135: 25,
KC130: 25,
KC135MPRS: 25,
A_50: 50,
KJ_2000: 50,
E_3A: 50,
E_2C: 50,
C_130: 25,
Hercules: 25,
# WW2
P_51D_30_NA: 18,
P_51D: 16,
@ -408,17 +476,14 @@ PRICES = {
P_47D_30bl1: 16,
P_47D_40: 18,
B_17G: 30,
# Drones
MQ_9_Reaper: 12,
RQ_1A_Predator: 6,
WingLoong_I: 6,
# Modded
Rafale_M: 26,
Rafale_A_S: 26,
Rafale_B: 26,
# armor
Armor.APC_MTLB: 4,
Armor.FDDM_Grad: 4,
@ -437,7 +502,6 @@ PRICES = {
Armor.IFV_BMP_3: 18,
Armor.ZBD_04A: 12,
Armor.ZTZ_96B: 30,
Armor.APC_Cobra: 4,
Armor.APC_M113: 6,
Armor.APC_M1043_HMMWV_Armament: 2,
@ -457,10 +521,8 @@ PRICES = {
Armor.IFV_Marder: 10,
Armor.IFV_MCV_80: 10,
Armor.IFV_LAV_25: 7,
Artillery.MLRS_M270: 55,
Artillery.SPH_M109_Paladin: 25,
Artillery.SPH_2S9_Nona: 12,
Artillery.SPH_2S1_Gvozdika: 18,
Artillery.SPH_2S3_Akatsia: 24,
@ -470,14 +532,11 @@ PRICES = {
Artillery.MLRS_9A52_Smerch: 40,
Artillery._2B11_mortar: 4,
Artillery.SpGH_Dana: 26,
Unarmed.Transport_UAZ_469: 3,
Unarmed.Transport_Ural_375: 3,
Infantry.Infantry_M4: 1,
Infantry.Soldier_AK: 1,
Unarmed.Transport_M818: 3,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
Armor.MT_Pz_Kpfw_IV_Ausf_H: 16,
@ -504,22 +563,18 @@ PRICES = {
Armor.Daimler_Armoured_Car: 8,
Armor.LT_Mk_VII_Tetrarch: 8,
Armor.M4_Tractor: 2,
# ship
CV_1143_5_Admiral_Kuznetsov: 100,
CVN_74_John_C__Stennis: 100,
LHA_1_Tarawa: 50,
Bulk_cargo_ship_Yakushev: 10,
Armed_speedboat: 10,
Dry_cargo_ship_Ivanov: 10,
Tanker_Elnya_160: 10,
# Air Defence units
AirDefence.SAM_SA_19_Tunguska_2S6: 30,
AirDefence.SAM_SA_6_Kub_LN_2P25: 20,
AirDefence.SAM_SA_3_S_125_LN_5P73: 6,
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
@ -558,7 +613,6 @@ PRICES = {
AirDefence.SAM_SA_18_Igla_S_comm: 8,
AirDefence.EWR_1L13: 30,
AirDefence.SAM_SA_6_Kub_STR_9S91: 22,
AirDefence.EWR_55G6: 30,
AirDefence.CP_9S80M1_Sborka: 10,
AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14,
@ -589,7 +643,6 @@ PRICES = {
AirDefence.AAA_M1_37mm: 7,
AirDefence.AAA_M45_Quadmount: 4,
AirDefence.AA_gun_QF_3_7: 10,
# FRENCH PACK MOD
frenchpack.AMX_10RCR: 10,
frenchpack.AMX_10RCR_SEPAR: 12,
@ -619,7 +672,6 @@ PRICES = {
frenchpack.DIM__TOYOTA_GREEN: 2,
frenchpack.DIM__TOYOTA_DESERT: 2,
frenchpack.DIM__KAMIKAZE: 6,
# SA-10
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
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_LN_5P85C: 22,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
# High digit sams mod
highdigitsams.AAA_SON_9_Fire_Can: 8,
highdigitsams.AAA_100mm_KS_19: 10,
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20,
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_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_40B6MD_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_9A82_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_9S19_SR: 34,
highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28,
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_truck: 32,
@ -656,21 +704,17 @@ PRICES = {
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_5P85DE: 28,
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_SR_64N6E2: 40,
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 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_9S19M2_SR: 45,
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35,
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32,
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32,
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40,
}
"""
@ -728,7 +772,7 @@ UNIT_BY_TASK = {
SpitfireLFMkIX,
A_4E_C,
Rafale_M,
SA342Mistral
SA342Mistral,
],
CAS: [
AH_1W,
@ -779,18 +823,12 @@ UNIT_BY_TASK = {
Tu_160,
Tu_22M3,
Tu_95MS,
UH_1H,
UH_1H,
SH_60B,
WingLoong_I,
Hercules
],
Transport: [
IL_76MD,
An_26B,
An_30M,
Yak_40,
C_130
Hercules,
],
Transport: [IL_76MD, An_26B, An_30M, Yak_40, C_130],
Refueling: [
IL_78M,
KC_135,
@ -798,12 +836,7 @@ UNIT_BY_TASK = {
S_3B_Tanker,
KC135MPRS,
],
AWACS: [
E_3A,
E_2C,
A_50,
KJ_2000
],
AWACS: [E_3A, E_2C, A_50, KJ_2000],
PinpointStrike: [
Armor.APC_MTLB,
Armor.APC_MTLB,
@ -851,7 +884,6 @@ UNIT_BY_TASK = {
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.ZTZ_96B,
Armor.APC_Cobra,
Armor.APC_Cobra,
Armor.APC_Cobra,
@ -895,7 +927,6 @@ UNIT_BY_TASK = {
Armor.MBT_Leopard_2,
Armor.MBT_Challenger_II,
Armor.MBT_Merkava_Mk__4,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
@ -945,7 +976,6 @@ UNIT_BY_TASK = {
Artillery.Sturmpanzer_IV_Brummbär,
Armor.Daimler_Armoured_Car,
Armor.LT_Mk_VII_Tetrarch,
Artillery.MLRS_M270,
Artillery.SPH_M109_Paladin,
Artillery.SPH_2S9_Nona,
@ -959,7 +989,6 @@ UNIT_BY_TASK = {
Artillery.SpGH_Dana,
Artillery.M12_GMC,
Artillery.Sturmpanzer_IV_Brummbär,
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZSU_57_2,
@ -983,12 +1012,10 @@ UNIT_BY_TASK = {
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7,
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
frenchpack.ERC_90,
@ -1006,15 +1033,29 @@ UNIT_BY_TASK = {
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
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: [],
Carriage: [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, ]
Carriage: [
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 = [
AirDefence.SAM_Linebacker_M6,
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_8_Osa_9A33,
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_SR_AN_MPQ_50: 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
# model, we can safely assume the other was deployed
# 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_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
"""
TAKEOFF_BAN: List[Type[FlyingType]] = [
]
TAKEOFF_BAN: List[Type[FlyingType]] = []
"""
Units that will be always spawned in the air if launched from the carrier
@ -1117,7 +1156,7 @@ COMMON_OVERRIDE = {
GroundAttack: "STRIKE",
Escort: "CAP",
RunwayAttack: "RUNWAY_ATTACK",
FighterSweep: "CAP"
FighterSweep: "CAP",
}
"""
@ -1134,19 +1173,18 @@ EXPANDED_TASK_PAYLOAD_OVERRIDE = {
"BARCAP": ("CAP HEAVY", "CAP"),
"CAS": ("CAS MAVERICK F", "CAS"),
"INTERCEPTION": ("CAP HEAVY", "CAP"),
"STRIKE": ("STRIKE",),
"STRIKE": ("STRIKE",),
"ANTISHIP": ("ANTISHIP",),
"SEAD": ("SEAD",),
"DEAD": ("SEAD",),
"ESCORT": ("CAP HEAVY", "CAP"),
"BAI": ( "BAI", "CAS MAVERICK F", "CAS"),
"BAI": ("BAI", "CAS MAVERICK F", "CAS"),
"SWEEP": ("CAP HEAVY", "CAP"),
"OCA_RUNWAY": ("RUNWAY_ATTACK","RUNWAY_STRIKE","STRIKE"),
"OCA_AIRCRAFT": ("OCA","CAS MAVERICK F", "CAS")
"OCA_RUNWAY": ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"),
"OCA_AIRCRAFT": ("OCA", "CAS MAVERICK F", "CAS"),
}
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
B_1B: COMMON_OVERRIDE,
B_52H: COMMON_OVERRIDE,
F_117A: COMMON_OVERRIDE,
@ -1253,11 +1291,9 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
AH_64A: COMMON_OVERRIDE,
SH_60B: COMMON_OVERRIDE,
Hercules: COMMON_OVERRIDE,
Su_25TM: {
SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410",
},
}
"""
@ -1318,9 +1354,17 @@ TIME_PERIODS = {
}
REWARDS = {
"power": 4, "warehouse": 2, "ware": 2, "fuel": 2, "ammo": 2,
"farp": 1, "fob": 1, "factory": 10, "comms": 10, "oil": 10,
"derrick": 8
"power": 4,
"warehouse": 2,
"ware": 2,
"fuel": 2,
"ammo": 2,
"farp": 1,
"fob": 1,
"factory": 10,
"comms": 10,
"oil": 10,
"derrick": 8,
}
CARRIER_CAPABLE = [
@ -1332,7 +1376,6 @@ CARRIER_CAPABLE = [
A_4E_C,
Rafale_M,
S_3B,
UH_1H,
Mi_8MT,
Ka_50,
@ -1340,7 +1383,6 @@ CARRIER_CAPABLE = [
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
SA342Minigun,
@ -1349,7 +1391,6 @@ CARRIER_CAPABLE = [
LHA_CAPABLE = [
AV8BNA,
UH_1H,
Mi_8MT,
Ka_50,
@ -1357,11 +1398,10 @@ LHA_CAPABLE = [
OH_58D,
UH_60A,
SH_60B,
SA342L,
SA342M,
SA342Minigun,
SA342Mistral
SA342Mistral,
]
"""
@ -1418,27 +1458,50 @@ def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]:
MANPADS: List[VehicleType] = [
AirDefence.SAM_SA_18_Igla_MANPADS,
AirDefence.SAM_SA_18_Igla_S_MANPADS,
AirDefence.Stinger_MANPADS
AirDefence.Stinger_MANPADS,
]
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.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,
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.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_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_Mauser_98,
Infantry.Infantry_Soldier_Rus,
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_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
Infantry.Infantry_Mauser_98,
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,
]
@ -1461,6 +1524,7 @@ def unit_type_name(unit_type) -> str:
def unit_type_name_2(unit_type) -> str:
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:
original_name = unit_type.name and unit_type.name or unit_type.id
default_value = None
@ -1489,6 +1553,7 @@ def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> s
return default_value
return faction_value
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
@ -1522,9 +1587,13 @@ def task_name(task) -> str:
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 = [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])
idx = int(len(suitable_unittypes) * factor)
@ -1572,7 +1641,10 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict
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:
@ -1616,7 +1688,9 @@ def _validate_db():
total_set = set()
for t, unit_collection in UNIT_BY_TASK.items():
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)
# check prices

View File

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

View File

@ -37,7 +37,15 @@ class Event:
to_cp = None # type: ControlPoint
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.from_cp = from_cp
self.to_cp = target_cp
@ -57,12 +65,14 @@ class Event:
Operation.prepare(self.game)
unit_map = Operation.generate()
Operation.current_mission.save(
persistency.mission_path_for("liberation_nextturn.miz"))
persistency.mission_path_for("liberation_nextturn.miz")
)
return unit_map
@staticmethod
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
for_player: bool) -> None:
def _transfer_aircraft(
ato: AirTaskingOrder, losses: AirLosses, for_player: bool
) -> None:
for package in ato.packages:
for flight in package.flights:
# No need to transfer to the same location.
@ -77,13 +87,16 @@ class Event:
if flight.arrival.captured != for_player:
logging.info(
f"Not transferring {flight} because {flight.arrival} "
"was captured")
"was captured"
)
continue
transfer_count = losses.surviving_flight_members(flight)
if transfer_count < 0:
logging.error(f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded.")
logging.error(
f"{flight} had {flight.count} aircraft but "
f"{transfer_count} losses were recorded."
)
continue
aircraft = flight.unit_type
@ -91,7 +104,8 @@ class Event:
if available < transfer_count:
logging.error(
f"Found killed {aircraft} from {flight.departure} but "
f"that airbase has only {available} available.")
f"that airbase has only {available} available."
)
continue
flight.departure.base.aircraft[aircraft] -= transfer_count
@ -101,10 +115,12 @@ class Event:
flight.arrival.base.aircraft[aircraft] += transfer_count
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(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.blue_ato, debriefing.air_losses, for_player=True
)
self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
)
@staticmethod
def commit_air_losses(debriefing: Debriefing) -> None:
@ -115,7 +131,8 @@ class Event:
if available <= 0:
logging.error(
f"Found killed {aircraft} from {cp} but that airbase has "
"none available.")
"none available."
)
continue
logging.info(f"{aircraft} destroyed from {cp}")
@ -130,7 +147,8 @@ class Event:
if available <= 0:
logging.error(
f"Found killed {unit_type} from {control_point} but that "
"airbase has none available.")
"airbase has none available."
)
continue
logging.info(f"{unit_type} destroyed from {control_point}")
@ -149,11 +167,14 @@ class Event:
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
loss.ground_object.kill()
self.game.informations.append(Information(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"location {loss.ground_object.obj_name}", self.game.turn
))
self.game.informations.append(
Information(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"location {loss.ground_object.obj_name}",
self.game.turn,
)
)
@staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None:
@ -171,9 +192,9 @@ class Event:
# ------------------------------
# Captured bases
#if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE
#else:
# if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE
# else:
# coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events:
@ -187,12 +208,22 @@ class Event:
if cp.captured and new_owner_coalition != coalition:
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)
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
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)
captured_cps.append(cp)
else:
@ -218,7 +249,12 @@ class Event:
for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured]
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
player_won = True
@ -234,7 +270,11 @@ class Event:
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:
player_won = False
@ -259,11 +299,17 @@ class Event:
delta = DEFEAT_INFLUENCE
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
player_won = True
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
delta = STRONG_DEFEAT_INFLUENCE
else:
@ -275,7 +321,10 @@ class Event:
delta = STRONG_DEFEAT_INFLUENCE
# 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")
delta = MINOR_DEFEAT_INFLUENCE
@ -283,28 +332,40 @@ class Event:
print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
self.game.turn)
info = Information(
"Frontline Report",
"Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info)
else:
print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
self.game.turn)
info = Information(
"Frontline Report",
"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)
def redeploy_units(self, cp: ControlPoint) -> None:
""""
""" "
Auto redeploy units to newly captured base
"""
ally_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]
ally_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,
# then it is not necessary to redeploy frontline units there.
@ -315,8 +376,7 @@ class Event:
for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint,
source: ControlPoint) -> None:
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
@ -333,8 +393,7 @@ class Event:
for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(
count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
destination.base.commision_units(moved_units)
source.base.commit_losses(moved_units)
@ -362,7 +421,6 @@ class Event:
class UnitsDeliveryEvent:
def __init__(self, control_point: ControlPoint) -> None:
self.to_cp = control_point
self.units: Dict[Type[UnitType], int] = {}
@ -390,8 +448,7 @@ class UnitsDeliveryEvent:
logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
logging.info(
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
logging.info(f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
game.adjust_budget(price * count, player=self.to_cp.captured)
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
@ -409,13 +466,13 @@ class UnitsDeliveryEvent:
aircraft = unit_type.id
name = self.to_cp.name
if count >= 0:
bought_units[unit_type] = count
bought_units[unit_type] = count
game.message(
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
f"{coalition} reinforcements: {aircraft} x {count} at {name}"
)
else:
sold_units[unit_type] = -count
game.message(
f"{coalition} sold: {aircraft} x {-count} at {name}")
game.message(f"{coalition} sold: {aircraft} x {-count} at {name}")
self.to_cp.base.commision_units(bought_units)
self.to_cp.base.commit_losses(sold_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
future unique Event handling
"""
def __str__(self):
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.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
)
from game.data.doctrine import (
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@ -103,8 +113,7 @@ class Faction:
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field(
default_factory=dict)
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless
@ -122,7 +131,11 @@ class Faction:
faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]:
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
raise AssertionError(
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
faction.country
)
)
faction.name = json.get("name", "")
if not faction.name:
@ -135,14 +148,10 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.frontline_units = load_all_vehicles(
json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(
json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(
json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(
json.get("logistics_units", []))
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.ewrs = json.get("ewrs", [])
@ -156,13 +165,10 @@ class Faction:
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get(
"helicopter_carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships(
json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(
json.get("helicopter_carrier", []))
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False)
@ -212,13 +218,18 @@ class Faction:
@property
def units(self) -> List[Type[UnitType]]:
return (self.infantry_units + self.aircrafts + self.awacs +
self.artillery_units + self.frontline_units +
self.tankers + self.logistics_units)
return (
self.infantry_units
+ self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def unit_loader(
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
"""
Find unit by name
:param unit: Unit name as string
@ -242,9 +253,10 @@ def unit_loader(
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(Optional[FlyingType], unit_loader(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
))
return cast(
Optional[FlyingType],
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def load_all_aircraft(data) -> List[Type[FlyingType]]:
@ -257,9 +269,12 @@ def load_all_aircraft(data) -> List[Type[FlyingType]]:
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(Optional[FlyingType], unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
))
return cast(
Optional[FlyingType],
unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]:

View File

@ -78,10 +78,16 @@ class TurnState(Enum):
class Game:
def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime,
settings: Settings, player_budget: float,
enemy_budget: float) -> None:
def __init__(
self,
player_name: str,
enemy_name: str,
theater: ConflictTheater,
start_date: datetime,
settings: Settings,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
@ -112,9 +118,7 @@ class Game:
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory(
self.theater.controlpoints
)
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.sanitize_sides()
@ -147,8 +151,9 @@ class Game:
self.on_load()
def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings)
return Conditions.generate(
self.theater, self.date, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self):
"""
@ -184,13 +189,24 @@ class Game:
return random.randint(1, 100) <= prob * mult
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):
for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b)
self._generate_player_event(
FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b,
)
def adjust_budget(self, amount: float, player: bool) -> None:
if player:
@ -208,7 +224,7 @@ class Game:
self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events
# assert event in self.events
logging.info("Generating {} (regular)".format(event))
return event.generate()
@ -223,7 +239,11 @@ class Game:
def is_player_attack(self, 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:
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:
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
for control_point in self.theater.controlpoints:
@ -281,7 +303,7 @@ class Game:
# Check for win or loss condition
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)
# Plan flights & combat for next turn
@ -305,8 +327,11 @@ class Game:
self.plan_procurement(blue_planner, red_planner)
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner) -> None:
def plan_procurement(
self,
blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner,
) -> None:
# 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
# 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_front_line=self.settings.automate_front_line_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)
self.enemy_budget = ProcurementAi(
@ -330,7 +355,7 @@ class Game:
manage_runways=True,
manage_front_line=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)
def message(self, text: str) -> None:
@ -361,10 +386,12 @@ class Game:
def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
self.theater)
self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
self.theater)
self.blue_navmesh = NavMesh.from_threat_zones(
self.red_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:
if player:
@ -386,9 +413,9 @@ class Game:
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_b,
self.theater)
position = Conflict.frontline_position(
front_line.control_point_a, front_line.control_point_b, self.theater
)
zones.append(position[0])
zones.append(front_line.control_point_a.position)
zones.append(front_line.control_point_b.position)
@ -413,7 +440,10 @@ class Game:
d = cp.position.distance_to_point(cp2.position)
if d < min_distance:
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(cp2.position)
break
@ -422,8 +452,7 @@ class Game:
if cpoint is not None:
zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages,
self.red_ato.packages)
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
@ -460,7 +489,10 @@ class Game:
return False
else:
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
for p in self.__culling_points:
if p.distance_to_point(pos) < 2500:
@ -502,6 +534,10 @@ class Game:
def process_win_loss(self, turn_state: TurnState):
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:
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:
if not tgo.is_dead:
count += 1
self.buildings.append(BuildingIncome(name, category, count,
REWARDS[category]))
self.buildings.append(
BuildingIncome(name, category, count, REWARDS[category])
)
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 = ((self.total_buildings + self.from_bases) *
self.multiplier)
self.total = (self.total_buildings + self.from_bases) * self.multiplier

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ class FrontlineData:
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.from_cp = from_cp
self.enemy_units_position = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,8 @@ class LuaPluginManager:
if not plugin_path.exists():
raise RuntimeError(
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}")
plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None:

View File

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

View File

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

View File

@ -22,7 +22,6 @@ BASE_MIN_STRENGTH = 0
class Base:
def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
@ -57,23 +56,43 @@ class Base:
return sum(self.aa.values())
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:
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
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],
for_type: Task, count: int) -> Dict[UnitType, int]:
def _find_best_unit(
self, available_units: Dict[UnitType, int], for_type: Task, count: int
) -> Dict[UnitType, int]:
if count <= 0:
logging.warning("{}: no units for {}".format(self, for_type))
return {}
sorted_units = [key for key in available_units if
key in db.UNIT_BY_TASK[for_type]]
sorted_units = [
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)
result: Dict[UnitType, int] = {}
@ -94,14 +113,18 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, 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)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
return self._find_best_unit(self.armor, for_type, count)
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]
if points >= 1:
self.commision_points[for_type] = points - math.floor(points)
@ -110,7 +133,9 @@ class Base:
return 0
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}
def commision_units(self, units: typing.Dict[typing.Any, int]):
@ -149,7 +174,7 @@ class Base:
if unit_type not in target_array:
print("Base didn't find event type {}".format(unit_type))
continue
target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0:
del target_array[unit_type]
@ -166,12 +191,20 @@ class Base:
def scramble_count(self, multiplier: float, task: Task = None) -> int:
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:
count = self.total_aircraft
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):
return int(self.total_armor * 0.5)
@ -202,4 +235,8 @@ class Base:
return self._find_best_armor(PinpointStrike, count)
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

@ -71,6 +71,7 @@ IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
@ -156,7 +157,8 @@ class MizCampaignLoader:
def country(self, blue: bool) -> 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.
assert country
return country
@ -183,7 +185,7 @@ class MizCampaignLoader:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE:
@ -253,22 +255,23 @@ class MizCampaignLoader:
for blue in (False, True):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(next(self.control_point_id),
str(group.name), group.position)
control_point = OffMapSpawn(
next(self.control_point_id), str(group.name), group.position
)
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.carriers(blue):
# TODO: Name the 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_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.lhas(blue):
# TODO: Name the LHA.
control_point = Lha(
"lha", group.position, next(self.control_point_id))
control_point = Lha("lha", group.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
@ -297,24 +300,24 @@ class MizCampaignLoader:
# final waypoint at the destination CP. Intermediate waypoints
# define the curve of the front line.
waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0])
origin = self.theater.closest_control_point(waypoints[0])
if origin is None:
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])
if destination is None:
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.
waypoints[0] = origin.position
waypoints[-1] = destination.position
front_line_id = f"{origin.id}|{destination.id}"
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
self.control_points[origin.id].connect(
self.control_points[destination.id])
self.control_points[destination.id].connect(
self.control_points[origin.id])
self.control_points[origin.id].connect(self.control_points[destination.id])
self.control_points[destination.id].connect(self.control_points[origin.id])
return front_lines
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
@ -328,8 +331,7 @@ class MizCampaignLoader:
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_garrisons.append(group.position)
else:
logging.warning(
f"Found garrison unit too far from base: {group.name}")
logging.warning(f"Found garrison unit too far from base: {group.name}")
for group in self.sams:
closest, distance = self.objective_info(group)
@ -344,8 +346,7 @@ class MizCampaignLoader:
for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append(
group.position)
closest.preset_locations.offshore_strike_locations.append(group.position)
for group in self.ships:
closest, distance = self.objective_info(group)
@ -361,15 +362,11 @@ class MizCampaignLoader:
for group in self.required_long_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_long_range_sams.append(
group.position
)
closest.preset_locations.required_long_range_sams.append(group.position)
for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append(
group.position
)
closest.preset_locations.required_medium_range_sams.append(group.position)
def populate_theater(self) -> None:
for control_point in self.control_points.values():
@ -423,8 +420,9 @@ class ConflictTheater:
logging.warning("Replacing existing frontline data")
self._frontline_data = data
def add_controlpoint(self, point: ControlPoint,
connected_to: Optional[List[ControlPoint]] = None):
def add_controlpoint(
self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
):
if connected_to is None:
connected_to = []
for connected_point in connected_to:
@ -486,8 +484,8 @@ class ConflictTheater:
for inclusion_zone in self.landmap.inclusion_zones:
nearest_pair = ops.nearest_points(point, inclusion_zone)
nearest_points.append(nearest_pair[1])
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
nearest_point = nearest_points[0] # type: geometry.Point
for pt in nearest_points[1:]:
distance = point.distance(pt)
if distance < min_distance:
@ -498,7 +496,7 @@ class ConflictTheater:
nearest_point = Point(nearest_point.x, nearest_point.y)
new_point = point.point_from_heading(
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
@ -512,7 +510,9 @@ class ConflictTheater:
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
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)
def enemy_points(self) -> List[ControlPoint]:
@ -547,7 +547,7 @@ class ConflictTheater:
closest = conflict
closest_distance = distance
return closest
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""
Returns a tuple of the two nearest opposing ControlPoints in theater.
@ -567,17 +567,22 @@ class ConflictTheater:
distances[cp.id] = dist
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 = [
self.find_control_point_by_id(i)
for i
in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
] # type: List[ControlPoint]
for i in min(
all_cp_min_distances, key=all_cp_min_distances.get
) # type: ignore
] # type: List[ControlPoint]
assert len(closest_opposing_cps) == 2
if closest_opposing_cps[0].captured:
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
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:
for i in self.controlpoints:
@ -613,7 +618,7 @@ class ConflictTheater:
cp.captured_invert = False
return cp
@staticmethod
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = {
@ -649,7 +654,7 @@ class ConflictTheater:
cps[l[1]].connect(cps[l[0]])
return t
class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus()
@ -672,8 +677,7 @@ class PersianGulfTheater(ConflictTheater):
terrain = persiangulf.PersianGulf()
overview_image = "persiangulf.gif"
reference_points = (
ReferencePoint(persiangulf.Jiroft_Airport.position,
Point(1692, 1343)),
ReferencePoint(persiangulf.Jiroft_Airport.position, Point(1692, 1343)),
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
)
landmap = load_landmap("resources\\gulflandmap.p")
@ -722,7 +726,7 @@ class TheChannelTheater(ConflictTheater):
overview_image = "thechannel.gif"
reference_points = (
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")
daytime_map = {
@ -791,7 +795,7 @@ class FrontLine(MissionTarget):
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater
theater: ConflictTheater,
) -> None:
self.control_point_a = control_point_a
self.control_point_b = control_point_b
@ -935,10 +939,9 @@ class FrontLine(MissionTarget):
)
)
@staticmethod
def load_json_frontlines(
theater: ConflictTheater
theater: ConflictTheater,
) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json"""
try:

View File

@ -231,10 +231,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Only airbases have IDs.
# TODO: has_frontline is only reasonable for airbases.
# TODO: cptype is obsolete.
def __init__(self, cp_id: int, name: str, position: Point,
at: db.StartingPosition, size: int,
importance: float, has_frontline=True,
cptype=ControlPointType.AIRBASE):
def __init__(
self,
cp_id: int,
name: str,
position: Point,
at: db.StartingPosition,
size: int,
importance: float,
has_frontline=True,
cptype=ControlPointType.AIRBASE,
):
super().__init__(name, position)
# TODO: Should be Airbase specific.
self.id = cp_id
@ -257,17 +264,17 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {}
from ..event import UnitsDeliveryEvent
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
self.target_position: Optional[Point] = None
def __repr__(self):
return f"<{__class__}: {self.name}>"
@property
def ground_objects(self) -> List[TheaterGroundObject]:
return list(
itertools.chain(self.connected_objectives, self.base_defenses))
return list(itertools.chain(self.connected_objectives, self.base_defenses))
@property
@abstractmethod
@ -342,15 +349,18 @@ class ControlPoint(MissionTarget, ABC):
Get the carrier group name if the airbase is a carrier
:return: Carrier group name
"""
if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP]:
if self.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]:
for g in self.ground_objects:
if g.dcs_identifier == "CARRIER":
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov]:
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
]:
return group.name
elif g.dcs_identifier == "LHA":
for group in g.groups:
@ -379,17 +389,15 @@ class ControlPoint(MissionTarget, ABC):
if isinstance(base_defense, EwrGroundObject):
self.preset_locations.ewrs.append(base_defense.position)
elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append(
base_defense.position)
self.preset_locations.base_air_defense.append(base_defense.position)
elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append(
base_defense.position)
self.preset_locations.base_garrisons.append(base_defense.position)
else:
logging.error(
"Could not determine preset location type for "
f"{base_defense}. Assuming garrison type.")
self.preset_locations.base_garrisons.append(
base_defense.position)
f"{base_defense}. Assuming garrison type."
)
self.preset_locations.base_garrisons.append(base_defense.position)
self.base_defenses = []
def capture_equipment(self, game: Game) -> None:
@ -398,15 +406,18 @@ class ControlPoint(MissionTarget, ABC):
game.adjust_budget(total, player=not self.captured)
game.message(
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):
# When there are multiple valid destinations, deliver units to whichever
# base is least defended first. The closest approximation of unit
# strength we have is price
destinations = [GroundUnitDestination(cp)
for cp in self.connected_points
if cp.captured == self.captured]
destinations = [
GroundUnitDestination(cp)
for cp in self.connected_points
if cp.captured == self.captured
]
if not destinations:
self.capture_equipment(game)
return
@ -419,8 +430,9 @@ class ControlPoint(MissionTarget, ABC):
destination.control_point.base.commision_units({unit_type: 1})
destination = heapq.heappushpop(destinations, destination)
def capture_aircraft(self, game: Game, airframe: Type[FlyingType],
count: int) -> None:
def capture_aircraft(
self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
try:
value = PRICES[airframe] * count
except KeyError:
@ -431,11 +443,12 @@ class ControlPoint(MissionTarget, ABC):
game.message(
f"No valid retreat destination in range of {self.name} for "
f"{airframe.id}. {count} aircraft have been captured and sold for "
f"${value}M.")
f"${value}M."
)
def aircraft_retreat_destination(
self, game: Game,
airframe: Type[FlyingType]) -> Optional[ControlPoint]:
self, game: Game, airframe: Type[FlyingType]
) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent.
max_retreat_distance = nautical_miles(200)
@ -451,8 +464,9 @@ class ControlPoint(MissionTarget, ABC):
return airbase
return None
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType],
count: int) -> None:
def _retreat_air_units(
self, game: Game, airframe: Type[FlyingType], count: int
) -> None:
while count:
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
destination = self.aircraft_retreat_destination(game, airframe)
@ -485,6 +499,7 @@ class ControlPoint(MissionTarget, ABC):
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
@abstractmethod
@ -514,16 +529,19 @@ class ControlPoint(MissionTarget, ABC):
if issubclass(unit_bought, FlyingType):
on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_aircraft, on_order,
self.aircraft_transferring(game))
return PendingOccupancy(
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
)
def unclaimed_parking(self, game: Game) -> int:
return (self.total_aircraft_parking -
self.expected_aircraft_next_turn(game).total)
return (
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
@abstractmethod
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
...
@property
@ -574,7 +592,13 @@ class ControlPoint(MissionTarget, ABC):
Get number of pending frontline aa units
"""
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:
return 0
@ -598,9 +622,12 @@ class ControlPoint(MissionTarget, ABC):
continue
on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(self.base.total_armor, on_order,
# Ground unit transfers not yet implemented.
transferring=0)
return PendingOccupancy(
self.base.total_armor,
on_order,
# Ground unit transfers not yet implemented.
transferring=0,
)
@property
def income_per_turn(self) -> int:
@ -608,16 +635,23 @@ class ControlPoint(MissionTarget, ABC):
@property
def has_active_frontline(self) -> bool:
return any(
not c.is_friendly(self.captured) for c in self.connected_points)
return any(not c.is_friendly(self.captured) for c in self.connected_points)
class Airfield(ControlPoint):
def __init__(self, airport: Airport, size: int,
importance: float, has_frontline=True):
super().__init__(airport.id, airport.name, airport.position, airport,
size, importance, has_frontline,
cptype=ControlPointType.AIRBASE)
def __init__(
self, airport: Airport, size: int, importance: float, has_frontline=True
):
super().__init__(
airport.id,
airport.name,
airport.position,
airport,
size,
importance,
has_frontline,
cptype=ControlPointType.AIRBASE,
)
self.airport = airport
self._runway_status = RunwayStatus()
@ -631,6 +665,7 @@ class Airfield(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
@ -661,8 +696,9 @@ class Airfield(ControlPoint):
def damage_runway(self) -> None:
self.runway_status.damage()
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport)
@ -680,13 +716,13 @@ class Airfield(ControlPoint):
class NavalControlPoint(ControlPoint, ABC):
@property
def is_fleet(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
@ -710,14 +746,17 @@ class NavalControlPoint(ControlPoint, ABC):
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock]:
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
]:
return True
return False
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
# TODO: Assign TACAN and ICLS earlier so we don't need this.
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
return dynamic_runways.get(self.name, fallback)
@ -740,12 +779,19 @@ class NavalControlPoint(ControlPoint, ABC):
class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
super().__init__(
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:
raise RuntimeError("Carriers cannot be captured")
@ -763,12 +809,19 @@ class Carrier(NavalControlPoint):
class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
super().__init__(
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:
raise RuntimeError("LHAs cannot be captured")
@ -786,15 +839,22 @@ class Lha(NavalControlPoint):
class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool:
return True
def __init__(self, cp_id: int, name: str, position: Point):
from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
super().__init__(cp_id, name, position, at=position,
size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
has_frontline=False, cptype=ControlPointType.OFF_MAP)
super().__init__(
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:
raise RuntimeError("Off map control points cannot be captured")
@ -813,8 +873,9 @@ class OffMapSpawn(ControlPoint):
def heading(self) -> int:
return 0
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="")
@ -828,19 +889,27 @@ class OffMapSpawn(ControlPoint):
class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
import game.theater.conflicttheater
super().__init__(cp_id, name, at, at,
game.theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=True, cptype=ControlPointType.FOB)
super().__init__(
cp_id,
name,
at,
at,
game.theater.conflicttheater.SIZE_SMALL,
1,
has_frontline=True,
cptype=ControlPointType.FOB,
)
self.name = name
def runway_is_operational(self) -> bool:
return False
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData:
logging.warning("TODO: FOBs have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="")
@ -850,6 +919,7 @@ class Fob(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
FlightType.BARCAP,

View File

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

View File

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

View File

@ -76,9 +76,14 @@ class GeneratorSettings:
class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings) -> None:
def __init__(
self,
player: str,
enemy: str,
theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings,
) -> None:
self.player = player
self.enemy = enemy
self.theater = theater
@ -96,7 +101,7 @@ class GameGenerator:
start_date=self.generator_settings.start_date,
settings=self.settings,
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()
@ -108,7 +113,7 @@ class GameGenerator:
# Auto-capture half the bases if midgame.
if self.generator_settings.midgame:
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
# Remove carrier and lha, invert situation if needed
@ -140,28 +145,37 @@ class LocationFinder:
self.game = game
self.control_point = control_point
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[Point]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
return position
logging.warning(f"No campaign location for %s at %s",
location_type.value, self.control_point)
logging.warning(
f"No campaign location for %s at %s",
location_type.value,
self.control_point,
)
position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget)
location_type == LocationType.OffshoreStrikeTarget
)
if position is not None:
return position
logging.debug(f"No mizdata location for %s at %s", location_type.value,
self.control_point)
logging.debug(
f"No mizdata location for %s at %s", location_type.value, self.control_point
)
position = self.random_position(location_type)
if position is not None:
return position
logging.error(f"Could not find position for %s at %s",
location_type.value, self.control_point)
logging.error(
f"Could not find position for %s at %s",
location_type.value,
self.control_point,
)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
@ -177,8 +191,11 @@ class LocationFinder:
def random_position(self, location_type: LocationType) -> Optional[Point]:
# TODO: Flesh out preset locations so we never hit this case.
logging.warning("Falling back to random location for %s at %s",
location_type.value, self.control_point)
logging.warning(
"Falling back to random location for %s at %s",
location_type.value,
self.control_point,
)
is_base_defense = location_type in {
LocationType.BaseAirDefense,
@ -212,23 +229,28 @@ class LocationFinder:
min_range = 10000
max_range = 40000
position = self._find_random_position(min_range, max_range,
on_land, is_base_defense,
avoid_others)
position = self._find_random_position(
min_range, max_range, on_land, is_base_defense, avoid_others
)
# Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away
# from small airfields.
if position is None and is_base_defense:
position = self._find_random_position(3200, 4800,
on_land, is_base_defense,
avoid_others)
position = self._find_random_position(
3200, 4800, on_land, is_base_defense, avoid_others
)
return position
def _find_random_position(self, min_range: int, max_range: int,
on_ground: bool, is_base_defense: bool,
avoid_others: bool) -> Optional[Point]:
def _find_random_position(
self,
min_range: int,
max_range: int,
on_ground: bool,
is_base_defense: bool,
avoid_others: bool,
) -> Optional[Point]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
@ -279,8 +301,12 @@ class LocationFinder:
class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, generator_settings: GeneratorSettings,
control_point: ControlPoint) -> None:
def __init__(
self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
) -> None:
self.game = game
self.generator_settings = generator_settings
self.control_point = control_point
@ -321,15 +347,15 @@ class ControlPointGroundObjectGenerator:
self.generate_ship()
def generate_ship(self) -> None:
point = self.location_finder.location_for(
LocationType.OffshoreStrikeTarget)
point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
if point is None:
return
group_id = self.game.next_group_id()
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
self.control_point)
g = ShipGroundObject(
namegen.random_objective_name(), group_id, point, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name)
g.groups = []
@ -352,13 +378,15 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not carrier_names:
logging.info(
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
# Create ground object group
group_id = self.game.next_group_id()
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
g = CarrierGroundObject(
namegen.random_objective_name(), group_id, self.control_point
)
group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
@ -377,13 +405,15 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not lha_names:
logging.info(
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
# Create ground object group
group_id = self.game.next_group_id()
g = LhaGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
g = LhaGroundObject(
namegen.random_objective_name(), group_id, self.control_point
)
group = generate_lha_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
@ -422,8 +452,9 @@ class BaseDefenseGenerator:
group_id = self.game.next_group_id()
g = EwrGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point)
g = EwrGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
@ -454,28 +485,35 @@ class BaseDefenseGenerator:
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point,
for_airbase=True)
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
f"Could not generate garrison at {self.control_point}")
logging.error(f"Could not generate garrison at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction)
if not groups:
@ -485,21 +523,25 @@ class BaseDefenseGenerator:
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
position = self.location_finder.location_for(
LocationType.BaseAirDefense)
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction,
ranges=[{AirDefenseRange.Short}])
groups = generate_anti_air_group(
self.game, g, self.faction, ranges=[{AirDefenseRange.Short}]
)
if not groups:
logging.error(
f"Could not generate SHORAD group at {self.control_point}")
logging.error(f"Could not generate SHORAD group at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
@ -509,7 +551,7 @@ class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None:
self.generate_garrison()
self.generate_fob_defenses()
def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison.
@ -528,9 +570,13 @@ class FobDefenseGenerator(BaseDefenseGenerator):
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates) -> None:
def __init__(
self,
game: Game,
generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates,
) -> None:
super().__init__(game, generator_settings, control_point)
self.templates = templates
@ -576,18 +622,25 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
"""
presets = self.control_point.preset_locations
for position in presets.required_long_range_sams:
self.generate_aa_at(position, ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
for position in presets.required_medium_range_sams:
self.generate_aa_at(position, ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
return (len(presets.required_long_range_sams) +
len(presets.required_medium_range_sams))
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
)
def generate_ground_point(self) -> None:
try:
@ -618,8 +671,15 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point,
unit["heading"], self.control_point, unit["type"])
obj_name,
category,
group_id,
object_id,
point + template_point,
unit["heading"],
self.control_point,
unit["type"],
)
self.control_point.connected_objectives.append(g)
@ -627,23 +687,34 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
position = self.location_finder.location_for(LocationType.Sam)
if position is None:
return
self.generate_aa_at(position, ranges=[
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
{AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
])
self.generate_aa_at(
position,
ranges=[
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
{AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
)
def generate_aa_at(
self, position: Point,
ranges: Iterable[Set[AirDefenseRange]]) -> None:
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=False)
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=False,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups:
logging.error("Could not generate air defense group for %s at %s",
g.name, self.control_point)
logging.error(
"Could not generate air defense group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = groups
self.control_point.connected_objectives.append(g)
@ -659,8 +730,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point)
g = MissileSiteGroundObject(
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_missile_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
@ -678,7 +750,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate_fob(self) -> None:
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:
logging.exception("Faction has no fob buildings defined")
return
@ -696,14 +768,21 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
template_point = Point(unit["offset"].x, unit["offset"].y)
g = BuildingGroundObject(
obj_name, category, group_id, object_id, point + template_point,
unit["heading"], self.control_point, unit["type"], airbase_group=True)
obj_name,
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)
class GroundObjectGenerator:
def __init__(self, game: Game,
generator_settings: GeneratorSettings) -> None:
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
self.game = game
self.generator_settings = generator_settings
with open("resources/groundobject_templates.p", "rb") as f:
@ -721,19 +800,22 @@ class GroundObjectGenerator:
generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(
self.game, self.generator_settings, control_point)
self.game, self.generator_settings, control_point
)
elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
self.game, self.generator_settings, control_point, self.templates
)
else:
generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
self.game, self.generator_settings, control_point, self.templates
)
return generator.generate()

View File

@ -33,7 +33,7 @@ NAME_BY_CATEGORY = {
"ww2bunker": "Bunker",
"village": "Village",
"allycamp": "Camp",
"EWR":"EWR",
"EWR": "EWR",
}
ABBREV_NAME = {
@ -54,34 +54,59 @@ ABBREV_NAME = {
}
CATEGORY_MAP = {
# Special cases
"CARRIER": ["CARRIER"],
"LHA": ["LHA"],
"aa": ["AA"],
# 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"],
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
"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"],
"factory": ["Tech combine", "Tech hangar A"],
"comms": ["TV tower", "Comms tower M"],
"oil": ["Oil platform"],
"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"],
"allycamp": [],
}
class TheaterGroundObject(MissionTarget):
def __init__(self, name: str, category: str, group_id: int, position: Point,
heading: int, control_point: ControlPoint, dcs_identifier: str,
airbase_group: bool, sea_object: bool) -> None:
def __init__(
self,
name: str,
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)
self.category = category
self.group_id = group_id
@ -131,6 +156,7 @@ class TheaterGroundObject(MissionTarget):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.LOGISTICS
@ -193,9 +219,18 @@ class TheaterGroundObject(MissionTarget):
class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int,
position: Point, heading: int, control_point: ControlPoint,
dcs_identifier: str, airbase_group=False) -> None:
def __init__(
self,
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__(
name=name,
category=category,
@ -205,7 +240,7 @@ class BuildingGroundObject(TheaterGroundObject):
control_point=control_point,
dcs_identifier=dcs_identifier,
airbase_group=airbase_group,
sea_object=False
sea_object=False,
)
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
@ -234,6 +269,7 @@ class BuildingGroundObject(TheaterGroundObject):
class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@ -249,8 +285,7 @@ class GenericCarrierGroundObject(NavalGroundObject):
# TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="CARRIER",
@ -260,7 +295,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
control_point=control_point,
dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True
sea_object=True,
)
@property
@ -272,8 +307,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="LHA",
@ -283,7 +317,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
control_point=control_point,
dcs_identifier="LHA",
airbase_group=True,
sea_object=True
sea_object=True,
)
@property
@ -294,8 +328,9 @@ class LhaGroundObject(GenericCarrierGroundObject):
class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__(
name=name,
category="aa",
@ -305,7 +340,7 @@ class MissileSiteGroundObject(TheaterGroundObject):
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False
sea_object=False,
)
@ -317,8 +352,14 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types.
class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
category="aa",
@ -328,7 +369,7 @@ class SamGroundObject(BaseDefenseGroundObject):
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
sea_object=False,
)
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
@ -345,6 +386,7 @@ class SamGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield from super().mission_types(for_player)
@ -355,8 +397,14 @@ class SamGroundObject(BaseDefenseGroundObject):
class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
category="aa",
@ -366,13 +414,14 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
sea_object=False,
)
class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__(
name=name,
category="EWR",
@ -382,7 +431,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
control_point=control_point,
dcs_identifier="EWR",
airbase_group=True,
sea_object=False
sea_object=False,
)
@property
@ -392,6 +441,7 @@ class EwrGroundObject(BaseDefenseGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield from super().mission_types(for_player)
@ -402,8 +452,9 @@ class EwrGroundObject(BaseDefenseGroundObject):
class ShipGroundObject(NavalGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__(
name=name,
category="aa",
@ -413,7 +464,7 @@ class ShipGroundObject(NavalGroundObject):
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=True
sea_object=True,
)
@property

View File

@ -32,8 +32,9 @@ class ThreatZones:
self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
boundary, _ = nearest_points(self.all.boundary,
self.dcs_to_shapely_point(point))
boundary, _ = nearest_points(
self.all.boundary, self.dcs_to_shapely_point(point)
)
return DcsPoint(boundary.x, boundary.y)
@singledispatchmethod
@ -49,8 +50,9 @@ class ThreatZones:
return self.all.intersects(self.dcs_to_shapely_point(position))
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
return self.threatened(LineString(
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
return self.threatened(
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
)
@singledispatchmethod
def threatened_by_aircraft(self, target) -> bool:
@ -62,9 +64,9 @@ class ThreatZones:
@threatened_by_aircraft.register
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
return self.threatened_by_aircraft(LineString((
self.dcs_to_shapely_point(p.position) for p in flight.points
)))
return self.threatened_by_aircraft(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
@singledispatchmethod
def threatened_by_air_defense(self, target) -> bool:
@ -76,13 +78,14 @@ class ThreatZones:
@threatened_by_air_defense.register
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
return self.threatened_by_air_defense(LineString((
self.dcs_to_shapely_point(p.position) for p in flight.points
)))
return self.threatened_by_air_defense(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
@classmethod
def closest_enemy_airbase(cls, location: ControlPoint,
max_distance: Distance) -> Optional[ControlPoint]:
def closest_enemy_airbase(
cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.airfields_within(max_distance):
if airfield.captured != location.captured:
@ -90,13 +93,14 @@ class ThreatZones:
return None
@classmethod
def barcap_threat_range(cls, game: Game,
control_point: ControlPoint) -> Distance:
def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
doctrine = game.faction_for(control_point.captured).doctrine
cap_threat_range = (doctrine.cap_max_distance_from_cp +
doctrine.cap_engagement_range)
opposing_airfield = cls.closest_enemy_airbase(control_point,
cap_threat_range * 2)
cap_threat_range = (
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
)
opposing_airfield = cls.closest_enemy_airbase(
control_point, cap_threat_range * 2
)
if opposing_airfield is None:
return cap_threat_range
@ -133,8 +137,7 @@ class ThreatZones:
if control_point.captured != player:
continue
if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x,
control_point.position.y)
point = ShapelyPoint(control_point.position.x, control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point)
airbases.append(point.buffer(cap_threat_range.meters))
@ -149,10 +152,9 @@ class ThreatZones:
air_defenses.append(threat_zone)
return cls(
airbases=unary_union(airbases),
air_defenses=unary_union(air_defenses)
airbases=unary_union(airbases), air_defenses=unary_union(air_defenses)
)
@staticmethod
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}")
if not issubclass(unit_type, VehicleType):
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)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None)
def add_ground_object_units(self, ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group) -> None:
def add_ground_object_units(
self,
ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group,
) -> None:
"""Adds a group associated with a TGO to the unit map.
Args:
@ -103,13 +107,13 @@ class UnitMap:
if name in self.ground_object_units:
raise RuntimeError(f"Duplicate TGO unit: {name}")
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]:
return self.ground_object_units.get(name, None)
def add_building(self, ground_object: BuildingGroundObject,
group: Group) -> None:
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
# 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}")
self.buildings[name] = Building(ground_object)
def add_fortification(self, ground_object: BuildingGroundObject,
group: VehicleGroup) -> None:
def add_fortification(
self, ground_object: BuildingGroundObject, group: VehicleGroup
) -> None:
if len(group.units) != 1:
raise ValueError("Fortification groups must have exactly one unit.")
unit = group.units[0]

View File

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

@ -32,6 +32,7 @@ AWACS_ALT = 13000
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
dcsGroupName: str
callsign: str
freq: RadioFrequency
@ -40,6 +41,7 @@ class AwacsInfo:
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
dcsGroupName: str
callsign: str
variant: str
@ -54,10 +56,14 @@ class AirSupport:
class AirSupportConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry) -> None:
def __init__(
self,
mission: Mission,
conflict: Conflict,
game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
@ -78,22 +84,37 @@ class AirSupportConflictGenerator:
elif unit_type is KC135MPRS:
return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574)
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
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)
variant = db.unit_type_name(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
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_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
tanker_heading = (
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(
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,
plane_type=tanker_unit_type,
position=tanker_position,
@ -124,28 +145,41 @@ class AirSupportConflictGenerator:
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
tacan.number, tacan.band.value, tacan_callsign, True,
tanker_group.units[0].id, True))
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
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(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)
)
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
if len(possible_awacs) > 0:
awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
awacs_flight = self.mission.awacs_flight(
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,
altitude=AWACS_ALT,
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,
start_type=StartType.Warm,
)
@ -154,7 +188,12 @@ class AirSupportConflictGenerator:
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(AwacsInfo(
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
self.air_support.awacs.append(
AwacsInfo(
str(awacs_flight.name),
callsign_for_support_unit(awacs_flight),
freq,
)
)
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.planes import MQ_9_Reaper
from dcs.point import PointAction
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
SetInvisibleCommand)
from dcs.task import (
EPLRS,
AttackGroup,
ControlledTask,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
@ -24,8 +32,11 @@ from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
CombatGroup, CombatGroupRole)
from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE,
CombatGroup,
CombatGroupRole,
)
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
@ -56,6 +67,7 @@ INFANTRY_GROUP_SIZE = 5
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
dcsGroupName: str
unit_name: str
callsign: str
@ -65,16 +77,16 @@ class JtacInfo:
class GroundConflictGenerator:
def __init__(
self,
mission: Mission,
conflict: Conflict,
game: Game,
player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance,
unit_map: UnitMap) -> None:
self,
mission: Mission,
conflict: Conflict,
game: Game,
player_planned_combat_groups: List[CombatGroup],
enemy_planned_combat_groups: List[CombatGroup],
player_stance: CombatStance,
unit_map: UnitMap,
) -> None:
self.mission = mission
self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups
@ -87,14 +99,16 @@ class GroundConflictGenerator:
def _enemy_stance(self):
"""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(
[
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH
CombatStance.BREAKTHROUGH,
]
)
else:
@ -104,31 +118,37 @@ class GroundConflictGenerator:
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE
CombatStance.AGGRESSIVE,
]
)
@staticmethod
def _group_point(point: Point, base_distance) -> Point:
distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
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(
self.conflict.from_cp,
self.conflict.to_cp,
self.game.theater
)
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
)
# 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
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
self.plan_action_for_groups(
@ -137,7 +157,7 @@ class GroundConflictGenerator:
enemy_groups,
self.conflict.heading + 90,
self.conflict.from_cp,
self.conflict.to_cp
self.conflict.to_cp,
)
self.plan_action_for_groups(
self.enemy_stance,
@ -145,7 +165,7 @@ class GroundConflictGenerator:
player_groups,
self.conflict.heading - 90,
self.conflict.to_cp,
self.conflict.from_cp
self.conflict.from_cp,
)
# Add JTAC
@ -157,34 +177,38 @@ class GroundConflictGenerator:
if self.game.player_faction.jtac_unit is not None:
utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
name=n,
aircraft_type=utype,
position=position[0],
airport=None,
altitude=5000)
jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country),
name=n,
aircraft_type=utype,
position=position[0],
airport=None,
altitude=5000,
)
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
jtac.points[0].tasks.append(
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.
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(
self,
group: VehicleGroup,
is_player: bool,
side: Country,
forward_heading: int
self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int
) -> None:
infantry_position = self.conflict.find_ground_position(
group.points[0].position.random_point_within(250, 50),
500,
forward_heading,
self.conflict.theater
)
self.conflict.theater,
)
if not infantry_position:
logging.warning("Could not find infantry position")
return
@ -208,44 +232,50 @@ class GroundConflictGenerator:
u = random.choice(manpads)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u), u,
namegen.next_infantry_name(side, cp.id, u),
u,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
move_formation=PointAction.OffRoad,
)
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:
return
u = random.choice(possible_infantry_units)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u), u,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
side,
namegen.next_infantry_name(side, cp.id, u),
u,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad,
)
for i in range(INFANTRY_GROUP_SIZE):
u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u), u,
namegen.next_infantry_name(side, cp.id, u),
u,
position=position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
move_formation=PointAction.OffRoad,
)
def _set_reform_waypoint(
self,
dcs_group: VehicleGroup,
forward_heading: int
self, dcs_group: VehicleGroup, forward_heading: int
) -> None:
"""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)
dcs_group.add_waypoint(reform_point)
@ -256,7 +286,7 @@ class GroundConflictGenerator:
gen_group: CombatGroup,
dcs_group: VehicleGroup,
forward_heading: int,
target: Point
target: Point,
) -> bool:
"""
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)
# 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))
# TODO: Update to fire at group instead of point
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
@ -283,12 +315,19 @@ class GroundConflictGenerator:
# Hold position
dcs_group.points[1].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
retreat = self.find_retreat_point(
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.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):
artillery_fallback.add_condition(UnitDamaged(u.id))
if i < len(dcs_group.units) - 1:
@ -302,7 +341,9 @@ class GroundConflictGenerator:
retreat_task.number = 4
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)
for u in dcs_group.units:
@ -330,12 +371,8 @@ class GroundConflictGenerator:
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
if target is not None:
rand_offset = Point(
random.randint(
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
),
random.randint(
-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.points[0].position + rand_offset
@ -345,8 +382,7 @@ class GroundConflictGenerator:
if (
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(
to_cp.position.random_point_within(500, 0)
@ -358,16 +394,16 @@ class GroundConflictGenerator:
if offset_heading < 0:
offset_heading = 358
attack_point = self.find_offensive_point(
dcs_group,
offset_heading,
AGGRESIVE_MOVE_DISTANCE
dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
)
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
elif stance == CombatStance.BREAKTHROUGH:
# In breakthrough mode, the units will move forward
# If the enemy base is close enough, the units will attack the base
if to_cp.position.distance_to_point(
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
if (
to_cp.position.distance_to_point(dcs_group.points[0].position)
<= BREAKTHROUGH_OFFENSIVE_DISTANCE
):
attack_point = self.conflict.theater.nearest_land_pos(
to_cp.position.random_point_within(500, 0)
)
@ -377,27 +413,27 @@ class GroundConflictGenerator:
offset_heading = forward_heading - 1
if offset_heading < 0:
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)
elif stance == CombatStance.ELIMINATION:
# 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)
for i, target in enumerate(targets, start=1):
rand_offset = Point(
random.randint(
-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
),
random.randint(
-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.points[0].position+rand_offset
target.points[0].position + rand_offset
)
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
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(
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.
"""
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
if 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))
if (
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:
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)
if stance != CombatStance.RETREAT:
@ -434,29 +481,36 @@ class GroundConflictGenerator:
return False
def plan_action_for_groups(
self, stance: CombatStance,
self,
stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int,
from_cp: ControlPoint,
to_cp: ControlPoint
to_cp: ControlPoint,
) -> None:
if not self.game.settings.perf_moving_units:
return
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))
if group.role == CombatGroupRole.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:
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]:
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]:
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
@ -464,11 +518,16 @@ class GroundConflictGenerator:
if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back
# 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)
else:
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(reposition_point, PointAction.OffRoad)
@ -490,8 +549,10 @@ class GroundConflictGenerator:
# We add a new retreat waypoint
dcs_group.add_waypoint(
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)),
PointAction.OffRoad
self.find_retreat_point(
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
),
PointAction.OffRoad,
)
# Fallback task
@ -515,7 +576,7 @@ class GroundConflictGenerator:
self,
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int = RETREAT_DISTANCE
distance: int = RETREAT_DISTANCE,
) -> Point:
"""
Find a point to retreat to
@ -523,17 +584,15 @@ class GroundConflictGenerator:
:param frontline_heading: Heading of the frontline
: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):
return desired_point
return self.conflict.theater.nearest_land_pos(desired_point)
def find_offensive_point(
self,
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int
self, dcs_group: VehicleGroup, frontline_heading: int, distance: int
) -> Point:
"""
Find a point to attack
@ -542,7 +601,9 @@ class GroundConflictGenerator:
:param distance: Distance of the offensive (how far unit should move)
: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):
return desired_point
return self.conflict.theater.nearest_land_pos(desired_point)
@ -551,7 +612,7 @@ class GroundConflictGenerator:
def find_n_nearest_enemy_groups(
player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
n: int
n: int,
) -> List[VehicleGroup]:
"""
Return the nearest enemy group for the player group
@ -562,7 +623,9 @@ class GroundConflictGenerator:
targets = [] # type: List[Optional[VehicleGroup]]
sorted_list = sorted(
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):
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
@ -574,8 +637,7 @@ class GroundConflictGenerator:
@staticmethod
def find_nearest_enemy_group(
player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
player_group: VehicleGroup, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
) -> Optional[VehicleGroup]:
"""
Search the enemy groups for a potential target suitable to armored assault
@ -585,7 +647,9 @@ class GroundConflictGenerator:
min_distance = 99999999
target = None
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:
min_distance = dist
target = dcs_group
@ -595,7 +659,7 @@ class GroundConflictGenerator:
def get_artillery_target_in_range(
dcs_group: VehicleGroup,
group: CombatGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
) -> Optional[Point]:
"""
Search the enemy groups for a potential target suitable to an artillery unit
@ -606,7 +670,9 @@ class GroundConflictGenerator:
return None
for _ in range(10):
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:
return potential_target.points[0].position
return None
@ -620,12 +686,12 @@ class GroundConflictGenerator:
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint(
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]:
rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1]
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1],
)
return rg
@ -635,42 +701,46 @@ class GroundConflictGenerator:
combat_width: int,
distance_from_frontline: int,
heading: int,
spawn_heading: int
spawn_heading: int,
):
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
desired_point = shifted.point_from_heading(
spawn_heading,
distance_from_frontline
shifted = conflict_position.point_from_heading(
heading, random.randint(0, combat_width)
)
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(
self,
groups: List[CombatGroup],
frontline_vector: Tuple[Point, int, int],
is_player: bool
is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = []
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
for group in groups:
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:
distance_from_frontline = random.randint(
DISTANCE_FROM_FRONTLINE[group.role][0],
DISTANCE_FROM_FRONTLINE[group.role][1]
DISTANCE_FROM_FRONTLINE[group.role][0],
DISTANCE_FROM_FRONTLINE[group.role][1],
)
final_position = self.get_valid_position_for_group(
position,
combat_width,
distance_from_frontline,
heading,
spawn_heading
position, combat_width, distance_from_frontline, heading, spawn_heading
)
if final_position is not None:
@ -693,7 +763,7 @@ class GroundConflictGenerator:
g,
is_player,
self.mission.country(country),
opposite_heading(spawn_heading)
opposite_heading(spawn_heading),
)
else:
logging.warning(f"Unable to get valid position for {group}")
@ -718,12 +788,14 @@ class GroundConflictGenerator:
logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, cp.id, unit), unit,
position=at,
group_size=count,
heading=heading,
move_formation=move_formation)
side,
namegen.next_unit_name(side, cp.id, unit),
unit,
position=at,
group_size=count,
heading=heading,
move_formation=move_formation,
)
self.unit_map.add_front_line_units(group, cp)

View File

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

View File

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

View File

@ -12,19 +12,21 @@ from game.utils import heading_sum, opposite_heading
FRONTLINE_LENGTH = 80000
class Conflict:
def __init__(self,
theater: ConflictTheater,
from_cp: ControlPoint,
to_cp: ControlPoint,
attackers_side: str,
defenders_side: str,
attackers_country: Country,
defenders_country: Country,
position: Point,
heading: Optional[int] = None,
size: Optional[int] = None
):
def __init__(
self,
theater: ConflictTheater,
from_cp: ControlPoint,
to_cp: ControlPoint,
attackers_side: str,
defenders_side: str,
attackers_country: Country,
defenders_country: Country,
position: Point,
heading: Optional[int] = None,
size: Optional[int] = None,
):
self.attackers_side = attackers_side
self.defenders_side = defenders_side
@ -43,27 +45,49 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline
@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)
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)
@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.
"""
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
left_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)
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
left_position = cls.extend_ground_position(
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))
return left_position, right_heading, distance
@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)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls(
@ -76,12 +100,14 @@ class Conflict:
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
size=distance
size=distance,
)
return conflict
@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"""
extended = initial.point_from_heading(heading, max_distance)
if theater.landmap is None:
@ -92,8 +118,7 @@ class Conflict:
p1 = ShapelyPoint(extended.x, extended.y)
line = LineString([p0, p1])
intersection = line.intersection(
theater.landmap.inclusion_zone_only.boundary)
intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary)
if intersection.is_empty:
# Max extent does not intersect with the boundary of the inclusion
# 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))
@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.
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
@ -123,4 +155,3 @@ class Conflict:
return pos
logging.error("Didn't find ground position ({})!".format(initial))
return None

View File

@ -3,15 +3,20 @@ import random
from dcs.vehicles import Armor
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
: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:
unit_type = random.choice(possible_unit)
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.generate()
return generator.get_generated_group()

View File

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

View File

@ -4,23 +4,48 @@ from gen.sam.group_generator import ShipGroupGenerator
class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add carrier
if len(self.faction.aircraft_carrier) > 0:
carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
self.add_unit(
carrier_type, "Carrier", self.position.x, self.position.y, self.heading
)
else:
return
# Add destroyers escort
if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading)
self.add_unit(
dd_type,
"DD1",
self.position.x + 2500,
self.position.y + 4500,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 2500,
self.position.y - 4500,
self.heading,
)
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
self.add_unit(
dd_type,
"DD3",
self.position.x + 4500,
self.position.y + 8500,
self.heading,
)
self.add_unit(
dd_type,
"DD4",
self.position.x + 4500,
self.position.y - 8500,
self.heading,
)
self.get_generated_group().points[0].speed = 20

View File

@ -20,7 +20,6 @@ if TYPE_CHECKING:
class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
include_frigate = random.choice([True, True, False])
@ -30,17 +29,45 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True
if include_frigate:
self.add_unit(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)
self.add_unit(
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:
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(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
self.add_unit(
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
class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)
def __init__(
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):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
def __init__(
self,
game: Game,
ground_object: TheaterGroundObject,
faction: Faction,
ddtype: ShipType,
):
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype
def generate(self):
self.add_unit(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.add_unit(
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
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, Oliver_Hazzard_Perry_class
)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)
def __init__(
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):
def generate(self):
# Add carrier
if len(self.faction.helicopter_carrier) > 0:
carrier_type = random.choice(self.faction.helicopter_carrier)
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading)
self.add_unit(
carrier_type, "LHA", self.position.x, self.position.y, self.heading
)
# Add destroyers escort
if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
self.add_unit(
dd_type,
"DD1",
self.position.x + 1250,
self.position.y + 1450,
self.heading,
)
self.add_unit(
dd_type,
"DD2",
self.position.x + 1250,
self.position.y - 1450,
self.heading,
)
self.get_generated_group().points[0].speed = 20

View File

@ -9,7 +9,7 @@ from dcs.ships import (
FF_1135M_Rezky,
CG_1164_Moskva,
SSK_877,
SSK_641B
SSK_641B,
)
from gen.fleet.dd_group import DDGroupGenerator
@ -23,7 +23,6 @@ if TYPE_CHECKING:
class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
include_frigate = random.choice([True, True, False])
@ -39,37 +38,79 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
if include_frigate:
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(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
self.add_unit(
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:
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(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
self.add_unit(
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:
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# 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
class GrishaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, FFL_1124_4_Grisha
)
class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, FSG_1241_1MP_Molniya
)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877
)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)
def __init__(
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):
def generate(self):
for i in range(random.randint(2, 4)):
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
self.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

View File

@ -4,10 +4,18 @@ import random
from game import db
from gen.fleet.carrier_group import CarrierGroupGenerator
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.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \
KiloSubGroupGenerator, TangoSubGroupGenerator
from gen.fleet.ru_dd_group import (
RussianNavyGroupGenerator,
GrishaGroupGenerator,
MolniyaGroupGenerator,
KiloSubGroupGenerator,
TangoSubGroupGenerator,
)
from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
@ -25,7 +33,7 @@ SHIP_MAP = {
"MolniyaGroupGenerator": MolniyaGroupGenerator,
"KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator
"Type54GroupGenerator": Type54GroupGenerator,
}
@ -42,7 +50,11 @@ def generate_ship_group(game, ground_object, faction_name: str):
generator.generate()
return generator.get_generated_group()
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

View File

@ -6,10 +6,15 @@ from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self):
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):
def generate(self):
# 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)):
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]
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}"
class AircraftAllocator:
"""Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool) -> None:
def __init__(
self,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
) -> None:
self.closest_airfields = closest_airfields
self.global_inventory = global_inventory
self.is_player = is_player
def find_aircraft_for_flight(
self, flight: ProposedFlight
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
"""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
responsible for returning them to the inventory.
"""
return self.find_aircraft_of_type(
flight, aircraft_for_task(flight.task)
)
return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
self,
flight: ProposedFlight,
types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance
@ -171,18 +174,22 @@ class AircraftAllocator:
class PackageBuilder:
"""Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
package_country: str,
start_type: str) -> None:
def __init__(
self,
location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
package_country: str,
start_type: str,
) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package_country = package_country
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
self.allocator = AircraftAllocator(
closest_airfields, global_inventory, is_player
)
self.global_inventory = global_inventory
self.start_type = start_type
@ -203,14 +210,23 @@ class PackageBuilder:
else:
start_type = self.start_type
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
start_type, departure=airfield, arrival=airfield,
divert=self.find_divert_field(aircraft, airfield))
flight = Flight(
self.package,
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)
return True
def find_divert_field(self, aircraft: Type[FlyingType],
arrival: ControlPoint) -> Optional[ControlPoint]:
def find_divert_field(
self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player:
@ -323,8 +339,8 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships())
def _targets_by_range(
self,
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
self, targets: Iterable[MissionTarget]
) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets:
ranges: List[int] = []
@ -430,13 +446,17 @@ class ObjectiveFinder:
def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if
c.is_friendly(self.is_player))
return (
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
)
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if
not c.is_friendly(self.is_player))
return (
c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
)
def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater.
@ -510,23 +530,36 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.vulnerable_control_points():
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
# these out appropriately is done in stagger_missions.
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
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.
for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
front_line,
[
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
ProposedFlight(
FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir
),
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
@ -537,30 +570,46 @@ class CoalitionMissionPlanner:
# Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
sam,
[
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
),
],
)
for group in self.objective_finder.threatening_ships():
yield ProposedMission(group, [
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT,
2,
self.MAX_ANTISHIP_RANGE,
EscortType.AirToAir,
),
],
)
for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(group, [
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
yield ProposedMission(
group,
[
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
# TODO: Max escort range.
ProposedFlight(
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):
flights = [
@ -569,27 +618,37 @@ class CoalitionMissionPlanner:
if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit.
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
self.MAX_OCA_RANGE))
flights.extend([
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
EscortType.Sead),
])
flights.append(
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
)
flights.extend(
[
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
),
]
)
yield ProposedMission(target, flights)
# Plan strike missions.
for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
EscortType.AirToAir),
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
EscortType.Sead),
])
yield ProposedMission(
target,
[
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
),
],
)
def plan_missions(self) -> None:
"""Identifies and plans mission for the turn."""
@ -604,19 +663,23 @@ class CoalitionMissionPlanner:
for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft",
f"{available} {aircraft.id} from {cp}")
self.message("Unused aircraft", f"{available} {aircraft.id} from {cp}")
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
builder: PackageBuilder, missing_types: Set[FlightType],
for_reserves: bool) -> None:
def plan_flight(
self,
mission: ProposedMission,
flight: ProposedFlight,
builder: PackageBuilder,
missing_types: Set[FlightType],
for_reserves: bool,
) -> None:
if not builder.plan_flight(flight):
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
range=flight.max_distance,
task_capability=flight.task,
number=flight.num_aircraft
number=flight.num_aircraft,
)
if for_reserves:
# Reserves are planned for critical missions, so prioritize
@ -626,26 +689,28 @@ class CoalitionMissionPlanner:
self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft(
self, mission: ProposedMission, builder: PackageBuilder,
missing_types: Set[FlightType],
not_attempted: Iterable[ProposedFlight],
reserves: bool) -> None:
self,
mission: ProposedMission,
builder: PackageBuilder,
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
# types to buy.
for flight in not_attempted:
self.plan_flight(mission, flight, builder, missing_types, reserves)
missing_types_str = ", ".join(
sorted([t.name for t in missing_types]))
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft"
self.message(
"Insufficient aircraft",
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(
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool)
for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight):
@ -654,8 +719,7 @@ class CoalitionMissionPlanner:
threats[EscortType.Sead] = True
return threats
def plan_mission(self, mission: ProposedMission,
reserves: bool = False) -> None:
def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player:
@ -669,7 +733,7 @@ class CoalitionMissionPlanner:
self.game.aircraft_inventory,
self.is_player,
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
@ -683,12 +747,12 @@ class CoalitionMissionPlanner:
# If the package does not need escorts they may be pruned.
escorts.append(proposed_flight)
continue
self.plan_flight(mission, proposed_flight, builder, missing_types,
reserves)
self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
# Create flight plans for the main flights of the package so we can
@ -697,8 +761,9 @@ class CoalitionMissionPlanner:
# flights that will rendezvous with their package will be affected by
# the other flights in the package. Escorts will not be able to
# contribute to this.
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
self.is_player)
flight_plan_builder = FlightPlanBuilder(
self.game, builder.package, self.is_player
)
for flight in builder.package.flights:
flight_plan_builder.populate_flight_plan(flight)
@ -708,14 +773,14 @@ class CoalitionMissionPlanner:
# impossible.
assert escort.escort_type is not None
if needed_escorts[escort.escort_type]:
self.plan_flight(mission, escort, builder, missing_types,
reserves)
self.plan_flight(mission, escort, builder, missing_types, reserves)
# Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission.
if missing_types:
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
escorts, reserves)
self.scrub_mission_missing_aircraft(
mission, builder, missing_types, escorts, reserves
)
return
if reserves:
@ -732,8 +797,9 @@ class CoalitionMissionPlanner:
self.ato.add_package(package)
def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int,
margin: int) -> Iterator[timedelta]:
def start_time_generator(
count: int, earliest: int, latest: int, margin: int
) -> Iterator[timedelta]:
interval = (latest - earliest) // count
for time in range(earliest, latest, interval):
error = random.randint(-margin, margin)
@ -744,17 +810,13 @@ class CoalitionMissionPlanner:
FlightType.TARCAP,
}
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
timedelta
)
non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types]
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
non_dca_packages = [
p for p in self.ato.packages if p.primary_task not in dca_types
]
start_time = start_time_generator(
count=len(non_dca_packages),
earliest=5,
latest=90,
margin=5
count=len(non_dca_packages), earliest=5, latest=90, margin=5
)
for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
@ -771,8 +833,7 @@ class CoalitionMissionPlanner:
departure_time = package.mission_departure_time
# Should be impossible for CAPs
if departure_time is None:
logging.error(
f"Could not determine mission end time for {package}")
logging.error(f"Could not determine mission end time for {package}")
continue
previous_cap_end_time[package.target] = departure_time
else:
@ -791,8 +852,6 @@ class CoalitionMissionPlanner:
message to the info panel.
"""
if self.is_player:
self.game.informations.append(
Information(title, text, self.game.turn)
)
self.game.informations.append(Information(title, text, self.game.turn))
else:
logging.info(f"{title}: {text}")

View File

@ -13,7 +13,7 @@ from dcs.helicopters import (
SA342L,
SA342M,
UH_1H,
SH_60B
SH_60B,
)
from dcs.planes import (
AJS37,
@ -83,7 +83,7 @@ from dcs.planes import (
Tu_22M3,
Tu_95MS,
WingLoong_I,
I_16
I_16,
)
from dcs.unittype import FlyingType
@ -367,11 +367,7 @@ TRANSPORT_CAPABLE = [
UH_1H,
]
DRONES = [
MQ_9_Reaper,
RQ_1A_Predator,
WingLoong_I
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:

View File

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

View File

@ -39,22 +39,22 @@ class FlightType(Enum):
class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
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_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
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_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
LOITER = 18
@ -68,9 +68,13 @@ class FlightWaypointType(Enum):
class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: Distance = meters(0)) -> None:
def __init__(
self,
waypoint_type: FlightWaypointType,
x: float,
y: float,
alt: Distance = meters(0),
) -> None:
"""Creates a flight waypoint.
Args:
@ -108,10 +112,13 @@ class FlightWaypoint:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
point.position.y, meters(point.alt))
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
point.position.x,
point.position.y,
meters(point.alt),
)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
@ -135,12 +142,19 @@ class FlightWaypoint:
class Flight:
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
count: int, flight_type: FlightType, start_type: str,
departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint],
custom_name: Optional[str] = None) -> None:
def __init__(
self,
package: Package,
country: str,
unit_type: Type[FlyingType],
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.country = country
self.unit_type = unit_type
@ -161,10 +175,9 @@ class Flight:
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan(
package=package,
flight=self,
custom_waypoints=[]
package=package, flight=self, custom_waypoints=[]
)
@property
@ -182,7 +195,7 @@ class Flight:
return f"[{self.flight_type}] {self.count} x {name}"
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:
return f"{self.custom_name} {self.count} x {name}"
return f"[{self.flight_type}] {self.count} x {name}"

View File

@ -75,7 +75,7 @@ class FlightPlan:
raise NotImplementedError
def edges(
self, until: Optional[FlightWaypoint] = None
self, until: Optional[FlightWaypoint] = None
) -> Iterator[Tuple[FlightWaypoint, FlightWaypoint]]:
"""A list of all paths between waypoints, in order."""
waypoints = self.waypoints
@ -86,8 +86,9 @@ class FlightPlan:
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
def best_speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
def best_speed_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> Speed:
"""Desired ground speed between points a and b."""
factor = 1.0
if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
@ -108,8 +109,7 @@ class FlightPlan:
# near 2000 ft MSL.
return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
return self.best_speed_between_waypoints(a, b)
@property
@ -121,11 +121,10 @@ class FlightPlan:
failed to generate. Nevertheless, we have to defend against it.
"""
raise NotImplementedError
@cached_property
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)
bingo = 1000.0 # Minimum Emergency Fuel
@ -142,18 +141,18 @@ class FlightPlan:
@cached_property
def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan
"""
"""Joker fuel value for the FlightPlan"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from.
"""
if not self.waypoints:
return meters(0)
return max([meters(cp.position.distance_to_point(w.position)) for w in
self.waypoints])
return max(
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
@property
def tot_offset(self) -> timedelta:
@ -164,30 +163,30 @@ class FlightPlan:
"""
return timedelta()
def _travel_time_to_waypoint(
self, destination: FlightWaypoint) -> timedelta:
def _travel_time_to_waypoint(self, destination: FlightWaypoint) -> timedelta:
total = timedelta()
if destination not in self.waypoints:
raise PlanningError(
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):
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
return total
def travel_time_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> timedelta:
return TravelTime.between_points(a.position, b.position,
self.speed_between_waypoints(a, b))
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
return TravelTime.between_points(
a.position, b.position, self.speed_between_waypoints(a, b)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
def request_escort_at(self) -> Optional[FlightWaypoint]:
@ -212,8 +211,7 @@ class FlightPlan:
if takeoff_time is None:
return None
start_time = (takeoff_time - self.estimate_startup() -
self.estimate_ground_ops())
start_time = takeoff_time - self.estimate_startup() - self.estimate_ground_ops()
# In case FP math has given us some barely below zero time, round to
# zero.
@ -269,14 +267,14 @@ class LoiterFlightPlan(FlightPlan):
def push_time(self) -> timedelta:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
def travel_time_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> timedelta:
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
) -> timedelta:
travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold:
return travel_time
@ -321,12 +319,12 @@ class FormationFlightPlan(LoiterFlightPlan):
speeds = []
for previous_waypoint, waypoint in self.edges():
if waypoint in self.package_speed_waypoints:
speeds.append(self.best_speed_between_waypoints(
previous_waypoint, waypoint))
speeds.append(
self.best_speed_between_waypoints(previous_waypoint, waypoint)
)
return min(speeds)
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
if b in self.package_speed_waypoints:
# Should be impossible, as any package with at least one
# FormationFlightPlan flight needs a formation speed.
@ -359,7 +357,7 @@ class FormationFlightPlan(LoiterFlightPlan):
return self.join_time - TravelTime.between_points(
self.hold.position,
self.join.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
GroundSpeed.for_flight(self.flight, self.hold.alt),
)
@property
@ -399,8 +397,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_start_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end:
return self.patrol_end_time
return None
@ -490,8 +487,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end:
return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint)
@ -542,13 +538,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
return {
self.ingress,
self.egress,
self.split,
self.ingress,
self.egress,
self.split,
} | set(self.targets)
def speed_between_waypoints(self, a: FlightWaypoint,
b: FlightWaypoint) -> Speed:
def speed_between_waypoints(self, a: FlightWaypoint, b: FlightWaypoint) -> Speed:
# FlightWaypoint is only comparable by identity, so adding
# target_area_waypoint to package_speed_waypoints is useless.
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
@ -564,10 +559,12 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x,
self.package.target.position.y,
meters(0))
return FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
self.package.target.position.x,
self.package.target.position.y,
meters(0),
)
@property
def travel_time_to_target(self) -> timedelta:
@ -581,14 +578,15 @@ class StrikeFlightPlan(FormationFlightPlan):
# package we need to use the travel time to the same position as
# the others.
total += self.travel_time_between_waypoints(
previous_waypoint, self.target_area_waypoint)
previous_waypoint, self.target_area_waypoint
)
break
total += self.travel_time_between_waypoints(previous_waypoint,
waypoint)
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
else:
raise PlanningError(
f"Did not find destination waypoint {destination} in "
f"waypoints for {self.flight}")
f"waypoints for {self.flight}"
)
return total
@property
@ -597,28 +595,28 @@ class StrikeFlightPlan(FormationFlightPlan):
@property
def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.join, self.ingress)
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
return self.ingress_time - travel_time
@property
def split_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.egress, self.split)
travel_time = self.travel_time_between_waypoints(self.egress, self.split)
return self.egress_time + travel_time
@property
def ingress_time(self) -> timedelta:
tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint)
self.ingress, self.target_area_waypoint
)
return tot - travel_time
@property
def egress_time(self) -> timedelta:
tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints(
self.target_area_waypoint, self.egress)
self.target_area_waypoint, self.egress
)
return tot + travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
@ -664,7 +662,8 @@ class SweepFlightPlan(LoiterFlightPlan):
@property
def sweep_start_time(self) -> timedelta:
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
@property
@ -678,8 +677,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@ -689,7 +687,7 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time - TravelTime.between_points(
self.hold.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:
@ -721,8 +719,7 @@ class CustomFlightPlan(FlightPlan):
return self.package.time_over_target
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
@ -752,9 +749,11 @@ class FlightPlanBuilder:
self.threat_zones = self.game.threat_zone_for(not self.is_player)
def populate_flight_plan(
self, flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None) -> None:
self,
flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None,
) -> None:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package")
@ -763,8 +762,8 @@ class FlightPlanBuilder:
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
def generate_flight_plan(
self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> FlightPlan:
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> FlightPlan:
# TODO: Flesh out mission types.
task = flight.flight_type
if task == FlightType.ANTISHIP:
@ -791,8 +790,7 @@ class FlightPlanBuilder:
return self.generate_sweep(flight)
elif task == FlightType.TARCAP:
return self.generate_tarcap(flight)
raise PlanningError(
f"{task} flight plan generation not implemented")
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
# The simple case is where the target is greater than the ingress
@ -829,6 +827,7 @@ class FlightPlanBuilder:
# | | | |
# +--------------+ +---------------+
from gen.ato import PackageWaypoints
target = self.package.target.position
join_point = self.preferred_join_point()
@ -862,10 +861,9 @@ class FlightPlanBuilder:
def legacy_package_waypoints_impl(self) -> None:
from gen.ato import PackageWaypoints
ingress_point = self._ingress_point(
self._target_heading_to_package_airfield())
egress_point = self._egress_point(
self._target_heading_to_package_airfield())
ingress_point = self._ingress_point(self._target_heading_to_package_airfield())
egress_point = self._egress_point(self._target_heading_to_package_airfield())
join_point = self._rendezvous_point(ingress_point)
split_point = self._rendezvous_point(egress_point)
self.package.waypoints = PackageWaypoints(
@ -877,7 +875,8 @@ class FlightPlanBuilder:
def preferred_join_point(self) -> Optional[Point]:
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):
if not self.threat_zones.threatened(point):
return point
@ -917,9 +916,9 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(building.category, building))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_STRIKE,
targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
)
def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a BAI flight plan.
@ -934,11 +933,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.
@ -960,11 +959,11 @@ class FlightPlanBuilder:
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
"""Generate a BARCAP flight at a given location.
@ -978,10 +977,12 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location, barcap=True)
patrol_alt = meters(random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters)
))
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start, end, patrol_alt)
@ -992,14 +993,16 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, start.position,
patrol_alt),
nav_from=builder.nav_path(end.position, flight.arrival.position,
patrol_alt),
nav_to=builder.nav_path(
flight.departure.position, start.position, patrol_alt
),
nav_from=builder.nav_path(
end.position, flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@ -1012,12 +1015,10 @@ class FlightPlanBuilder:
target = self.package.target.position
heading = self.package.waypoints.join.heading_between_point(target)
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance.meters)
start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight))
@ -1028,18 +1029,21 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, start.position,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(end.position, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, start.position, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
end.position, flight.arrival.position, self.doctrine.ingress_altitude
),
sweep_start=start,
sweep_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def racetrack_for_objective(self, location: MissionTarget,
barcap: bool) -> Tuple[Point, Point]:
def racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.operational_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next*
@ -1052,20 +1056,21 @@ class FlightPlanBuilder:
else:
raise PlanningError("Could not find any enemy airfields")
heading = location.position.heading_between_point(
closest_airfield.position
)
heading = location.position.heading_between_point(closest_airfield.position)
position = ShapelyPoint(self.package.target.position.x,
self.package.target.position.y)
position = ShapelyPoint(
self.package.target.position.x, self.package.target.position.y
)
if barcap:
# BARCAPs should remain far enough back from the enemy that their
# commit range does not enter the enemy's threat zone. Include a 5nm
# buffer.
distance_to_no_fly = meters(
position.distance(self.threat_zones.all)
) - self.doctrine.cap_engagement_range - nautical_miles(5)
distance_to_no_fly = (
meters(position.distance(self.threat_zones.all))
- self.doctrine.cap_engagement_range
- nautical_miles(5)
)
else:
# Other race tracks (TARCAPs, currently) just try to keep some
# distance from the nearest enemy airbase, but since they are by
@ -1075,28 +1080,31 @@ class FlightPlanBuilder:
distance_to_airfield = meters(
closest_airfield.position.distance_to_point(
self.package.target.position
))
)
)
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
distance_to_no_fly)
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
distance_to_no_fly)
min_cap_distance = min(
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
)
max_cap_distance = min(
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
)
end = location.position.point_from_heading(
heading,
random.randint(int(min_cap_distance.meters),
int(max_cap_distance.meters))
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
)
diameter = random.randint(
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)
return start, end
def racetrack_for_frontline(self, origin: Point,
front_line: FrontLine) -> Tuple[Point, Point]:
def racetrack_for_frontline(
self, origin: Point, front_line: FrontLine
) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points
# Find targets waypoints
@ -1105,8 +1113,10 @@ class FlightPlanBuilder:
)
center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading(
heading - 90, random.randint(int(nautical_miles(6).meters),
int(nautical_miles(15).meters))
heading - 90,
random.randint(
int(nautical_miles(6).meters), int(nautical_miles(15).meters)
),
)
combat_width = distance / 2
@ -1132,18 +1142,21 @@ class FlightPlanBuilder:
location = self.package.target
patrol_alt = meters(
random.randint(int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters)))
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
# Create points
builder = WaypointBuilder(flight, self.game, self.is_player)
if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(
flight.departure.position, location)
flight.departure.position, location
)
else:
orbit0p, orbit1p = self.racetrack_for_objective(location,
barcap=False)
orbit0p, orbit1p = self.racetrack_for_objective(location, barcap=False)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return TarCapFlightPlan(
@ -1157,18 +1170,17 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p,
patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
patrol_alt),
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position, patrol_alt),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_dead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
def generate_dead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a DEAD flight at a given location.
Args:
@ -1181,7 +1193,8 @@ class FlightPlanBuilder:
is_sam = isinstance(location, SamGroundObject)
if not is_ewr and not is_sam:
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)
# TODO: Unify these.
@ -1193,8 +1206,9 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_DEAD, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_DEAD, targets
)
def generate_oca_strike(self, flight: Flight) -> StrikeFlightPlan:
"""Generate an OCA Strike flight plan at a given location.
@ -1207,11 +1221,13 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for OCA Strike flight "
f"{flight=} at {location=}.")
f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_OCA_AIRCRAFT)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_OCA_AIRCRAFT
)
def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a runway attack flight plan at a given location.
@ -1224,14 +1240,17 @@ class FlightPlanBuilder:
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for runway bombing flight "
f"{flight=} at {location=}.")
f"{flight=} at {location=}."
)
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_OCA_RUNWAY)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_OCA_RUNWAY
)
def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
def generate_sead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
) -> StrikeFlightPlan:
"""Generate a SEAD flight at a given location.
Args:
@ -1249,16 +1268,19 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_SEAD, targets)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_SEAD, targets
)
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player)
ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.target,
self.package.waypoints.egress)
self.package.waypoints.ingress,
self.package.target,
self.package.waypoints.egress,
)
hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
@ -1269,17 +1291,19 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=ingress,
targets=[target],
egress=egress,
split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(
split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def generate_cas(self, flight: Flight) -> CasFlightPlan:
@ -1294,8 +1318,7 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1],
self.game.theater
location.control_points[0], location.control_points[1], self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
@ -1312,22 +1335,26 @@ class FlightPlanBuilder:
flight=flight,
patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, ingress,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(egress, flight.arrival.position,
self.doctrine.ingress_altitude),
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
ingress, location),
nav_to=builder.nav_path(
flight.departure.position, ingress, self.doctrine.ingress_altitude
),
nav_from=builder.nav_path(
egress, flight.arrival.position, self.doctrine.ingress_altitude
),
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
),
engagement_distance=meters(FRONTLINE_LENGTH) / 2,
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
@staticmethod
def target_waypoint(flight: Flight, builder: WaypointBuilder,
target: StrikeTarget) -> FlightWaypoint:
def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
) -> FlightWaypoint:
if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
return builder.bai_group(target)
elif flight.flight_type == FlightType.DEAD:
@ -1338,8 +1365,9 @@ class FlightPlanBuilder:
return builder.strike_point(target)
@staticmethod
def target_area_waypoint(flight: Flight, location: MissionTarget,
builder: WaypointBuilder) -> FlightWaypoint:
def target_area_waypoint(
flight: Flight, location: MissionTarget, builder: WaypointBuilder
) -> FlightWaypoint:
if flight.flight_type == FlightType.DEAD:
return builder.dead_area(location)
elif flight.flight_type == FlightType.SEAD:
@ -1360,12 +1388,14 @@ class FlightPlanBuilder:
# If the origin airfield is closer to the target than the join
# point, plan the hold point such that it retreats from the origin
# airfield.
return join.point_from_heading(target.heading_between_point(origin),
self.doctrine.push_distance.meters)
return join.point_from_heading(
target.heading_between_point(origin), self.doctrine.push_distance.meters
)
heading_to_join = origin.heading_between_point(join)
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))
if hold_distance >= self.doctrine.push_distance:
# Hold point is between the origin airfield and the join point and
@ -1379,26 +1409,27 @@ class FlightPlanBuilder:
# properly.
origin_to_join = origin.distance_to_point(join)
cos_theta = (
(self.doctrine.hold_distance.meters ** 2 +
origin_to_join ** 2 -
self.doctrine.join_distance.meters ** 2) /
(2 * self.doctrine.hold_distance.meters * origin_to_join)
)
self.doctrine.hold_distance.meters ** 2
+ origin_to_join ** 2
- self.doctrine.join_distance.meters ** 2
) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
try:
theta = math.acos(cos_theta)
except ValueError:
# No solution that maintains hold and join distances. Extend the
# hold point away from the target.
return origin.point_from_heading(
target.heading_between_point(origin),
self.doctrine.hold_distance.meters)
target.heading_between_point(origin), self.doctrine.hold_distance.meters
)
return origin.point_from_heading(heading_to_join - theta,
self.doctrine.hold_distance.meters)
return origin.point_from_heading(
heading_to_join - theta, self.doctrine.hold_distance.meters
)
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
def generate_rtb_waypoint(
self, flight: Flight, arrival: ControlPoint
) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
@ -1409,20 +1440,23 @@ class FlightPlanBuilder:
return builder.land(arrival)
def strike_flightplan(
self, flight: Flight, location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
self,
flight: Flight,
location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None,
) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
target_waypoints: List[FlightWaypoint] = []
if targets is not None:
for target in targets:
target_waypoints.append(
self.target_waypoint(flight, builder, target))
target_waypoints.append(self.target_waypoint(flight, builder, target))
else:
target_waypoints.append(
self.target_area_waypoint(flight, location, builder))
self.target_area_waypoint(flight, location, builder)
)
hold = builder.hold(self._hold_point(flight))
join = builder.join(self.package.waypoints.join)
@ -1434,32 +1468,38 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=hold,
hold_duration=timedelta(minutes=5),
nav_to=builder.nav_path(hold.position, join.position,
self.doctrine.ingress_altitude),
nav_to=builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location),
ingress=builder.ingress(
ingress_type, self.package.waypoints.ingress, location
),
targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location),
split=split,
nav_from=builder.nav_path(split.position, flight.arrival.position,
self.doctrine.ingress_altitude),
nav_from=builder.nav_path(
split.position, flight.arrival.position, self.doctrine.ingress_altitude
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
divert=builder.divert(flight.divert),
)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that retreats from the origin airfield."""
return attack_transition.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position),
self.doctrine.join_distance.meters)
self.package_airfield().position
),
self.doctrine.join_distance.meters,
)
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition)
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:
transition_target_distance = attack_transition.distance_to_point(
@ -1514,13 +1554,9 @@ class FlightPlanBuilder:
# The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields(
self.package.target
)
cache = ObjectiveDistanceCache.get_closest_airfields(self.package.target)
for airfield in cache.operational_airfields:
for flight in self.package.flights:
if flight.departure == airfield:
return airfield
raise RuntimeError(
"Could not find any airfield assigned to this package"
)
raise RuntimeError("Could not find any airfield assigned to this package")

View File

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

View File

@ -36,8 +36,13 @@ class StrikeTarget:
class WaypointBuilder:
def __init__(self, flight: Flight, game: Game, player: bool,
targets: Optional[List[StrikeTarget]] = None) -> None:
def __init__(
self,
flight: Flight,
game: Game,
player: bool,
targets: Optional[List[StrikeTarget]] = None,
) -> None:
self.flight = flight
self.conditions = game.conditions
self.doctrine = game.faction_for(player).doctrine
@ -65,9 +70,7 @@ class WaypointBuilder:
FlightWaypointType.NAV,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
@ -75,10 +78,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Enter theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
meters(0)
FlightWaypointType.TAKEOFF, position.x, position.y, meters(0)
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
@ -98,9 +98,7 @@ class WaypointBuilder:
FlightWaypointType.NAV,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
@ -108,10 +106,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
meters(0)
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0)
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
@ -119,8 +114,7 @@ class WaypointBuilder:
waypoint.pretty_name = "Land"
return waypoint
def divert(self,
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
def divert(self, divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier.
Args:
@ -141,10 +135,7 @@ class WaypointBuilder:
altitude_type = "RADIO"
waypoint = FlightWaypoint(
FlightWaypointType.DIVERT,
position.x,
position.y,
altitude
FlightWaypointType.DIVERT, position.x, position.y, altitude
)
waypoint.alt_type = altitude_type
waypoint.name = "DIVERT"
@ -158,9 +149,7 @@ class WaypointBuilder:
FlightWaypointType.LOITER,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.rendezvous_altitude
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
)
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
@ -172,9 +161,7 @@ class WaypointBuilder:
FlightWaypointType.JOIN,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.ingress_altitude
meters(500) if self.is_helo else self.doctrine.ingress_altitude,
)
waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package"
@ -186,24 +173,24 @@ class WaypointBuilder:
FlightWaypointType.SPLIT,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.ingress_altitude
meters(500) if self.is_helo else self.doctrine.ingress_altitude,
)
waypoint.pretty_name = "Split"
waypoint.description = "Depart from package"
waypoint.name = "SPLIT"
return waypoint
def ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
def ingress(
self,
ingress_type: FlightWaypointType,
position: Point,
objective: MissionTarget,
) -> FlightWaypoint:
waypoint = FlightWaypoint(
ingress_type,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.ingress_altitude
meters(500) if self.is_helo else self.doctrine.ingress_altitude,
)
waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name
@ -217,9 +204,7 @@ class WaypointBuilder:
FlightWaypointType.EGRESS,
position.x,
position.y,
meters(
500
) if self.is_helo else self.doctrine.ingress_altitude
meters(500) if self.is_helo else self.doctrine.ingress_altitude,
)
waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name
@ -244,7 +229,7 @@ class WaypointBuilder:
FlightWaypointType.TARGET_POINT,
target.target.position.x,
target.target.position.y,
meters(0)
meters(0),
)
waypoint.description = description
waypoint.pretty_name = description
@ -269,13 +254,14 @@ class WaypointBuilder:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@staticmethod
def _target_area(name: str, location: MissionTarget,
flyover: bool = False) -> FlightWaypoint:
def _target_area(
name: str, location: MissionTarget, flyover: bool = False
) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
meters(0)
meters(0),
)
waypoint.description = name
waypoint.pretty_name = name
@ -300,7 +286,7 @@ class WaypointBuilder:
FlightWaypointType.CAS,
position.x,
position.y,
meters(500) if self.is_helo else meters(1000)
meters(500) if self.is_helo else meters(1000),
)
waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS"
@ -317,10 +303,7 @@ class WaypointBuilder:
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
position.x,
position.y,
altitude
FlightWaypointType.PATROL_TRACK, position.x, position.y, altitude
)
waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point"
@ -336,18 +319,16 @@ class WaypointBuilder:
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL,
position.x,
position.y,
altitude
FlightWaypointType.PATROL, position.x, position.y, altitude
)
waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end"
return waypoint
def race_track(self, start: Point, end: Point,
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
def race_track(
self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
@ -355,8 +336,10 @@ class WaypointBuilder:
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
return (
self.race_track_start(start, altitude),
self.race_track_end(end, altitude),
)
@staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
@ -367,10 +350,7 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP,
position.x,
position.y,
altitude
FlightWaypointType.INGRESS_SWEEP, position.x, position.y, altitude
)
waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft"
@ -386,18 +366,16 @@ class WaypointBuilder:
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
altitude
FlightWaypointType.EGRESS, position.x, position.y, altitude
)
waypoint.name = "SWEEP END"
waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end"
return waypoint
def sweep(self, start: Point, end: Point,
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
def sweep(
self, start: Point, end: Point, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
@ -405,11 +383,11 @@ class WaypointBuilder:
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude),
self.sweep_end(end, altitude))
return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
def escort(
self, ingress: Point, target: MissionTarget, egress: Point
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package.
Args:
@ -423,16 +401,13 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
meters(
500
) if self.is_helo else self.doctrine.ingress_altitude
meters(500) if self.is_helo else self.doctrine.ingress_altitude,
)
waypoint.name = "TARGET"
waypoint.description = "Escort the package"
@ -450,18 +425,14 @@ class WaypointBuilder:
altitude: Altitude of the waypoint.
"""
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
altitude
FlightWaypointType.NAV, position.x, position.y, altitude
)
waypoint.name = "NAV"
waypoint.description = "NAV"
waypoint.pretty_name = "Nav"
return waypoint
def nav_path(self, a: Point, b: Point,
altitude: Distance) -> List[FlightWaypoint]:
def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path]
@ -488,10 +459,8 @@ class WaypointBuilder:
previous = current
current = nxt
def nav_point_prunable(self, previous: Point, current: Point,
nxt: Point) -> bool:
previous_threatened = self.threat_zones.path_threatened(previous,
current)
def nav_point_prunable(self, previous: Point, current: Point, nxt: Point) -> bool:
previous_threatened = self.threat_zones.path_threatened(previous, current)
next_threatened = self.threat_zones.path_threatened(current, nxt)
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
previous_distance = meters(previous.distance_to_point(current))

View File

@ -15,11 +15,15 @@ class ForcedOptionsGenerator:
self.game = game
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:
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:
# 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.BREAKTHROUGH: [4, 6, 6, 8],
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:
def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = []
self.role = role
@ -60,11 +59,12 @@ class CombatGroup:
class GroundPlanner:
def __init__(self, cp:ControlPoint, game):
def __init__(self, cp: ControlPoint, game):
self.cp = cp
self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
self.connected_enemy_cp = [
cp for cp in self.cp.connected_points if cp.captured != self.cp.captured
]
self.tank_groups: List[CombatGroup] = []
self.apc_group: List[CombatGroup] = []
self.ifv_group: List[CombatGroup] = []
@ -80,7 +80,7 @@ class GroundPlanner:
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]
else:
self.cp.stance = CombatStance.DEFENSIVE
@ -152,12 +152,3 @@ class GroundPlanner:
print("For : #" + str(key))
for group in self.units_per_cp[key]:
print(str(group))

View File

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

View File

@ -2,10 +2,11 @@ from enum import Enum
class CombatStance(Enum):
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
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
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
)
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
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,
ActivateICLSCommand,
EPLRS,
OptAlarmState, FireAtPoint,
OptAlarmState,
FireAtPoint,
)
from dcs.unit import Ship, Unit, Vehicle
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.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject,
BuildingGroundObject,
CarrierGroundObject,
GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject,
LhaGroundObject,
ShipGroundObject,
MissileSiteGroundObject,
)
from game.unitmap import UnitMap
from game.utils import knots, mps
@ -53,8 +57,15 @@ class GenericGroundObjectGenerator:
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.country = country
self.game = game
@ -72,18 +83,22 @@ class GenericGroundObjectGenerator:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
vg = self.m.vehicle_group(
self.country,
group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
vg.units[0].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True
for i, u in enumerate(group.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(),
self.m.string(u.name), u.type)
vehicle = Vehicle(
self.m.next_unit_id(), self.m.string(u.name), u.type
)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
@ -96,7 +111,7 @@ class GenericGroundObjectGenerator:
@staticmethod
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, 'eplrs'):
if hasattr(unit_type, "eplrs"):
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
@ -106,14 +121,13 @@ class GenericGroundObjectGenerator:
else:
group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group,
miz_group: Group) -> None:
self.unit_map.add_ground_object_units(self.ground_object,
persistence_group, miz_group)
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
self.unit_map.add_ground_object_units(
self.ground_object, persistence_group, miz_group
)
class MissileSiteGenerator(GenericGroundObjectGenerator):
def generate(self) -> None:
super(MissileSiteGenerator, self).generate()
# 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)
if 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))
logging.info("Set up fire task for missile group.")
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:
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]:
"""
@ -190,7 +210,8 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
break
else:
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:
if not self.ground_object.is_dead:
@ -228,11 +249,20 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
Used by both CV(N) groups and LHA groups.
"""
def __init__(self, ground_object: GenericCarrierGroundObject,
control_point: ControlPoint, country: Country, game: Game,
mission: Mission, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None:
def __init__(
self,
ground_object: GenericCarrierGroundObject,
control_point: ControlPoint,
country: Country,
game: Game,
mission: Mission,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
icls_alloc: Iterator[int],
runways: Dict[str, RunwayData],
unit_map: UnitMap,
) -> None:
super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object
self.control_point = control_point
@ -245,8 +275,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
# TODO: Require single group?
for group in self.ground_object.groups:
if not group.units:
logging.warning(
f"Found empty carrier group in {self.control_point}")
logging.warning(f"Found empty carrier group in {self.control_point}")
continue
atc = self.radio_registry.alloc_uhf()
@ -270,25 +299,29 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized carrier name: {group.units[0].type}")
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def configure_carrier(self, group: Group,
atc_channel: RadioFrequency) -> ShipGroup:
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
ship_group = self.m.ship_group(
self.country,
group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name),
unit_type_from_name(unit.type))
ship = Ship(
self.m.next_unit_id(),
self.m.string(unit.name),
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
@ -303,7 +336,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
carrier_speed = knots(25) - mps(wind.speed)
for attempt in range(5):
point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000)
brc, 100000 - attempt * 20000
)
if self.game.theater.is_in_sea(point):
group.points[0].speed = carrier_speed.meters_per_second
group.add_waypoint(point, carrier_speed.kph)
@ -314,21 +348,30 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
raise NotImplementedError
@staticmethod
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
callsign: str, icls: int) -> None:
group.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=callsign,
unit_id=group.units[0].id,
aa=False
))
group.points[0].tasks.append(ActivateICLSCommand(
icls, unit_id=group.units[0].id
))
def activate_beacons(
group: ShipGroup, tacan: TacanChannel, callsign: str, icls: int
) -> None:
group.points[0].tasks.append(
ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=callsign,
unit_id=group.units[0].id,
aa=False,
)
)
group.points[0].tasks.append(
ActivateICLSCommand(icls, unit_id=group.units[0].id)
)
def add_runway_data(self, brc: int, atc: RadioFrequency,
tacan: TacanChannel, callsign: str, icls: int) -> None:
def add_runway_data(
self,
brc: int,
atc: RadioFrequency,
tacan: TacanChannel,
callsign: str,
icls: int,
) -> None:
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway".
@ -354,24 +397,25 @@ class CarrierGenerator(GenericCarrierGenerator):
def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type,
self.control_point.name)
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
return unit_type
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
return random.choice(
[
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
]
)
class LhaGenerator(GenericCarrierGenerator):
@ -379,14 +423,16 @@ class LhaGenerator(GenericCarrierGenerator):
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
return random.choice(
[
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
]
)
class ShipObjectGenerator(GenericGroundObjectGenerator):
@ -403,22 +449,23 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group,
first_unit_type: Type[UnitType]) -> None:
group = self.m.ship_group(self.country, group_def.name, first_unit_type,
position=group_def.position,
heading=group_def.units[0].heading)
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
group = self.m.ship_group(
self.country,
group_def.name,
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)
# TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name), unit_type)
ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
@ -436,9 +483,14 @@ class GroundObjectsGenerator:
the appropriate generators.
"""
def __init__(self, mission: Mission, game: Game,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry,
unit_map: UnitMap) -> None:
def __init__(
self,
mission: Mission,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
unit_map: UnitMap,
) -> None:
self.m = mission
self.game = game
self.radio_registry = radio_registry
@ -458,28 +510,44 @@ class GroundObjectsGenerator:
for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m,
self.radio_registry, self.tacan_registry,
self.icls_alloc, self.runways, self.unit_map)
ground_object,
cp,
country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(
ground_object, cp, country, self.game, self.m,
self.radio_registry, self.tacan_registry,
self.icls_alloc, self.runways, self.unit_map)
ground_object,
cp,
country,
self.game,
self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc,
self.runways,
self.unit_map,
)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
ground_object, country, self.game, self.m, self.unit_map
)
else:
generator = GenericGroundObjectGenerator(
ground_object, country, self.game, self.m,
self.unit_map)
ground_object, country, self.game, self.m, self.unit_map
)
generator.generate()

View File

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

View File

@ -6,10 +6,16 @@ from dcs import Point
@dataclass
class PresetLocation:
"""A preset location"""
position: Point
heading: int
id: str
def __str__(self):
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
self.id) + "-" * 10
return (
"-" * 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.v1_group import V1GroupGenerator
MISSILES_MAP = {
"V1GroupGenerator": V1GroupGenerator,
"ScudGenerator": ScudGenerator
}
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
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()
return generator.get_generated_group()
else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
return None
logging.info(
"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):
def __init__(self, game, ground_object, faction):
super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction
@ -14,17 +13,50 @@ class ScudGenerator(GroupGenerator):
def generate(self):
# Scuds
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(
MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#0",
self.position.x,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#1",
self.position.x + 50,
self.position.y + random.randint(1, 8),
self.heading,
)
self.add_unit(
MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#2",
self.position.x + 100,
self.position.y + random.randint(1, 8),
self.heading,
)
# Commander
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
self.heading)
self.add_unit(
Unarmed.Transport_UAZ_469,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
self.heading)
self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
self.position.x + 200, self.position.y + 15, 90)
self.add_unit(
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):
def __init__(self, game, ground_object, faction):
super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction
@ -14,19 +13,54 @@ class V1GroupGenerator(GroupGenerator):
def generate(self):
# 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(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)
self.add_unit(
MissilesSS.V_1_ramp,
"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
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20,
self.heading)
self.add_unit(
Unarmed.Kübelwagen_82,
"Kubel#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# 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.heading)
self.add_unit(
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.position.x + 200, self.position.y + 15, 90)
self.add_unit(
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
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot",
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike",
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra",
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee",
"Zulu", "Zero"]
ALPHA_MILITARY = [
"Alpha",
"Bravo",
"Charlie",
"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 = [
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "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"
]
"SHARK",
"TORTOISE",
"BAT",
"PANGOLIN",
"AARDWOLF",
"MONKEY",
"BUFFALO",
"DOG",
"BOBCAT",
"LYNX",
"PANTHER",
"TIGER",
"LION",
"OWL",
"BUTTERFLY",
"BISON",
"DUCK",
"COBRA",
"MAMBA",
"DOLPHIN",
"PHEASANT",
"ARMADILLLO",
"RACOON",
"ZEBRA",
"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:
number = 0
@ -72,21 +275,36 @@ class NameGenerator:
name_str = flight.custom_name
else:
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
name_str = "{} {}".format(
flight.package.target.name, flight.flight_type)
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type))
name_str = "{} {}".format(flight.package.target.name, flight.flight_type)
return "{}|{}|{}|{}|{}|".format(
name_str,
country.id,
cls.aircraft_number,
parent_base_id,
db.unit_type_name(flight.unit_type),
)
@classmethod
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
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
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
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
def next_basedefense_name():
@ -100,7 +318,9 @@ class NameGenerator:
@classmethod
def next_tanker_name(cls, country: Country, unit_type: UnitType):
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
def next_carrier_name(cls, country: Country):
@ -112,7 +332,11 @@ class NameGenerator:
if len(cls.ANIMALS) == 0:
for i in range(10):
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:
if existing_name == alpha_mil_name:
new_name_generated = False

View File

@ -68,9 +68,10 @@ class Radio:
def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio."""
return (RadioFrequency(x) for x in range(
self.minimum.hertz, self.maximum.hertz, self.step.hertz
))
return (
RadioFrequency(x)
for x in range(self.minimum.hertz, self.maximum.hertz, self.step.hertz)
)
@property
def last_channel(self) -> RadioFrequency:
@ -99,14 +100,12 @@ RADIOS: List[Radio] = [
Radio("SCR-522", 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)),
# 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
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed.
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)),
# Tomcat radios
# # 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)),
@ -114,31 +113,23 @@ RADIOS: List[Radio] = [
# 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
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.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
# P-51 / P-47 Radio
# 4 preset channels (A/B/C/D)
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 UHF", MHz(225), MHz(400), step=MHz(1)),
# MiG-15bis
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
# MiG-19P
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
# UH-1H
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
@ -218,7 +209,8 @@ class RadioRegistry:
# https://github.com/Khopa/dcs_liberation/issues/598
channel = radio.last_channel
logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}.")
f"No more free channels for {radio.name}. Reusing {channel}."
)
return channel
def alloc_uhf(self) -> RadioFrequency:

View File

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

View File

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

View File

@ -37,34 +37,64 @@ class FlakGenerator(AirDefenseGroupGenerator):
for i in range(grid_x):
for j in range(grid_y):
index = index+1
self.add_unit(unit_type, "AAA#" + str(index),
self.position.x + spacing*i + random.randint(1,5),
self.position.y + spacing*j + random.randint(1,5), self.heading)
index = index + 1
self.add_unit(
unit_type,
"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)
# 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):
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
self.add_unit(AirDefence.Maschinensatz_33, "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)
self.add_unit(
AirDefence.Maschinensatz_33,
"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
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#", self.position.x - 35, self.position.y - 20,
self.heading)
self.add_unit(
Unarmed.Kübelwagen_82,
"Kubel#",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Some Opel Blitz trucks
for i 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.position.x + 125 + 15*i + random.randint(1,5),
self.position.y + 15*j + random.randint(1,5), 75)
for i 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.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5),
75,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

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

View File

@ -19,15 +19,25 @@ class KS19Generator(AirDefenseGroupGenerator):
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
for i in range(3):
for j in range(3):
index = index + 1
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j, self.heading)
self.add_unit(
highdigitsams.AAA_100mm_KS_19,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

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

View File

@ -16,9 +16,17 @@ class ZSU57Generator(AirDefenseGroupGenerator):
def generate(self):
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):
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
def range(cls) -> AirDefenseRange:

View File

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

View File

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

View File

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

View File

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

View File

@ -17,27 +17,93 @@ class FreyaGenerator(AirDefenseGroupGenerator):
def generate(self):
# TODO : would be better with the Concrete structure that is supposed to protect it
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
self.add_unit(
AirDefence.EWR_FuMG_401_Freya_LZ,
"EWR#1",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
self.add_unit(
AirDefence.AAA_Flak_Vierling_38,
"AA#" + str(i),
position[0],
position[1],
position[2],
)
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
self.add_unit(
AirDefence.AAA_8_8cm_Flak_18,
"AA#" + str(4 + i),
position[0],
position[1],
position[2],
)
# Command/Logi
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
self.add_unit(
Unarmed.Kübelwagen_82,
"Kubel#1",
self.position.x - 20,
self.position.y - 20,
self.heading,
)
self.add_unit(
Unarmed.Sd_Kfz_7,
"Sdkfz#1",
self.position.x + 20,
self.position.y + 22,
self.heading,
)
self.add_unit(
Unarmed.Sd_Kfz_2,
"Sdkfz#2",
self.position.x - 22,
self.position.y + 20,
self.heading,
)
# Maschinensatz_33 and Kdo.g 40 Telemeter
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
self.add_unit(
AirDefence.Maschinensatz_33,
"Energy#1",
self.position.x + 20,
self.position.y - 20,
self.heading,
)
self.add_unit(
AirDefence.AAA_Kdo_G_40,
"Telemeter#1",
self.position.x + 20,
self.position.y - 10,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#1",
self.position.x + 20,
self.position.y - 14,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#2",
self.position.x + 20,
self.position.y - 22,
self.heading,
)
self.add_unit(
Infantry.Infantry_Mauser_98,
"Inf#3",
self.position.x + 20,
self.position.y - 24,
self.heading + 45,
)
@classmethod
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
# types rather than pydcs groups.
class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
self.go.group_name)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
@ -40,16 +38,27 @@ class GroupGenerator:
def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg
def add_unit(self, unit_type: Type[VehicleType], 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(
self,
unit_type: Type[VehicleType],
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,
unit_type: Type[VehicleType], name: str,
position: Point, heading: int) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(),
f"{group.name}|{name}", unit_type.id)
def add_unit_to_group(
self,
group: unitgroup.VehicleGroup,
unit_type: Type[VehicleType],
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.heading = heading
group.add_unit(unit)
@ -82,31 +91,36 @@ class GroupGenerator:
current_offset = self.heading
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for x in range(1, num_units + 1):
positions.append((
self.position.x + launcher_distance * math.cos(math.radians(current_offset)),
self.position.y + launcher_distance * math.sin(math.radians(current_offset)),
current_offset,
))
positions.append(
(
self.position.x
+ 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
return positions
class ShipGroupGenerator(GroupGenerator):
"""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.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
self.go.group_name)
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
unit = Ship(self.game.next_unit_id(),
f"{self.go.group_name}|{name}", unit_type)
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x
unit.position.y = pos_y
unit.heading = heading

View File

@ -19,10 +19,24 @@ class AvengerGenerator(AirDefenseGroupGenerator):
def generate(self):
num_launchers = random.randint(2, 3)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
self.add_unit(
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -19,10 +19,24 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
def generate(self):
num_launchers = random.randint(2, 4)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
self.add_unit(
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -17,10 +17,28 @@ class GepardGenerator(AirDefenseGroupGenerator):
price = 50
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:
self.add_unit(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)
self.add_unit(
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
def range(cls) -> AirDefenseRange:

View File

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

View File

@ -18,20 +18,51 @@ class HawkGenerator(AirDefenseGroupGenerator):
price = 115
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(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)
self.add_unit(
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
"SR",
self.position.x + 20,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Hawk_PCP,
"PCP",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
"TR",
self.position.x + 40,
self.position.y,
self.heading,
)
# Triple A for close range defense
aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA",
self.position + Point(20, 30), self.heading)
self.add_unit_to_group(
aa_group,
AirDefence.AAA_Vulcan_M163,
"AAA",
self.position + Point(20, 30),
self.heading,
)
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -18,23 +18,51 @@ class HQ7Generator(AirDefenseGroupGenerator):
price = 120
def generate(self):
self.add_unit(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)
self.add_unit(
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
aa_group = self.add_auxiliary_group("AA")
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
"AAA1", self.position + Point(20, 30),
self.heading)
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
"AAA2", self.position - Point(20, 30),
self.heading)
self.add_unit_to_group(
aa_group,
AirDefence.AAA_ZU_23_on_Ural_375,
"AAA1",
self.position + Point(20, 30),
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)
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -19,10 +19,24 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
def generate(self):
num_launchers = random.randint(2, 4)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x, self.position.y, self.heading)
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
self.add_unit(
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -19,24 +19,65 @@ class PatriotGenerator(AirDefenseGroupGenerator):
def generate(self):
# 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(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_ECS_AN_MSQ_104, "MSQ", self.position.x + 30, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_ICC, "ICC", self.position.x + 60, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading)
self.add_unit(
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
"STR",
self.position.x + 30,
self.position.y + 30,
self.heading,
)
self.add_unit(
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
"MRC",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
"MSQ",
self.position.x + 30,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Patriot_ICC,
"ICC",
self.position.x + 60,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Patriot_EPP_III,
"EPP",
self.position.x,
self.position.y + 30,
self.heading,
)
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):
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
aa_group = self.add_auxiliary_group("AA")
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):
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163,
f"SPAAA#{i}", Point(x, y), heading)
self.add_unit_to_group(
aa_group, AirDefence.AAA_Vulcan_M163, f"SPAAA#{i}", Point(x, y), heading
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@ -17,14 +17,34 @@ class RapierGenerator(AirDefenseGroupGenerator):
price = 50
def generate(self):
self.add_unit(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)
self.add_unit(
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)
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):
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
def range(cls) -> AirDefenseRange:

View File

@ -15,9 +15,27 @@ class RolandGenerator(AirDefenseGroupGenerator):
price = 40
def generate(self):
self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
self.add_unit(
AirDefence.SAM_Roland_EWR,
"EWR",
self.position.x + 40,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_Roland_ADS,
"ADS",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
Unarmed.Transport_M818,
"TRUCK",
self.position.x + 80,
self.position.y,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@ -33,28 +33,41 @@ class SA10Generator(AirDefenseGroupGenerator):
def generate(self):
# Search Radar
self.add_unit(self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading)
self.add_unit(
self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading
)
# Search radar for missiles (optionnal)
self.add_unit(self.sr2, "SR2", self.position.x - 40, self.position.y, self.heading)
self.add_unit(
self.sr2, "SR2", self.position.x - 40, self.position.y, self.heading
)
# Command Post
self.add_unit(self.cp, "CP", self.position.x, self.position.y, self.heading)
# 2 Tracking radars
self.add_unit(self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
self.add_unit(
self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading
)
self.add_unit(self.tr2, "TR2", self.position.x + 40, self.position.y - 40,
self.heading)
self.add_unit(
self.tr2, "TR2", self.position.x + 40, self.position.y - 40, self.heading
)
# 2 different launcher type (C & D)
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
positions = self.get_circular_position(
num_launchers, launcher_distance=100, coverage=360
)
for i, position in enumerate(positions):
if i%2 == 0:
self.add_unit(self.ln1, "LN#" + str(i), position[0], position[1], position[2])
if i % 2 == 0:
self.add_unit(
self.ln1, "LN#" + str(i), position[0], position[1], position[2]
)
else:
self.add_unit(self.ln2, "LN#" + str(i), position[0], position[1], position[2])
self.add_unit(
self.ln2, "LN#" + str(i), position[0], position[1], position[2]
)
self.generate_defensive_groups()
@ -67,10 +80,16 @@ class SA10Generator(AirDefenseGroupGenerator):
aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360)
num_launchers, launcher_distance=210, coverage=360
)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(aa_group, AirDefence.SPAAA_ZSU_23_4_Shilka,
f"AA#{i}", Point(x, y), heading)
self.add_unit_to_group(
aa_group,
AirDefence.SPAAA_ZSU_23_4_Shilka,
f"AA#{i}",
Point(x, y),
heading,
)
class Tier2SA10Generator(SA10Generator):
@ -82,10 +101,16 @@ class Tier2SA10Generator(SA10Generator):
pd_group = self.add_auxiliary_group("PD")
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360)
num_launchers, launcher_distance=140, coverage=360
)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
f"PD#{i}", Point(x, y), heading)
self.add_unit_to_group(
pd_group,
AirDefence.SAM_SA_15_Tor_9A331,
f"PD#{i}",
Point(x, y),
heading,
)
class Tier3SA10Generator(SA10Generator):
@ -94,19 +119,31 @@ class Tier3SA10Generator(SA10Generator):
aa_group = self.add_auxiliary_group("AA")
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360)
num_launchers, launcher_distance=210, coverage=360
)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(aa_group, AirDefence.SAM_SA_19_Tunguska_2S6,
f"AA#{i}", Point(x, y), heading)
self.add_unit_to_group(
aa_group,
AirDefence.SAM_SA_19_Tunguska_2S6,
f"AA#{i}",
Point(x, y),
heading,
)
# SA-15 for both shorter range targets and point defense.
pd_group = self.add_auxiliary_group("PD")
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360)
num_launchers, launcher_distance=140, coverage=360
)
for i, (x, y, heading) in enumerate(positions):
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
f"PD#{i}", Point(x, y), heading)
self.add_unit_to_group(
pd_group,
AirDefence.SAM_SA_15_Tor_9A331,
f"PD#{i}",
Point(x, y),
heading,
)
class SA10BGenerator(Tier3SA10Generator):
@ -186,4 +223,4 @@ class SA23Generator(Tier3SA10Generator):
self.tr1 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
self.tr2 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
self.ln1 = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
self.ln2 = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
self.ln2 = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN

View File

@ -17,14 +17,34 @@ class SA11Generator(AirDefenseGroupGenerator):
price = 180
def generate(self):
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
self.add_unit(
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
"SR",
self.position.x + 20,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
"CC",
self.position.x,
self.position.y,
self.heading,
)
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=180)
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=180
)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_11_Buk_LN_9A310M1, "LN#" + str(i), position[0], position[1], position[2])
self.add_unit(
AirDefence.SAM_SA_11_Buk_LN_9A310M1,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@ -17,13 +17,33 @@ class SA13Generator(AirDefenseGroupGenerator):
price = 50
def generate(self):
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading)
self.add_unit(
Unarmed.Transport_UAZ_469,
"UAZ",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
Unarmed.Transport_KAMAZ_43101,
"TRUCK",
self.position.x + 40,
self.position.y,
self.heading,
)
num_launchers = random.randint(2, 3)
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):
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "LN#" + str(i), position[0], position[1], position[2])
self.add_unit(
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@ -15,10 +15,28 @@ class SA15Generator(AirDefenseGroupGenerator):
price = 55
def generate(self):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x + 80, self.position.y, self.heading)
self.add_unit(
AirDefence.SAM_SA_15_Tor_9A331,
"ADS",
self.position.x,
self.position.y,
self.heading,
)
self.add_unit(
Unarmed.Transport_UAZ_469,
"EWR",
self.position.x + 40,
self.position.y,
self.heading,
)
self.add_unit(
Unarmed.Transport_KAMAZ_43101,
"TRUCK",
self.position.x + 80,
self.position.y,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium
return AirDefenseRange.Medium

View File

@ -16,14 +16,31 @@ class SA17Generator(AirDefenseGroupGenerator):
price = 180
def generate(self):
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x + 20, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
self.add_unit(
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
"SR",
self.position.x + 20,
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
"CC",
self.position.x,
self.position.y,
self.heading,
)
positions = self.get_circular_position(3, launcher_distance=140, coverage=180)
for i, position in enumerate(positions):
self.add_unit(highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2, "LN#" + str(i), position[0], position[1],
position[2])
self.add_unit(
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
"LN#" + str(i),
position[0],
position[1],
position[2],
)
@classmethod
def range(cls) -> AirDefenseRange:

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