Merge branch 'develop' into ITAHawkmoon-update-db

This commit is contained in:
C. Perreau 2020-12-13 16:25:52 +01:00 committed by GitHub
commit 95db2aa14f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
359 changed files with 15717 additions and 13979 deletions

View File

@ -37,11 +37,6 @@ jobs:
./venv/scripts/activate ./venv/scripts/activate
mypy gen mypy gen
- name: mypy theater
run: |
./venv/scripts/activate
mypy theater
- name: update build number - name: update build number
run: | run: |
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER) [IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)

View File

@ -43,11 +43,6 @@ jobs:
./venv/scripts/activate ./venv/scripts/activate
mypy gen mypy gen
- name: mypy theater
run: |
./venv/scripts/activate
mypy theater
- name: Build binaries - name: Build binaries
run: | run: |
./venv/scripts/activate ./venv/scripts/activate

View File

@ -1,3 +1,21 @@
# 2.3.0
# Features/Improvements
* **[Campaign Map]** Overhauled the campaign model
* **[Campaign Map]** Possible to add FOB as control points
* **[Campaign AI]** Overhauled AI recruiting behaviour
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
* **[Mission Generator]** Infantry squads on frontline can have manpads
* **[Flight Planner]** Added fighter sweep missions.
* **[Flight Planner]** Added BAI missions.
* **[Flight Planner]** Added anti-ship missions.
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
* **[Culling]** Added possibility to include/exclude carriers from culling zones
* **[QOL]** On liberation startup, your latest save game is loaded automatically
## Fixes :
* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
# 2.2.1 # 2.2.1
# Features/Improvements # Features/Improvements

View File

@ -3,9 +3,9 @@ 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'] WW2_FREE = ['fuel', 'factory', 'ware', 'fob']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp'] WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp'] WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2', FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5', 'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',

View File

@ -36,6 +36,8 @@ class Doctrine:
cas_duration: timedelta cas_duration: timedelta
sweep_distance: int
MODERN_DOCTRINE = Doctrine( MODERN_DOCTRINE = Doctrine(
cap=True, cap=True,
@ -62,6 +64,7 @@ MODERN_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(10), cap_min_distance_from_cp=nm_to_meter(10),
cap_max_distance_from_cp=nm_to_meter(40), cap_max_distance_from_cp=nm_to_meter(40),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(60),
) )
COLDWAR_DOCTRINE = Doctrine( COLDWAR_DOCTRINE = Doctrine(
@ -89,6 +92,7 @@ COLDWAR_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(8), cap_min_distance_from_cp=nm_to_meter(8),
cap_max_distance_from_cp=nm_to_meter(25), cap_max_distance_from_cp=nm_to_meter(25),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(40),
) )
WWII_DOCTRINE = Doctrine( WWII_DOCTRINE = Doctrine(
@ -116,4 +120,5 @@ WWII_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(0), cap_min_distance_from_cp=nm_to_meter(0),
cap_max_distance_from_cp=nm_to_meter(5), cap_max_distance_from_cp=nm_to_meter(5),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(10),
) )

View File

@ -106,7 +106,8 @@ from dcs.planes import (
Tu_95MS, Tu_95MS,
WingLoong_I, WingLoong_I,
Yak_40, Yak_40,
plane_map plane_map,
I_16
) )
from dcs.ships import ( from dcs.ships import (
Armed_speedboat, Armed_speedboat,
@ -115,6 +116,7 @@ from dcs.ships import (
CVN_72_Abraham_Lincoln, CVN_72_Abraham_Lincoln,
CVN_73_George_Washington, CVN_73_George_Washington,
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CVN_75_Harry_S__Truman,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5_Admiral_Kuznetsov_2017,
Dry_cargo_ship_Ivanov, Dry_cargo_ship_Ivanov,
@ -159,15 +161,19 @@ import pydcs_extensions.frenchpack.frenchpack as frenchpack
# PATCH pydcs data with MODS # PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57 from pydcs_extensions.su57.su57 import Su_57
plane_map["A-4E-C"] = A_4E_C plane_map["A-4E-C"] = A_4E_C
plane_map["MB-339PAN"] = MB_339PAN plane_map["MB-339PAN"] = MB_339PAN
plane_map["Rafale_M"] = Rafale_M plane_map["Rafale_M"] = Rafale_M
plane_map["Rafale_A_S"] = Rafale_A_S plane_map["Rafale_A_S"] = Rafale_A_S
plane_map["Rafale_B"] = Rafale_B
plane_map["Su-57"] = Su_57 plane_map["Su-57"] = Su_57
plane_map["Hercules"] = Hercules
vehicle_map["FieldHL"] = frenchpack._FIELD_HIDE vehicle_map["FieldHL"] = frenchpack._FIELD_HIDE
vehicle_map["HARRIERH"] = frenchpack._FIELD_HIDE_SMALL vehicle_map["HARRIERH"] = frenchpack._FIELD_HIDE_SMALL
@ -225,6 +231,11 @@ from this example `Identifier` should be used (which may or may not include cate
For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla is called `AirDefence.SAM_SA_18_Igla_S_MANPADS` For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla is called `AirDefence.SAM_SA_18_Igla_S_MANPADS`
""" """
# This should probably be much higher, but the AI doesn't rollover their budget
# and isn't smart enough to save to repair a critical runway anyway, so it has
# to be cheap enough to repair with a single turn's income.
RUNWAY_REPAIR_COST = 100
""" """
Prices for the aircraft. Prices for the aircraft.
This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable) This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable)
@ -247,6 +258,7 @@ PRICES = {
SpitfireLFMkIX: 14, SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14, SpitfireLFMkIXCW: 14,
I_16: 10,
Bf_109K_4: 14, Bf_109K_4: 14,
FW_190D9: 16, FW_190D9: 16,
FW_190A8: 14, FW_190A8: 14,
@ -274,6 +286,7 @@ PRICES = {
F_16A: 14, F_16A: 14,
F_14A_135_GR: 20, F_14A_135_GR: 20,
F_14B: 24, F_14B: 24,
F_22A: 40,
Tornado_IDS: 20, Tornado_IDS: 20,
Tornado_GR4: 20, Tornado_GR4: 20,
@ -330,6 +343,7 @@ PRICES = {
KJ_2000: 50, KJ_2000: 50,
E_3A: 50, E_3A: 50,
C_130: 25, C_130: 25,
Hercules: 25,
# WW2 # WW2
P_51D_30_NA: 18, P_51D_30_NA: 18,
@ -347,6 +361,7 @@ PRICES = {
# Modded # Modded
Rafale_M: 26, Rafale_M: 26,
Rafale_A_S: 26, Rafale_A_S: 26,
Rafale_B: 26,
# armor # armor
Armor.APC_MTLB: 4, Armor.APC_MTLB: 4,
@ -579,6 +594,7 @@ UNIT_BY_TASK = {
MiG_31, MiG_31,
FA_18C_hornet, FA_18C_hornet,
F_15C, F_15C,
F_22A,
F_14A_135_GR, F_14A_135_GR,
F_14B, F_14B,
F_16A, F_16A,
@ -593,6 +609,7 @@ UNIT_BY_TASK = {
JF_17, JF_17,
F_4E, F_4E,
C_101CC, C_101CC,
I_16,
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
@ -635,6 +652,7 @@ UNIT_BY_TASK = {
P_47D_40, P_47D_40,
RQ_1A_Predator, RQ_1A_Predator,
Rafale_A_S, Rafale_A_S,
Rafale_B,
SA342L, SA342L,
SA342M, SA342M,
SA342Minigun, SA342Minigun,
@ -651,14 +669,14 @@ UNIT_BY_TASK = {
Tu_95MS, Tu_95MS,
UH_1H, UH_1H,
WingLoong_I, WingLoong_I,
Hercules
], ],
Transport: [ Transport: [
IL_76MD, IL_76MD,
An_26B, An_26B,
An_30M, An_30M,
Yak_40, Yak_40,
C_130
C_130,
], ],
Refueling: [ Refueling: [
IL_78M, IL_78M,
@ -1010,6 +1028,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
F_14B: COMMON_OVERRIDE, F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE, F_15C: COMMON_OVERRIDE,
F_111F: COMMON_OVERRIDE, F_111F: COMMON_OVERRIDE,
F_22A: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE, F_16C_50: COMMON_OVERRIDE,
JF_17: COMMON_OVERRIDE, JF_17: COMMON_OVERRIDE,
M_2000C: COMMON_OVERRIDE, M_2000C: COMMON_OVERRIDE,
@ -1054,6 +1073,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
FW_190D9: COMMON_OVERRIDE, FW_190D9: COMMON_OVERRIDE,
FW_190A8: COMMON_OVERRIDE, FW_190A8: COMMON_OVERRIDE,
Bf_109K_4: COMMON_OVERRIDE, Bf_109K_4: COMMON_OVERRIDE,
I_16: COMMON_OVERRIDE,
SpitfireLFMkIXCW: COMMON_OVERRIDE, SpitfireLFMkIXCW: COMMON_OVERRIDE,
SpitfireLFMkIX: COMMON_OVERRIDE, SpitfireLFMkIX: COMMON_OVERRIDE,
A_20G: COMMON_OVERRIDE, A_20G: COMMON_OVERRIDE,
@ -1061,6 +1081,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
MB_339PAN: COMMON_OVERRIDE, MB_339PAN: COMMON_OVERRIDE,
Rafale_M: COMMON_OVERRIDE, Rafale_M: COMMON_OVERRIDE,
Rafale_A_S: COMMON_OVERRIDE, Rafale_A_S: COMMON_OVERRIDE,
Rafale_B: COMMON_OVERRIDE,
OH_58D: COMMON_OVERRIDE, OH_58D: COMMON_OVERRIDE,
F_16A: COMMON_OVERRIDE, F_16A: COMMON_OVERRIDE,
MQ_9_Reaper: COMMON_OVERRIDE, MQ_9_Reaper: COMMON_OVERRIDE,
@ -1069,6 +1090,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
AH_1W: COMMON_OVERRIDE, AH_1W: COMMON_OVERRIDE,
AH_64D: COMMON_OVERRIDE, AH_64D: COMMON_OVERRIDE,
AH_64A: COMMON_OVERRIDE, AH_64A: COMMON_OVERRIDE,
Hercules: COMMON_OVERRIDE,
Su_25TM: { Su_25TM: {
SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410", SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410",
@ -1130,7 +1152,7 @@ TIME_PERIODS = {
} }
REWARDS = { REWARDS = {
"power": 4, "warehouse": 2, "fuel": 2, "ammo": 2, "power": 4, "warehouse": 2, "ware": 2, "fuel": 2, "ammo": 2,
"farp": 1, "fob": 1, "factory": 10, "comms": 10, "oil": 10, "farp": 1, "fob": 1, "factory": 10, "comms": 10, "oil": 10,
"derrick": 8 "derrick": 8
} }
@ -1201,6 +1223,8 @@ def upgrade_to_supercarrier(unit, name: str):
return CVN_72_Abraham_Lincoln return CVN_72_Abraham_Lincoln
elif name == "CVN-73 George Washington": elif name == "CVN-73 George Washington":
return CVN_73_George_Washington return CVN_73_George_Washington
elif name == "CVN-75 Harry S. Truman":
return CVN_75_Harry_S__Truman
else: else:
return CVN_71_Theodore_Roosevelt return CVN_71_Theodore_Roosevelt
elif unit == CV_1143_5_Admiral_Kuznetsov: elif unit == CV_1143_5_Admiral_Kuznetsov:
@ -1221,29 +1245,45 @@ def unit_task(unit: UnitType) -> Optional[Task]:
return None return None
def find_unittype(for_task: Task, country_name: str) -> List[UnitType]: def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]:
return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units] return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units]
def find_infantry(country_name: str) -> List[UnitType]: MANPADS: List[VehicleType] = [
inf = [ AirDefence.SAM_SA_18_Igla_MANPADS,
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, AirDefence.SAM_SA_18_Igla_S_MANPADS,
Infantry.Paratrooper_AKS, AirDefence.Stinger_MANPADS
Infantry.Soldier_RPG, ]
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Soldier_M249, INFANTRY: List[VehicleType] = [
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16, Infantry.Paratrooper_AKS,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Soldier_RPG,
Infantry.Georgian_soldier_with_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Soldier_M249,
Infantry.Infantry_Soldier_Rus, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Paratrooper_RPG_16,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Mauser_98, Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents 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_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
]
def find_manpad(country_name: str) -> List[VehicleType]:
return [x for x in MANPADS if x in FACTIONS[country_name].infantry_units]
def find_infantry(country_name: str, allow_manpad: bool = False) -> List[VehicleType]:
if allow_manpad:
inf = INFANTRY + MANPADS
else:
inf = INFANTRY
return [x for x in inf if x in FACTIONS[country_name].infantry_units] return [x for x in inf if x in FACTIONS[country_name].infantry_units]
@ -1255,7 +1295,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id return unit_type.name and unit_type.name or unit_type.id
def unit_type_from_name(name: str) -> Optional[UnitType]: def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map: if name in vehicle_map:
return vehicle_map[name] return vehicle_map[name]
elif name in plane_map: elif name in plane_map:

View File

@ -1,176 +1,233 @@
from __future__ import annotations
import itertools
import json import json
import logging import logging
import os import os
import threading import threading
import time import time
import typing from collections import defaultdict
from dataclasses import dataclass, field
from typing import (
Any,
Callable,
Dict,
Iterator,
List,
Type,
TYPE_CHECKING,
)
from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.theater import Airfield, ControlPoint
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
from gen.flights.flight import Flight
if TYPE_CHECKING:
from game import Game
DEBRIEFING_LOG_EXTENSION = "log" DEBRIEFING_LOG_EXTENSION = "log"
class DebriefingDeadUnitInfo:
country_id = -1
player_unit = False
type = None
def __init__(self, country_id, player_unit , type): @dataclass(frozen=True)
self.country_id = country_id class AirLosses:
self.player_unit = player_unit player: List[Flight]
self.type = type enemy: List[Flight]
@property
def losses(self) -> Iterator[Flight]:
return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
losses = self.player if player else self.enemy
for loss in losses:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def surviving_flight_members(self, flight: Flight) -> int:
losses = 0
for loss in self.losses:
if loss == flight:
losses += 1
return flight.count - losses
@dataclass
class GroundLosses:
player_front_line: List[FrontLineUnit] = field(default_factory=list)
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
player_buildings: List[Building] = field(default_factory=list)
enemy_buildings: List[Building] = field(default_factory=list)
player_airfields: List[Airfield] = field(default_factory=list)
enemy_airfields: List[Airfield] = field(default_factory=list)
@dataclass(frozen=True)
class StateData:
#: True if the mission ended. If False, the mission exited abnormally.
mission_ended: bool
#: Names of aircraft units that were killed during the mission.
killed_aircraft: List[str]
#: Names of vehicle (and ship) units that were killed during the mission.
killed_ground_units: List[str]
#: Names of static units that were destroyed during the mission.
destroyed_statics: List[str]
#: Mangled names of bases that were captured during the mission.
base_capture_events: List[str]
@classmethod
def from_json(cls, data: Dict[str, Any]) -> StateData:
return cls(
mission_ended=data["mission_ended"],
killed_aircraft=data["killed_aircrafts"],
# Airfields emit a new "dead" event every time a bomb is dropped on
# 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"]
)
def __repr__(self):
return str(self.country_id) + " " + str(self.player_unit) + " " + str(self.type)
class Debriefing: class Debriefing:
def __init__(self, state_data, game): def __init__(self, state_data: Dict[str, Any], game: Game,
self.state_data = state_data unit_map: UnitMap) -> None:
self.killed_aircrafts = state_data["killed_aircrafts"] self.state_data = StateData.from_json(state_data)
self.killed_ground_units = state_data["killed_ground_units"] self.unit_map = unit_map
self.weapons_fired = state_data["weapons_fired"]
self.mission_ended = state_data["mission_ended"]
self.destroyed_units = state_data["destroyed_objects_positions"]
self.__destroyed_units = []
logging.info("--------------------------------")
logging.info("Starting Debriefing preprocessing")
logging.info("--------------------------------")
logging.info(self.base_capture_events)
logging.info(self.killed_aircrafts)
logging.info(self.killed_ground_units)
logging.info(self.weapons_fired)
logging.info(self.mission_ended)
logging.info(self.destroyed_units)
logging.info("--------------------------------")
self.player_country = game.player_country
self.enemy_country = game.enemy_country
self.player_country_id = db.country_id_from_name(game.player_country) self.player_country_id = db.country_id_from_name(game.player_country)
self.enemy_country_id = db.country_id_from_name(game.enemy_country) self.enemy_country_id = db.country_id_from_name(game.enemy_country)
self.dead_aircraft = [] self.air_losses = self.dead_aircraft()
self.dead_units = [] self.ground_losses = self.dead_ground_units()
self.dead_aaa_groups = []
self.dead_buildings = []
for aircraft in self.killed_aircrafts: @property
try: def front_line_losses(self) -> Iterator[FrontLineUnit]:
country = int(aircraft.split("|")[1]) yield from self.ground_losses.player_front_line
type = db.unit_type_from_name(aircraft.split("|")[4]) yield from self.ground_losses.enemy_front_line
player_unit = (country == self.player_country_id)
aircraft = DebriefingDeadUnitInfo(country, player_unit, type)
if type is not None:
self.dead_aircraft.append(aircraft)
except Exception as e:
logging.error(e)
for unit in self.killed_ground_units: @property
try: def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
country = int(unit.split("|")[1]) yield from self.ground_losses.player_ground_objects
type = db.unit_type_from_name(unit.split("|")[4]) yield from self.ground_losses.enemy_ground_objects
player_unit = (country == self.player_country_id)
unit = DebriefingDeadUnitInfo(country, player_unit, type)
if type is not None:
self.dead_units.append(unit)
except Exception as e:
logging.error(e)
for unit in self.killed_ground_units: @property
for cp in game.theater.controlpoints: def building_losses(self) -> Iterator[Building]:
yield from self.ground_losses.player_buildings
yield from self.ground_losses.enemy_buildings
logging.info(cp.name) @property
logging.info(cp.captured) def damaged_runways(self) -> Iterator[Airfield]:
yield from self.ground_losses.player_airfields
yield from self.ground_losses.enemy_airfields
if cp.captured: def casualty_count(self, control_point: ControlPoint) -> int:
country = self.player_country_id 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]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
if player:
losses = self.ground_losses.player_front_line
else:
losses = self.ground_losses.enemy_front_line
for loss in losses:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_buildings
else:
losses = self.ground_losses.enemy_buildings
for loss in losses:
if loss.ground_object.control_point.captured != player:
continue
losses_by_type[loss.ground_object.dcs_identifier] += 1
return losses_by_type
def dead_aircraft(self) -> AirLosses:
player_losses = []
enemy_losses = []
for unit_name in self.state_data.killed_aircraft:
flight = self.unit_map.flight(unit_name)
if flight is None:
logging.error(f"Could not find Flight matching {unit_name}")
continue
if flight.departure.captured:
player_losses.append(flight)
else:
enemy_losses.append(flight)
return AirLosses(player_losses, enemy_losses)
def dead_ground_units(self) -> GroundLosses:
losses = GroundLosses()
for unit_name in self.state_data.killed_ground_units:
front_line_unit = self.unit_map.front_line_unit(unit_name)
if front_line_unit is not None:
if front_line_unit.origin.captured:
losses.player_front_line.append(front_line_unit)
else: else:
country = self.enemy_country_id losses.enemy_front_line.append(front_line_unit)
player_unit = (country == self.player_country_id) continue
for i, ground_object in enumerate(cp.ground_objects): ground_object_unit = self.unit_map.ground_object_unit(unit_name)
logging.info(unit) if ground_object_unit is not None:
logging.info(ground_object.group_name) if ground_object_unit.ground_object.control_point.captured:
if ground_object.is_same_group(unit): losses.player_ground_objects.append(ground_object_unit)
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier) else:
self.dead_buildings.append(unit) losses.enemy_ground_objects.append(ground_object_unit)
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]: continue
for g in ground_object.groups:
for u in g.units:
if u.name == unit:
unit = DebriefingDeadUnitInfo(country, player_unit, db.unit_type_from_name(u.type))
self.dead_units.append(unit)
self.player_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.player_country_id] building = self.unit_map.building_or_fortification(unit_name)
self.enemy_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.enemy_country_id] if building is not None:
self.player_dead_units = [a for a in self.dead_units if a.country_id == self.player_country_id] if building.ground_object.control_point.captured:
self.enemy_dead_units = [a for a in self.dead_units if a.country_id == self.enemy_country_id] losses.player_buildings.append(building)
self.player_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.player_country_id] else:
self.enemy_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.enemy_country_id] losses.enemy_buildings.append(building)
continue
logging.info(self.player_dead_aircraft) airfield = self.unit_map.airfield(unit_name)
logging.info(self.enemy_dead_aircraft) if airfield is not None:
logging.info(self.player_dead_units) if airfield.captured:
logging.info(self.enemy_dead_units) losses.player_airfields.append(airfield)
else:
losses.enemy_airfields.append(airfield)
continue
self.player_dead_aircraft_dict = {} # Only logging as debug because we don't currently track infantry
for a in self.player_dead_aircraft: # deaths, so we expect to see quite a few unclaimed dead ground
if a.type in self.player_dead_aircraft_dict.keys(): # units. We should start tracking those and covert this to a
self.player_dead_aircraft_dict[a.type] = self.player_dead_aircraft_dict[a.type] + 1 # warning.
else: logging.debug(f"Death of untracked ground unit {unit_name} will "
self.player_dead_aircraft_dict[a.type] = 1 "have no effect. This may be normal behavior.")
self.enemy_dead_aircraft_dict = {} return losses
for a in self.enemy_dead_aircraft:
if a.type in self.enemy_dead_aircraft_dict.keys():
self.enemy_dead_aircraft_dict[a.type] = self.enemy_dead_aircraft_dict[a.type] + 1
else:
self.enemy_dead_aircraft_dict[a.type] = 1
self.player_dead_units_dict = {}
for a in self.player_dead_units:
if a.type in self.player_dead_units_dict.keys():
self.player_dead_units_dict[a.type] = self.player_dead_units_dict[a.type] + 1
else:
self.player_dead_units_dict[a.type] = 1
self.enemy_dead_units_dict = {}
for a in self.enemy_dead_units:
if a.type in self.enemy_dead_units_dict.keys():
self.enemy_dead_units_dict[a.type] = self.enemy_dead_units_dict[a.type] + 1
else:
self.enemy_dead_units_dict[a.type] = 1
self.player_dead_buildings_dict = {}
for a in self.player_dead_buildings:
if a.type in self.player_dead_buildings_dict.keys():
self.player_dead_buildings_dict[a.type] = self.player_dead_buildings_dict[a.type] + 1
else:
self.player_dead_buildings_dict[a.type] = 1
self.enemy_dead_buildings_dict = {}
for a in self.enemy_dead_buildings:
if a.type in self.enemy_dead_buildings_dict.keys():
self.enemy_dead_buildings_dict[a.type] = self.enemy_dead_buildings_dict[a.type] + 1
else:
self.enemy_dead_buildings_dict[a.type] = 1
logging.info("--------------------------------")
logging.info("Debriefing pre process results :")
logging.info("--------------------------------")
logging.info(self.player_dead_aircraft_dict)
logging.info(self.enemy_dead_aircraft_dict)
logging.info(self.player_dead_units_dict)
logging.info(self.enemy_dead_units_dict)
logging.info(self.player_dead_buildings_dict)
logging.info(self.enemy_dead_buildings_dict)
@property @property
def base_capture_events(self): def base_capture_events(self):
"""Keeps only the last instance of a base capture event for each base ID""" """Keeps only the last instance of a base capture event for each base ID."""
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]] reversed_captures = list(reversed(self.state_data.base_capture_events))
last_base_cap_indexes = [] last_base_cap_indexes = []
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures): for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base in [x[1] for x in last_base_cap_indexes]: if base not in [x[1] for x in last_base_cap_indexes]:
continue
else:
last_base_cap_indexes.append((idx, base)) last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes] return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
@ -179,11 +236,13 @@ class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check """Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition.""" regularly for the stopped() condition."""
def __init__(self, callback: typing.Callable, game): def __init__(self, callback: Callable[[Debriefing], None],
super(PollDebriefingFileThread, self).__init__() game: Game, unit_map: UnitMap) -> None:
super().__init__()
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.callback = callback self.callback = callback
self.game = game self.game = game
self.unit_map = unit_map
def stop(self): def stop(self):
self._stop_event.set() self._stop_event.set()
@ -200,14 +259,14 @@ class PollDebriefingFileThread(threading.Thread):
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified: if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
with open("state.json", "r") as json_file: with open("state.json", "r") as json_file:
json_data = json.load(json_file) json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game) debriefing = Debriefing(json_data, self.game, self.unit_map)
self.callback(debriefing) self.callback(debriefing)
break break
time.sleep(5) time.sleep(5)
def wait_for_debriefing(callback: typing.Callable, game)->PollDebriefingFileThread: def wait_for_debriefing(callback: Callable[[Debriefing], None],
thread = PollDebriefingFileThread(callback, game) game: Game, unit_map) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start() thread.start()
return thread return thread

14
game/event/airwar.py Normal file
View File

@ -0,0 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .event import Event
if TYPE_CHECKING:
from game.theater import ConflictTheater
class AirWarEvent(Event):
"""Event handler for the air battle"""
def __str__(self):
return "AirWar"

View File

@ -2,22 +2,25 @@ from __future__ import annotations
import logging import logging
import math import math
from typing import Dict, List, Optional, Type, TYPE_CHECKING from typing import Dict, List, TYPE_CHECKING, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import Task from dcs.task import Task
from dcs.unittype import UnitType from dcs.unittype import UnitType
from game import db, persistency from game import persistency
from game.debriefing import Debriefing from game.debriefing import AirLosses, Debriefing
from game.infos.information import Information from game.infos.information import Information
from game.operation.operation import Operation from game.operation.operation import Operation
from game.theater import ControlPoint
from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
from ..game import Game from ..game import Game
DIFFICULTY_LOG_BASE = 1.1 DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000 EVENT_DEPARTURE_MAX_DISTANCE = 340000
@ -30,21 +33,16 @@ STRONG_DEFEAT_INFLUENCE = 0.5
class Event: class Event:
silent = False silent = False
informational = False informational = False
is_awacs_enabled = False
ca_slots = 0
game = None # type: Game game = None # type: Game
location = None # type: Point location = None # type: Point
from_cp = None # type: ControlPoint from_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint to_cp = None # type: ControlPoint
operation = None # type: Operation
difficulty = 1 # type: int difficulty = 1 # type: int
BONUS_BASE = 5 BONUS_BASE = 5
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
self.game = game self.game = game
self.departure_cp: Optional[ControlPoint] = None
self.from_cp = from_cp self.from_cp = from_cp
self.to_cp = target_cp self.to_cp = target_cp
self.location = location self.location = location
@ -55,131 +53,130 @@ class Event:
def is_player_attacking(self) -> bool: def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_name return self.attacker_name == self.game.player_name
@property
def enemy_cp(self) -> Optional[ControlPoint]:
if self.attacker_name == self.game.player_name:
return self.to_cp
else:
return self.departure_cp
@property @property
def tasks(self) -> List[Type[Task]]: def tasks(self) -> List[Type[Task]]:
return [] return []
@property
def global_cp_available(self) -> bool:
return False
def is_departure_available_from(self, cp: ControlPoint) -> bool:
if not cp.captured:
return False
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
return False
if cp.is_global and not self.global_cp_available:
return False
return True
def bonus(self) -> int: def bonus(self) -> int:
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE) return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
def is_successfull(self, debriefing: Debriefing) -> bool: def generate(self) -> UnitMap:
return self.operation.is_successfull(debriefing) Operation.prepare(self.game)
unit_map = Operation.generate()
Operation.current_mission.save(
persistency.mission_path_for("liberation_nextturn.miz"))
return unit_map
def generate(self): @staticmethod
self.operation.is_awacs_enabled = self.is_awacs_enabled def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
self.operation.ca_slots = self.ca_slots for_player: bool) -> None:
for package in ato.packages:
self.operation.prepare(self.game.theater.terrain, is_quick=False) for flight in package.flights:
self.operation.generate() # No need to transfer to the same location.
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz")) if flight.departure == flight.arrival:
self.environment_settings = self.operation.environment_settings
def commit(self, debriefing: Debriefing):
logging.info("Commiting mission results")
# ------------------------------
# Destroyed aircrafts
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
for destroyed_aircraft in debriefing.killed_aircrafts:
try:
cpid = int(destroyed_aircraft.split("|")[3])
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
if cpid in cp_map.keys():
cp = cp_map[cpid]
if type in cp.base.aircraft.keys():
logging.info("Aircraft destroyed : " + str(type))
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
except Exception as e:
print(e)
# ------------------------------
# Destroyed ground units
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
for killed_ground_unit in debriefing.killed_ground_units:
try:
cpid = int(killed_ground_unit.split("|")[3])
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
if cpid in cp_map.keys():
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
cp = cp_map[cpid]
if type in cp.base.armor.keys():
logging.info("Ground unit destroyed : " + str(type))
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
except Exception as e:
print(e)
# ------------------------------
# Static ground objects
for destroyed_ground_unit_name in debriefing.killed_ground_units:
for cp in self.game.theater.controlpoints:
if not cp.ground_objects:
continue continue
# -- Static ground objects # Don't transfer to bases that were captured. Note that if the
for i, ground_object in enumerate(cp.ground_objects): # airfield was back-filling transfers it may overflow. We could
if ground_object.is_dead: # attempt to be smarter in the future by performing transfers in
continue # order up a graph to prevent transfers to full airports and
# send overflow off-map, but overflow is fine for now.
if flight.arrival.captured != for_player:
logging.info(
f"Not transferring {flight} because {flight.arrival} "
"was captured")
continue
if ( transfer_count = losses.surviving_flight_members(flight)
(ground_object.group_name == destroyed_ground_unit_name) if transfer_count < 0:
or logging.error(f"{flight} had {flight.count} aircraft but "
(ground_object.is_same_group(destroyed_ground_unit_name)) f"{transfer_count} losses were recorded.")
): continue
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
cp.ground_objects[i].is_dead = True
info = Information("Building destroyed", aircraft = flight.unit_type
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name, available = flight.departure.base.total_units_of_type(aircraft)
self.game.turn) if available < transfer_count:
self.game.informations.append(info) logging.error(
f"Found killed {aircraft} from {flight.departure} but "
f"that airbase has only {available} available.")
continue
flight.departure.base.aircraft[aircraft] -= transfer_count
if aircraft not in flight.arrival.base.aircraft:
# TODO: Should use defaultdict.
flight.arrival.base.aircraft[aircraft] = 0
flight.arrival.base.aircraft[aircraft] += transfer_count
# -- AA Site groups def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
destroyed_units = 0 self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
info = Information("Units destroyed at " + ground_object.obj_name, for_player=True)
"", self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
self.game.turn) for_player=False)
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]: @staticmethod
for g in ground_object.groups: def commit_air_losses(debriefing: Debriefing) -> None:
if not hasattr(g, "units_losts"): for loss in debriefing.air_losses.losses:
g.units_losts = [] aircraft = loss.unit_type
for u in g.units: cp = loss.departure
if u.name == destroyed_ground_unit_name: available = cp.base.total_units_of_type(aircraft)
g.units.remove(u) if available <= 0:
g.units_losts.append(u) logging.error(
destroyed_units = destroyed_units + 1 f"Found killed {aircraft} from {cp} but that airbase has "
info.text = u.type "none available.")
ucount = sum([len(g.units) for g in ground_object.groups]) continue
if ucount == 0:
ground_object.is_dead = True logging.info(f"{aircraft} destroyed from {cp}")
if destroyed_units > 0: cp.base.aircraft[aircraft] -= 1
self.game.informations.append(info)
@staticmethod
def commit_front_line_losses(debriefing: Debriefing) -> None:
for loss in debriefing.front_line_losses:
unit_type = loss.unit_type
control_point = loss.origin
available = control_point.base.total_units_of_type(unit_type)
if available <= 0:
logging.error(
f"Found killed {unit_type} from {control_point} but that "
"airbase has none available.")
continue
logging.info(f"{unit_type} destroyed from {control_point}")
control_point.base.armor[unit_type] -= 1
@staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None:
for loss in debriefing.ground_object_losses:
# TODO: This should be stored in the TGO, not in the pydcs Group.
if not hasattr(loss.group, "units_losts"):
loss.group.units_losts = []
loss.group.units.remove(loss.unit)
loss.group.units_losts.append(loss.unit)
if not loss.ground_object.alive_unit_count:
loss.ground_object.is_dead = True
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
loss.ground_object.is_dead = True
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:
for damaged_runway in debriefing.damaged_runways:
damaged_runway.damage_runway()
def commit(self, debriefing: Debriefing):
logging.info("Committing mission results")
self.commit_air_losses(debriefing)
self.commit_front_line_losses(debriefing)
self.commit_ground_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
# ------------------------------ # ------------------------------
# Captured bases # Captured bases
@ -215,14 +212,14 @@ class Event:
for cp in captured_cps: for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name) logging.info("Will run redeploy for " + cp.name)
self.redeploy_units(cp) self.redeploy_units(cp)
except Exception:
logging.exception(f"Could not process base capture {captured}")
self.complete_aircraft_transfers(debriefing)
except Exception as e:
print(e)
# Destroyed units carcass # Destroyed units carcass
# ------------------------- # -------------------------
for destroyed_unit in debriefing.destroyed_units: for destroyed_unit in debriefing.state_data.destroyed_statics:
self.game.add_destroyed_units(destroyed_unit) self.game.add_destroyed_units(destroyed_unit)
# ----------------------------------- # -----------------------------------
@ -234,8 +231,8 @@ class Event:
delta = 0.0 delta = 0.0
player_won = True player_won = True
ally_casualties = killed_unit_count_by_cp[cp.id] ally_casualties = debriefing.casualty_count(cp)
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id] enemy_casualties = debriefing.casualty_count(enemy_cp)
ally_units_alive = cp.base.total_armor ally_units_alive = cp.base.total_armor
enemy_units_alive = enemy_cp.base.total_armor enemy_units_alive = enemy_cp.base.total_armor
@ -352,11 +349,13 @@ class Event:
logging.info(info.text) logging.info(info.text)
class UnitsDeliveryEvent(Event): class UnitsDeliveryEvent(Event):
informational = True informational = True
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): def __init__(self, attacker_name: str, defender_name: str,
from_cp: ControlPoint, to_cp: ControlPoint,
game: Game) -> None:
super(UnitsDeliveryEvent, self).__init__(game=game, super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position, location=to_cp.position,
from_cp=from_cp, from_cp=from_cp,
@ -364,19 +363,22 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name, attacker_name=attacker_name,
defender_name=defender_name) defender_name=defender_name)
self.units: Dict[UnitType, int] = {} self.units: Dict[Type[UnitType], int] = {}
def __str__(self): def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp) return "Pending delivery to {}".format(self.to_cp)
def deliver(self, units: Dict[UnitType, int]): def deliver(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items(): for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v self.units[k] = self.units.get(k, 0) + v
def skip(self): def skip(self) -> None:
for k, v in self.units.items(): for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn) if self.to_cp.captured:
self.game.informations.append(info) name = "Ally "
else:
name = "Enemy "
self.game.message(
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
self.to_cp.base.commision_units(self.units) self.to_cp.base.commision_units(self.units)

View File

@ -1,49 +1,11 @@
from typing import List, Type
from dcs.task import CAP, CAS, Task
from game import db
from game.operation.frontlineattack import FrontlineAttackOperation
from .event import Event from .event import Event
from ..debriefing import Debriefing
class FrontlineAttackEvent(Event): class FrontlineAttackEvent(Event):
"""
@property An event centered on a FrontLine Conflict.
def tasks(self) -> List[Type[Task]]: Currently the same as its parent, but here for legacy compatibility as well as to allow for
if self.is_player_attacking: future unique Event handling
return [CAS, CAP] """
else:
return [CAP]
@property
def global_cp_available(self) -> bool:
return True
def __str__(self): def __str__(self):
return "Frontline attack" return "Frontline attack"
def is_successfull(self, debriefing: Debriefing):
attackers_success = True
if self.from_cp.captured:
return attackers_success
else:
return not attackers_success
def commit(self, debriefing: Debriefing):
super(FrontlineAttackEvent, self).commit(debriefing)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, flights: db.TaskForceDict):
assert self.departure_cp is not None
op = FrontlineAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
from_cp=self.from_cp,
departure_cp=self.departure_cp,
to_cp=self.to_cp)
self.operation = op

View File

@ -31,31 +31,28 @@ class Faction:
description: str = field(default="") description: str = field(default="")
# Available aircraft # Available aircraft
aircrafts: List[UnitType] = field(default_factory=list) aircrafts: List[Type[FlyingType]] = field(default_factory=list)
# Available awacs aircraft # Available awacs aircraft
awacs: List[UnitType] = field(default_factory=list) awacs: List[Type[FlyingType]] = field(default_factory=list)
# Available tanker aircraft # Available tanker aircraft
tankers: List[UnitType] = field(default_factory=list) tankers: List[Type[FlyingType]] = field(default_factory=list)
# Available frontline units # Available frontline units
frontline_units: List[VehicleType] = field(default_factory=list) frontline_units: List[Type[VehicleType]] = field(default_factory=list)
# Available artillery units # Available artillery units
artillery_units: List[VehicleType] = field(default_factory=list) artillery_units: List[Type[VehicleType]] = field(default_factory=list)
# Infantry units used # Infantry units used
infantry_units: List[VehicleType] = field(default_factory=list) infantry_units: List[Type[VehicleType]] = field(default_factory=list)
# Logistics units used # Logistics units used
logistics_units: List[VehicleType] = field(default_factory=list) logistics_units: List[Type[VehicleType]] = field(default_factory=list)
# List of units that can be deployed as SHORAD
shorads: List[str] = field(default_factory=list)
# Possible SAMS site generators for this faction # Possible SAMS site generators for this faction
sams: List[str] = field(default_factory=list) air_defenses: List[str] = field(default_factory=list)
# Possible EWR generators for this faction. # Possible EWR generators for this faction.
ewrs: List[str] = field(default_factory=list) ewrs: List[str] = field(default_factory=list)
@ -67,10 +64,10 @@ class Faction:
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units # possible aircraft carrier units
aircraft_carrier: List[UnitType] = field(default_factory=list) aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
# possible helicopter carrier units # possible helicopter carrier units
helicopter_carrier: List[UnitType] = field(default_factory=list) helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
# Possible carrier names # Possible carrier names
carrier_names: List[str] = field(default_factory=list) carrier_names: List[str] = field(default_factory=list)
@ -82,10 +79,10 @@ class Faction:
navy_generators: List[str] = field(default_factory=list) navy_generators: List[str] = field(default_factory=list)
# Available destroyers # Available destroyers
destroyers: List[str] = field(default_factory=list) destroyers: List[Type[ShipType]] = field(default_factory=list)
# Available cruisers # Available cruisers
cruisers: List[str] = field(default_factory=list) cruisers: List[Type[ShipType]] = field(default_factory=list)
# How many navy group should we try to generate per CP on startup for this faction # How many navy group should we try to generate per CP on startup for this faction
navy_group_count: int = field(default=1) navy_group_count: int = field(default=1)
@ -97,7 +94,7 @@ class Faction:
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction # Unit to use as JTAC for this faction
jtac_unit: Optional[FlyingType] = field(default=None) jtac_unit: Optional[Type[FlyingType]] = field(default=None)
# doctrine # doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE) doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@ -106,7 +103,17 @@ class Faction:
building_set: List[str] = field(default_factory=list) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
liveries_overrides: Dict[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
#: of the time period or operator. For example, the CJTF "countries" don't
#: appear to have GPS capability, so they need this.
#:
#: Note that this option cannot be set per-side. If either faction needs it,
#: both will use it.
unrestricted_satnav: bool = False
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@ -137,9 +144,14 @@ class Faction:
faction.logistics_units = load_all_vehicles( faction.logistics_units = load_all_vehicles(
json.get("logistics_units", [])) json.get("logistics_units", []))
faction.sams = json.get("sams", [])
faction.ewrs = json.get("ewrs", []) faction.ewrs = json.get("ewrs", [])
faction.shorads = json.get("shorads", [])
faction.air_defenses = json.get("air_defenses", [])
# Compatibility for older factions. All air defenses now belong to a
# single group and the generator decides what belongs where.
faction.air_defenses.extend(json.get("sams", []))
faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", []) faction.missiles = json.get("missiles", [])
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
@ -194,16 +206,19 @@ class Faction:
if k is not None: if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v] faction.liveries_overrides[k] = [s.lower() for s in v]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction return faction
@property @property
def units(self) -> List[UnitType]: def units(self) -> List[Type[UnitType]]:
return (self.infantry_units + self.aircrafts + self.awacs + return (self.infantry_units + self.aircrafts + self.awacs +
self.artillery_units + self.frontline_units + self.artillery_units + self.frontline_units +
self.tankers + self.logistics_units) self.tankers + self.logistics_units)
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]: def unit_loader(
unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
""" """
Find unit by name Find unit by name
:param unit: Unit name as string :param unit: Unit name as string
@ -226,13 +241,13 @@ def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
return None return None
def load_aircraft(name: str) -> Optional[FlyingType]: def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(Optional[FlyingType], unit_loader( return cast(Optional[FlyingType], unit_loader(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES] name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
)) ))
def load_all_aircraft(data) -> List[FlyingType]: def load_all_aircraft(data) -> List[Type[FlyingType]]:
items = [] items = []
for name in data: for name in data:
item = load_aircraft(name) item = load_aircraft(name)
@ -241,13 +256,13 @@ def load_all_aircraft(data) -> List[FlyingType]:
return items return items
def load_vehicle(name: str) -> Optional[VehicleType]: def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(Optional[FlyingType], unit_loader( return cast(Optional[FlyingType], unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES] name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
)) ))
def load_all_vehicles(data) -> List[VehicleType]: def load_all_vehicles(data) -> List[Type[VehicleType]]:
items = [] items = []
for name in data: for name in data:
item = load_vehicle(name) item = load_vehicle(name)
@ -256,11 +271,11 @@ def load_all_vehicles(data) -> List[VehicleType]:
return items return items
def load_ship(name: str) -> Optional[ShipType]: def load_ship(name: str) -> Optional[Type[ShipType]]:
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships])) return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
def load_all_ships(data) -> List[ShipType]: def load_all_ships(data) -> List[Type[ShipType]]:
items = [] items = []
for name in data: for name in data:
item = load_ship(name) item = load_ship(name)

View File

@ -1,14 +1,13 @@
import logging import logging
import math
import random import random
import sys import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from enum import Enum
from typing import Dict, List from typing import Dict, List
from dcs.action import Coalition from dcs.action import Coalition
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike, Task from dcs.task import CAP, CAS, PinpointStrike
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from game import db from game import db
@ -21,15 +20,16 @@ from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner import GroundPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner
from theater import ConflictTheater, ControlPoint
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from . import persistency from . import persistency
from .debriefing import Debriefing from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction from .factions.faction import Faction
from .infos.information import Information from .infos.information import Information
from .procurement import ProcurementAi
from .settings import Settings from .settings import Settings
from .theater import ConflictTheater, ControlPoint
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4 COMMISION_UNIT_VARIETY = 4
@ -62,17 +62,19 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
# cost of AWACS for single operation # cost of AWACS for single operation
AWACS_BUDGET_COST = 4 AWACS_BUDGET_COST = 4
# Initial budget value
PLAYER_BUDGET_INITIAL = 650
# Bonus multiplier logarithm base # Bonus multiplier logarithm base
PLAYER_BUDGET_IMPORTANCE_LOG = 2 PLAYER_BUDGET_IMPORTANCE_LOG = 2
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2
class Game: class Game:
def __init__(self, player_name: str, enemy_name: str, def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime, theater: ConflictTheater, start_date: datetime,
settings: Settings): settings: Settings, player_budget: int,
enemy_budget: int) -> None:
self.settings = settings self.settings = settings
self.events: List[Event] = [] self.events: List[Event] = []
self.theater = theater self.theater = theater
@ -87,10 +89,12 @@ class Game:
self.ground_planners: Dict[int, GroundPlanner] = {} self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = [] self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0)) self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points = self.compute_conflicts_position() self.__culling_points: List[Point] = []
self.compute_conflicts_position()
self.__destroyed_units: List[str] = [] self.__destroyed_units: List[str] = []
self.savepath = "" self.savepath = ""
self.budget = PLAYER_BUDGET_INITIAL self.budget = player_budget
self.enemy_budget = enemy_budget
self.current_unit_id = 0 self.current_unit_id = 0
self.current_group_id = 0 self.current_group_id = 0
@ -103,9 +107,24 @@ class Game:
self.theater.controlpoints self.theater.controlpoints
) )
for cp in self.theater.controlpoints:
cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.sanitize_sides() self.sanitize_sides()
self.on_load() self.on_load()
# Turn 0 procurement. We don't actually have any missions to plan, but
# the planner will tell us what it would like to plan so we can use that
# to drive purchase decisions.
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
self.plan_procurement(blue_planner, red_planner)
def generate_conditions(self) -> Conditions: def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date, return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings) self.current_turn_time_of_day, self.settings)
@ -148,23 +167,29 @@ class Game:
front_line.control_point_b) front_line.control_point_b)
@property @property
def budget_reward_amount(self): def budget_reward_amount(self) -> int:
reward = 0 reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
if len(self.theater.player_points()) > 0: for cp in self.theater.player_points():
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points()) for g in cp.ground_objects:
for cp in self.theater.player_points(): if g.category in REWARDS.keys() and not g.is_dead:
for g in cp.ground_objects: reward += REWARDS[g.category]
if g.category in REWARDS.keys(): return int(reward * self.settings.player_income_multiplier)
reward = reward + REWARDS[g.category]
return reward
else:
return reward
def _budget_player(self): def process_player_income(self):
self.budget += self.budget_reward_amount self.budget += self.budget_reward_amount
def awacs_expense_commit(self): def process_enemy_income(self):
self.budget -= AWACS_BUDGET_COST # TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0
production = 0.0
for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects:
if g.category in REWARDS.keys() and not g.is_dead:
production = production + REWARDS[g.category]
self.enemy_budget += production * self.settings.enemy_income_multiplier
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent: def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
event = UnitsDeliveryEvent(attacker_name=self.player_name, event = UnitsDeliveryEvent(attacker_name=self.player_name,
@ -175,20 +200,16 @@ class Game:
self.events.append(event) self.events.append(event)
return event return event
def units_delivery_remove(self, event: Event): def initiate_event(self, event: Event) -> UnitMap:
if event in self.events:
self.events.remove(event)
def initiate_event(self, event: Event):
#assert event in self.events #assert event in self.events
logging.info("Generating {} (regular)".format(event)) logging.info("Generating {} (regular)".format(event))
event.generate() return event.generate()
def finish_event(self, event: Event, debriefing: Debriefing): def finish_event(self, event: Event, debriefing: Debriefing):
logging.info("Finishing event {}".format(event)) logging.info("Finishing event {}".format(event))
event.commit(debriefing) event.commit(debriefing)
if event.is_successfull(debriefing): self.budget += int(event.bonus() *
self.budget += event.bonus() self.settings.player_income_multiplier)
if event in self.events: if event in self.events:
self.events.remove(event) self.events.remove(event)
@ -199,18 +220,12 @@ class Game:
if isinstance(event, Event): if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name return event and event.attacker_name and event.attacker_name == self.player_name
else: else:
return event and event.name and event.name == self.player_name raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self) -> None: def on_load(self) -> None:
LuaPluginManager.load_settings(self.settings) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
# Save game compatibility.
# TODO: Remove in 2.3.
if not hasattr(self, "conditions"):
self.conditions = self.generate_conditions()
def pass_turn(self, no_action: bool = False) -> None: def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn") logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@ -224,8 +239,12 @@ class Game:
else: else:
event.skip() event.skip()
self._enemy_reinforcement() for control_point in self.theater.controlpoints:
self._budget_player() control_point.process_turn()
self.process_enemy_income()
self.process_player_income()
if not no_action and self.turn > 1: if not no_action and self.turn > 1:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
@ -242,6 +261,14 @@ class Game:
# Autosave progress # Autosave progress
persistency.autosave(self) persistency.autosave(self)
def check_win_loss(self):
captured_states = {i.captured for i in self.theater.controlpoints}
if True not in captured_states:
return TurnState.LOSS
if False not in captured_states:
return TurnState.WIN
return TurnState.CONTINUE
def initialize_turn(self) -> None: def initialize_turn(self) -> None:
self.events = [] self.events = []
self._generate_events() self._generate_events()
@ -251,92 +278,56 @@ class Game:
self.aircraft_inventory.reset() self.aircraft_inventory.reset()
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.aircraft_inventory.set_from_control_point(cp) self.aircraft_inventory.set_from_control_point(cp)
# Check for win or loss condition
turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS,TurnState.WIN):
return self.process_win_loss(turn_state)
# Plan flights & combat for next turn # Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position() self.compute_conflicts_position()
self.ground_planners = {} self.ground_planners = {}
self.blue_ato.clear() self.blue_ato.clear()
self.red_ato.clear() self.red_ato.clear()
CoalitionMissionPlanner(self, is_player=True).plan_missions()
CoalitionMissionPlanner(self, is_player=False).plan_missions() blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
if cp.has_frontline: if cp.has_frontline:
gplanner = GroundPlanner(cp, self) gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar() gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner self.ground_planners[cp.id] = gplanner
def _enemy_reinforcement(self): self.plan_procurement(blue_planner, red_planner)
"""
Compute and commision reinforcement for enemy bases
"""
MAX_ARMOR = 30 * self.settings.multiplier def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
MAX_AIRCRAFT = 25 * self.settings.multiplier red_planner: CoalitionMissionPlanner) -> None:
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements
).spend_budget(self.budget, blue_planner.procurement_requests)
production = 0.0 self.enemy_budget = ProcurementAi(
for enemy_point in self.theater.enemy_points(): self,
for g in enemy_point.ground_objects: for_player=False,
if g.category in REWARDS.keys(): faction=self.enemy_faction,
production = production + REWARDS[g.category] manage_runways=True,
manage_front_line=True,
manage_aircraft=True
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
production = production * 0.75 def message(self, text: str) -> None:
budget_for_armored_units = production / 2 self.informations.append(Information(text, turn=self.turn))
budget_for_aircraft = production / 2
potential_cp_armor = []
for cp in self.theater.enemy_points():
for cpe in cp.connected_points:
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
potential_cp_armor.append(cp)
if len(potential_cp_armor) == 0:
potential_cp_armor = self.theater.enemy_points()
i = 0
potential_units = db.FACTIONS[self.enemy_name].frontline_units
print("Enemy Recruiting")
print(potential_cp_armor)
print(budget_for_armored_units)
print(potential_units)
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
while budget_for_armored_units > 0:
i = i + 1
if i > 50 or budget_for_armored_units <= 0:
break
target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_armor >= MAX_ARMOR:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
budget_for_armored_units -= price * 2
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
print(str(info))
self.informations.append(info)
if budget_for_armored_units > 0:
budget_for_aircraft += budget_for_armored_units
potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
while budget_for_aircraft > 0:
i = i + 1
if i > 50 or budget_for_aircraft <= 0:
break
target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_planes >= MAX_AIRCRAFT:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
budget_for_aircraft -= price * 2
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
print(str(info))
self.informations.append(info)
@property @property
def current_turn_time_of_day(self) -> TimeOfDay: def current_turn_time_of_day(self) -> TimeOfDay:
@ -369,13 +360,19 @@ class Game:
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
position = Conflict.frontline_position(self.theater, position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_a, front_line.control_point_b,
front_line.control_point_b) self.theater)
points.append(position[0]) points.append(position[0])
points.append(front_line.control_point_a.position) points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position) points.append(front_line.control_point_b.position)
# If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier:
for cp in self.theater.controlpoints:
if cp.is_carrier or cp.is_lha:
points.append(cp.position)
# If there is no conflict take the center point between the two nearest opposing bases # If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0: if len(points) == 0:
cpoint = None cpoint = None
@ -399,7 +396,7 @@ class Game:
if len(points) == 0: if len(points) == 0:
points.append(Point(0, 0)) points.append(Point(0, 0))
return points self.__culling_points = points
def add_destroyed_units(self, data): def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"]) pos = Point(data["x"], data["z"])
@ -448,3 +445,9 @@ class Game:
def get_enemy_color(self): def get_enemy_color(self):
return "red" return "red"
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.")
elif turn_state is TurnState.LOSS:
return self.message("Game Over, you lose. Start a new campaign to continue.")

View File

@ -1,3 +1,4 @@
import datetime
class Information(): class Information():
@ -5,7 +6,12 @@ class Information():
self.title = title self.title = title
self.text = text self.text = text
self.turn = turn self.turn = turn
self.timestamp = datetime.datetime.now()
def __str__(self): def __str__(self):
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text return '[{}][{}] {} {}'.format(
return s self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
self.turn,
self.title,
self.text
)

View File

@ -1,11 +1,15 @@
"""Inventory management APIs.""" """Inventory management APIs."""
from collections import defaultdict from __future__ import annotations
from typing import Dict, Iterable, Iterator, Set, Tuple
from dcs.unittype import UnitType from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
from gen.flights.flight import Flight from gen.flights.flight import Flight
from theater import ControlPoint
if TYPE_CHECKING:
from game.theater import ControlPoint
class ControlPointAircraftInventory: class ControlPointAircraftInventory:
@ -13,9 +17,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None: def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point self.control_point = control_point
self.inventory: Dict[UnitType, int] = defaultdict(int) self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
def add_aircraft(self, aircraft: UnitType, count: int) -> None: def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Adds aircraft to the inventory. """Adds aircraft to the inventory.
Args: Args:
@ -24,7 +28,7 @@ class ControlPointAircraftInventory:
""" """
self.inventory[aircraft] += count self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: UnitType, count: int) -> None: def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
"""Removes aircraft from the inventory. """Removes aircraft from the inventory.
Args: Args:
@ -43,7 +47,7 @@ class ControlPointAircraftInventory:
) )
self.inventory[aircraft] -= count self.inventory[aircraft] -= count
def available(self, aircraft: UnitType) -> int: def available(self, aircraft: Type[FlyingType]) -> int:
"""Returns the number of available aircraft of the given type. """Returns the number of available aircraft of the given type.
Args: Args:
@ -55,14 +59,14 @@ class ControlPointAircraftInventory:
return 0 return 0
@property @property
def types_available(self) -> Iterator[UnitType]: def types_available(self) -> Iterator[Type[FlyingType]]:
"""Iterates over all available aircraft types.""" """Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
yield aircraft yield aircraft
@property @property
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]: def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
"""Iterates over all available aircraft types, including amounts.""" """Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items(): for aircraft, count in self.inventory.items():
if count > 0: if count > 0:
@ -102,12 +106,14 @@ class GlobalAircraftInventory:
return self.inventories[control_point] return self.inventories[control_point]
@property @property
def available_types_for_player(self) -> Iterator[UnitType]: def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
"""Iterates over all aircraft types available to the player.""" """Iterates over all aircraft types available to the player."""
seen: Set[UnitType] = set() seen: Set[Type[FlyingType]] = set()
for control_point, inventory in self.inventories.items(): for control_point, inventory in self.inventories.items():
if control_point.captured: if control_point.captured:
for aircraft in inventory.types_available: for aircraft in inventory.types_available:
if not control_point.can_operate(aircraft):
continue
if aircraft not in seen: if aircraft not in seen:
seen.add(aircraft) seen.add(aircraft)
yield aircraft yield aircraft

View File

@ -1,4 +1,4 @@
from theater import ControlPoint from game.theater import ControlPoint
class FrontlineData: class FrontlineData:

View File

@ -1,38 +0,0 @@
from dcs.terrain.terrain import Terrain
from gen.conflictgen import Conflict
from .operation import Operation
from .. import db
MAX_DISTANCE_BETWEEN_GROUPS = 12000
class FrontlineAttackOperation(Operation):
interceptors = None # type: db.AssignedUnitsDict
escort = None # type: db.AssignedUnitsDict
strikegroup = None # type: db.AssignedUnitsDict
attackers = None # type: db.ArmorDict
defenders = None # type: db.ArmorDict
def prepare(self, terrain: Terrain, is_quick: bool):
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player_name:
self.attackers_starting_position = None
self.defenders_starting_position = None
conflict = Conflict.frontline_cas_conflict(
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker=self.current_mission.country(self.attacker_country),
defender=self.current_mission.country(self.defender_country),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.current_mission,
conflict=conflict)
def generate(self):
super(FrontlineAttackOperation, self).generate()

View File

@ -1,7 +1,10 @@
from __future__ import annotations
from game.theater.theatergroundobject import TheaterGroundObject
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import List, Optional, Set from typing import TYPE_CHECKING, Iterable, List, Optional, Set
from dcs import Mission from dcs import Mission
from dcs.action import DoScript, DoScriptFile from dcs.action import DoScript, DoScriptFile
@ -9,11 +12,8 @@ from dcs.coalition import Coalition
from dcs.countries import country_dict from dcs.countries import country_dict
from dcs.lua.parse import loads from dcs.lua.parse import loads
from dcs.mapping import Point from dcs.mapping import Point
from dcs.terrain.terrain import Terrain
from dcs.translation import String from dcs.translation import String
from dcs.triggers import TriggerStart from dcs.triggers import TriggerStart
from dcs.unittype import UnitType
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from gen import Conflict, FlightType, VisualGenerator from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
@ -29,19 +29,19 @@ from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from theater import ControlPoint
from .. import db from .. import db
from ..debriefing import Debriefing from ..debriefing import Debriefing
from ..theater import Airfield
from ..unitmap import UnitMap
if TYPE_CHECKING:
from game import Game
class Operation: class Operation:
attackers_starting_position = None # type: db.StartingPosition """Static class for managing the final Mission generation"""
defenders_starting_position = None # type: db.StartingPosition
current_mission = None # type: Mission current_mission = None # type: Mission
regular_mission = None # type: Mission
quick_mission = None # type: Mission
conflict = None # type: Conflict
airgen = None # type: AircraftConflictGenerator airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator airsupportgen = None # type: AirSupportConflictGenerator
@ -51,104 +51,96 @@ class Operation:
forcedoptionsgen = None # type: ForcedOptionsGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None tacan_registry: Optional[TacanRegistry] = None
game = None # type: Game
environment_settings = None environment_settings = None
trigger_radius = TRIGGER_RADIUS_MEDIUM trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None is_quick = None
is_awacs_enabled = False player_awacs_enabled = True
ca_slots = 0 # TODO: #436 Generate Air Support for red
enemy_awacs_enabled = True
ca_slots = 1
unit_map: UnitMap
jtacs: List[JtacInfo] = []
plugin_scripts: List[str] = []
def __init__(self, @classmethod
game, def prepare(cls, game: Game):
attacker_name: str,
defender_name: str,
from_cp: ControlPoint,
departure_cp: ControlPoint,
to_cp: ControlPoint):
self.game = game
self.attacker_name = attacker_name
self.attacker_country = db.FACTIONS[attacker_name].country
self.defender_name = defender_name
self.defender_country = db.FACTIONS[defender_name].country
print(self.defender_country, self.attacker_country)
self.from_cp = from_cp
self.departure_cp = departure_cp
self.to_cp = to_cp
self.is_quick = False
self.plugin_scripts: List[str] = []
def units_of(self, country_name: str) -> List[UnitType]:
return []
def is_successfull(self, debriefing: Debriefing) -> bool:
return True
@property
def is_player_attack(self) -> bool:
return self.from_cp.captured
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
# self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat?
def prepare(self, terrain: Terrain, is_quick: bool):
with open("resources/default_options.lua", "r") as f: with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"] options_dict = loads(f.read())["options"]
cls._set_mission(Mission(game.theater.terrain))
cls.game = game
cls._setup_mission_coalitions()
cls.current_mission.options.load_from_dict(options_dict)
self.current_mission = Mission(terrain) @classmethod
def conflicts(cls) -> Iterable[Conflict]:
assert cls.game
for frontline in cls.game.theater.conflicts():
yield Conflict(
cls.game.theater,
frontline.control_point_a,
frontline.control_point_b,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
frontline.position
)
print(self.game.player_country) @classmethod
print(country_dict[db.country_id_from_name(self.game.player_country)]) def air_conflict(cls) -> Conflict:
print(country_dict[db.country_id_from_name(self.game.player_country)]()) assert cls.game
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
)
return Conflict(
cls.game.theater,
player_cp,
enemy_cp,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
mid_point
)
# Setup coalition : @classmethod
self.current_mission.coalition["blue"] = Coalition("blue") def _set_mission(cls, mission: Mission) -> None:
self.current_mission.coalition["red"] = Coalition("red") cls.current_mission = mission
p_country = self.game.player_country @classmethod
e_country = self.game.enemy_country def _setup_mission_coalitions(cls):
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]()) cls.current_mission.coalition["blue"] = Coalition("blue")
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]()) cls.current_mission.coalition["red"] = Coalition("red")
print([c for c in self.current_mission.coalition["blue"].countries.keys()]) p_country = cls.game.player_country
print([c for c in self.current_mission.coalition["red"].countries.keys()]) e_country = cls.game.enemy_country
cls.current_mission.coalition["blue"].add_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)]())
if is_quick: @classmethod
self.quick_mission = self.current_mission def inject_lua_trigger(cls, contents: str, comment: str) -> None:
else:
self.regular_mission = self.current_mission
self.current_mission.options.load_from_dict(options_dict)
self.is_quick = is_quick
if is_quick:
self.attackers_starting_position = None
self.defenders_starting_position = None
else:
self.attackers_starting_position = self.departure_cp.at
# TODO: Is this possible?
if self.to_cp is not None:
self.defenders_starting_position = self.to_cp.at
else:
self.defenders_starting_position = None
def inject_lua_trigger(self, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment) trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents))) trigger.add_action(DoScript(String(contents)))
self.current_mission.triggerrules.triggers.append(trigger) cls.current_mission.triggerrules.triggers.append(trigger)
def bypass_plugin_script(self, mnemonic: str) -> None: @classmethod
self.plugin_scripts.append(mnemonic) def bypass_plugin_script(cls, mnemonic: str) -> None:
cls.plugin_scripts.append(mnemonic)
def inject_plugin_script(self, plugin_mnemonic: str, script: str, @classmethod
def inject_plugin_script(cls, plugin_mnemonic: str, script: str,
script_mnemonic: str) -> None: script_mnemonic: str) -> None:
if script_mnemonic in self.plugin_scripts: if script_mnemonic in cls.plugin_scripts:
logging.debug( logging.debug(
f"Skipping already loaded {script} for {plugin_mnemonic}" f"Skipping already loaded {script} for {plugin_mnemonic}"
) )
else: else:
self.plugin_scripts.append(script_mnemonic) cls.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic) plugin_path = Path("./resources/plugins", plugin_mnemonic)
@ -161,23 +153,25 @@ class Operation:
trigger = TriggerStart(comment=f"Load {script_mnemonic}") trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve() filename = script_path.resolve()
fileref = self.current_mission.map_resource.add_resource_file(filename) fileref = cls.current_mission.map_resource.add_resource_file(
filename)
trigger.add_action(DoScriptFile(fileref)) trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger) cls.current_mission.triggerrules.triggers.append(trigger)
@classmethod
def notify_info_generators( def notify_info_generators(
self, cls,
groundobjectgen: GroundObjectsGenerator, groundobjectgen: GroundObjectsGenerator,
airsupportgen: AirSupportConflictGenerator, airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo], jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
): ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings) """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
""" """
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
KneeboardGenerator(self.current_mission, self.game), KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(self.current_mission, self.game) BriefingGenerator(cls.current_mission, cls.game)
] ]
for gen in gens: for gen in gens:
for dynamic_runway in groundobjectgen.runways.values(): for dynamic_runway in groundobjectgen.runways.values():
gen.add_dynamic_runway(dynamic_runway) gen.add_dynamic_runway(dynamic_runway)
@ -185,7 +179,7 @@ class Operation:
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker) gen.add_tanker(tanker)
if self.is_awacs_enabled: if cls.player_awacs_enabled:
for awacs in airsupportgen.air_support.awacs: for awacs in airsupportgen.air_support.awacs:
gen.add_awacs(awacs) gen.add_awacs(awacs)
@ -196,14 +190,54 @@ class Operation:
gen.add_flight(flight) gen.add_flight(flight)
gen.generate() gen.generate()
def generate(self): @classmethod
radio_registry = RadioRegistry() def create_unit_map(cls) -> None:
tacan_registry = TacanRegistry() cls.unit_map = UnitMap()
for control_point in cls.game.theater.controlpoints:
if isinstance(control_point, Airfield):
cls.unit_map.add_airfield(control_point)
@classmethod
def create_radio_registries(cls) -> None:
unique_map_frequencies = set() # type: Set[RadioFrequency]
cls._create_tacan_registry(unique_map_frequencies)
cls._create_radio_registry(unique_map_frequencies)
@classmethod
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:
continue
cls.assign_channels_to_flight(flight, air_support)
@staticmethod
def assign_channels_to_flight(flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)
@classmethod
def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None:
"""
Dedup beacon/radio frequencies, since some maps have some frequencies
used multiple times.
"""
cls.tacan_registry = TacanRegistry()
beacons = load_beacons_for_terrain(cls.game.theater.terrain.name)
# Dedup beacon/radio frequencies, since some maps have some frequencies
# used multiple times.
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
unique_map_frequencies: Set[RadioFrequency] = set()
for beacon in beacons: for beacon in beacons:
unique_map_frequencies.add(beacon.frequency) unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan: if beacon.is_tacan:
@ -211,46 +245,45 @@ class Operation:
logging.error( logging.error(
f"TACAN beacon has no channel: {beacon.callsign}") f"TACAN beacon has no channel: {beacon.callsign}")
else: else:
tacan_registry.reserve(beacon.tacan_channel) cls.tacan_registry.reserve(beacon.tacan_channel)
for airfield, data in AIRFIELD_DATA.items(): @classmethod
if data.theater == self.game.theater.terrain.name: 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:
unique_map_frequencies.add(data.atc.hf) unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm) unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am) unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf) unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the # No need to reserve ILS or TACAN because those are in the
# beacon list. # beacon list.
for frequency in unique_map_frequencies: @classmethod
radio_registry.reserve(frequency) def _generate_ground_units(cls):
cls.groundobjectgen = GroundObjectsGenerator(
# Set mission time and weather conditions. cls.current_mission,
EnvironmentGenerator(self.current_mission, cls.game,
self.game.conditions).generate() cls.radio_registry,
cls.tacan_registry,
# Generate ground object first cls.unit_map
groundobjectgen = GroundObjectsGenerator(
self.current_mission,
self.conflict,
self.game,
radio_registry,
tacan_registry
) )
groundobjectgen.generate() cls.groundobjectgen.generate()
# Generate destroyed units @classmethod
for d in self.game.get_destroyed_units(): def _generate_destroyed_units(cls) -> None:
"""Add destroyed units to the Mission"""
for d in cls.game.get_destroyed_units():
try: try:
utype = db.unit_type_from_name(d["type"]) utype = db.unit_type_from_name(d["type"])
except KeyError: except KeyError:
continue continue
pos = Point(d["x"], d["z"]) pos = Point(d["x"], d["z"])
if utype is not None and not self.game.position_culled(pos) and self.game.settings.perf_destroyed_units: if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
self.current_mission.static_group( cls.current_mission.static_group(
country=self.current_mission.country(self.game.player_country), country=cls.current_mission.country(
cls.game.player_country),
name="", name="",
_type=utype, _type=utype,
hidden=True, hidden=True,
@ -259,76 +292,133 @@ class Operation:
dead=True, dead=True,
) )
# Air Support (Tanker & Awacs) @classmethod
airsupportgen = AirSupportConflictGenerator( def generate(cls) -> UnitMap:
self.current_mission, self.conflict, self.game, radio_registry, """Build the final Mission to be exported"""
tacan_registry) cls.create_unit_map()
airsupportgen.generate(self.is_awacs_enabled) cls.create_radio_registries()
# Set mission time and weather conditions.
# Generate Activity on the map EnvironmentGenerator(cls.current_mission,
airgen = AircraftConflictGenerator( cls.game.conditions).generate()
self.current_mission, self.conflict, self.game.settings, self.game, cls._generate_ground_units()
radio_registry) cls._generate_destroyed_units()
cls._generate_air_units()
airgen.generate_flights( cls.assign_channels_to_flights(cls.airgen.flights,
self.current_mission.country(self.game.player_country), cls.airsupportgen.air_support)
self.game.blue_ato, cls._generate_ground_conflicts()
groundobjectgen.runways
)
airgen.generate_flights(
self.current_mission.country(self.game.enemy_country),
self.game.red_ato,
groundobjectgen.runways
)
# Generate ground units on frontline everywhere
jtacs: List[JtacInfo] = []
for front_line in self.game.theater.conflicts(True):
player_cp = front_line.control_point_a
enemy_cp = front_line.control_point_b
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
self.current_mission.country(self.attacker_country),
self.current_mission.country(self.defender_country),
player_cp, enemy_cp, self.game.theater)
# Generate frontline ops
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
groundConflictGen.generate()
jtacs.extend(groundConflictGen.jtacs)
# Setup combined arms parameters
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]:
self.current_mission.groundControl.blue_tactical_commander = self.ca_slots
else:
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
# Triggers # Triggers
triggersgen = TriggersGenerator(self.current_mission, self.conflict, triggersgen = TriggersGenerator(cls.current_mission, cls.game)
self.game)
triggersgen.generate() triggersgen.generate()
# 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()]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
# Options # Options
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, forcedoptionsgen = ForcedOptionsGenerator(
self.conflict, self.game) cls.current_mission, cls.game)
forcedoptionsgen.generate() forcedoptionsgen.generate()
# Generate Visuals Smoke Effects # Generate Visuals Smoke Effects
visualgen = VisualGenerator(self.current_mission, self.conflict, visualgen = VisualGenerator(cls.current_mission, cls.game)
self.game) if cls.game.settings.perf_smoke_gen:
if self.game.settings.perf_smoke_gen:
visualgen.generate() visualgen.generate()
luaData = {} cls.generate_lua(cls.airgen, cls.airsupportgen, cls.jtacs)
luaData["AircraftCarriers"] = {}
luaData["Tankers"] = {}
luaData["AWACs"] = {}
luaData["JTACs"] = {}
luaData["TargetPoints"] = {}
self.assign_channels_to_flights(airgen.flights, # Inject Plugins Lua Scripts and data
airsupportgen.air_support) cls.plugin_scripts.clear()
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(cls)
plugin.inject_configuration(cls)
cls.assign_channels_to_flights(cls.airgen.flights,
cls.airsupportgen.air_support)
cls.notify_info_generators(
cls.groundobjectgen,
cls.airsupportgen,
cls.jtacs,
cls.airgen
)
return cls.unit_map
@classmethod
def _generate_air_units(cls) -> None:
"""Generate the air units for the 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.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.airgen.clear_parking_slots()
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato,
cls.groundobjectgen.runways
)
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato,
cls.groundobjectgen.runways
)
cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country))
@classmethod
def _generate_ground_conflicts(cls) -> None:
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
for front_line in cls.game.theater.conflicts(True):
player_cp = front_line.control_point_a
enemy_cp = front_line.control_point_b
conflict = Conflict.frontline_cas_conflict(
cls.game.player_name,
cls.game.enemy_name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
player_cp,
enemy_cp,
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,
player_cp.stances[enemy_cp.id],
cls.unit_map
)
ground_conflict_gen.generate()
cls.jtacs.extend(ground_conflict_gen.jtacs)
@classmethod
def generate_lua(cls, airgen: AircraftConflictGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo]) -> None:
# TODO: Refactor this
luaData = {
"AircraftCarriers": {},
"Tankers": {},
"AWACs": {},
"JTACs": {},
"TargetPoints": {},
} # type: ignore
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = { luaData["Tankers"][tanker.callsign] = {
@ -339,7 +429,7 @@ class Operation:
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name "tacan": str(tanker.tacan.number) + tanker.tacan.band.name
} }
if self.is_awacs_enabled: if airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs: for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = { luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName, "dcsGroupName": awacs.dcsGroupName,
@ -357,22 +447,27 @@ class Operation:
} }
for flight in airgen.flights: 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 = flight.flight_type.name FlightType.DEAD,
FlightType.SEAD,
FlightType.STRIKE]:
flightType = str(flight.flight_type)
flightTarget = flight.package.target flightTarget = flight.package.target
if flightTarget: if flightTarget:
flightTargetName = None flightTargetName = None
flightTargetType = None flightTargetType = None
if hasattr(flightTarget, 'obj_name'): if isinstance(flightTarget, TheaterGroundObject):
flightTargetName = flightTarget.obj_name flightTargetName = flightTarget.obj_name
flightTargetType = flightType + f" TGT ({flightTarget.category})" flightTargetType = flightType + \
f" TGT ({flightTarget.category})"
elif hasattr(flightTarget, 'name'): elif hasattr(flightTarget, 'name'):
flightTargetName = flightTarget.name flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)" flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = { luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName, "name": flightTargetName,
"type": flightTargetType, "type": flightTargetType,
"position": { "x": flightTarget.position.x, "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 # set a LUA table with data from Liberation that we want to set
@ -380,71 +475,71 @@ class Operation:
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts # later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]" state_location = "[[" + os.path.abspath(".") + "]]"
lua = """ lua = """
-- setting configuration table -- setting configuration table
env.info("DCSLiberation|: setting configuration table") env.info("DCSLiberation|: setting configuration table")
-- all data in this table is overridable. -- all data in this table is overridable.
dcsLiberation = {} dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory -- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath=""" + state_location + """ dcsLiberation.installPath=""" + state_location + """
""" """
# Process the tankers # Process the tankers
lua += """ lua += """
-- list the tankers generated by Liberation -- list the tankers generated by Liberation
dcsLiberation.Tankers = { dcsLiberation.Tankers = {
""" """
for key in luaData["Tankers"]: for key in luaData["Tankers"]:
data = luaData["Tankers"][key] data = luaData["Tankers"][key]
dcsGroupName= data["dcsGroupName"] dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"] callsign = data["callsign"]
variant = data["variant"] variant = data["variant"]
tacan = data["tacan"] tacan = data["tacan"]
radio = data["radio"] radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n" lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n" # lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
lua += "}" lua += "}"
# Process the AWACSes # Process the AWACSes
lua += """ lua += """
-- list the AWACs generated by Liberation -- list the AWACs generated by Liberation
dcsLiberation.AWACs = { dcsLiberation.AWACs = {
""" """
for key in luaData["AWACs"]: for key in luaData["AWACs"]:
data = luaData["AWACs"][key] data = luaData["AWACs"][key]
dcsGroupName= data["dcsGroupName"] dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"] callsign = data["callsign"]
radio = data["radio"] radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n" lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n" # lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
lua += "}" lua += "}"
# Process the JTACs # Process the JTACs
lua += """ lua += """
-- list the JTACs generated by Liberation -- list the JTACs generated by Liberation
dcsLiberation.JTACs = { dcsLiberation.JTACs = {
""" """
for key in luaData["JTACs"]: for key in luaData["JTACs"]:
data = luaData["JTACs"][key] data = luaData["JTACs"][key]
dcsGroupName= data["dcsGroupName"] dcsGroupName = data["dcsGroupName"]
callsign = data["callsign"] callsign = data["callsign"]
zone = data["zone"] zone = data["zone"]
laserCode = data["laserCode"] laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"] dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n" lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n" # lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += "}" lua += "}"
# Process the Target Points # Process the Target Points
lua += """ lua += """
-- list the target points generated by Liberation -- list the target points generated by Liberation
dcsLiberation.TargetPoints = { dcsLiberation.TargetPoints = {
""" """
for key in luaData["TargetPoints"]: for key in luaData["TargetPoints"]:
data = luaData["TargetPoints"][key] data = luaData["TargetPoints"][key]
name = data["name"] name = data["name"]
@ -452,56 +547,21 @@ dcsLiberation.TargetPoints = {
positionX = data["position"]["x"] positionX = data["position"]["x"]
positionY = data["position"]["y"] positionY = data["position"]["y"]
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n" lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
#lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n" # lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
lua += "}" lua += "}"
lua += """ lua += """
-- list the airbases generated by Liberation -- list the airbases generated by Liberation
-- dcsLiberation.Airbases = {} -- dcsLiberation.Airbases = {}
-- list the aircraft carriers generated by Liberation -- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {} -- dcsLiberation.Carriers = {}
-- later, we'll add more data to the table -- later, we'll add more data to the table
"""
"""
trigger = TriggerStart(comment="Set DCS Liberation data") trigger = TriggerStart(comment="Set DCS Liberation data")
trigger.add_action(DoScript(String(lua))) trigger.add_action(DoScript(String(lua)))
self.current_mission.triggerrules.triggers.append(trigger) Operation.current_mission.triggerrules.triggers.append(trigger)
# Inject Plugins Lua Scripts and data
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(self)
plugin.inject_configuration(self)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
def assign_channels_to_flights(self, flights: List[FlightData],
air_support: AirSupport) -> None:
"""Assigns preset radio channels for client flights."""
for flight in flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight, air_support)
def assign_channels_to_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)

View File

@ -7,45 +7,31 @@ from typing import Optional
_dcs_saved_game_folder: Optional[str] = None _dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None _file_abs_path = None
def setup(user_folder: str): def setup(user_folder: str):
global _dcs_saved_game_folder global _dcs_saved_game_folder
_dcs_saved_game_folder = user_folder _dcs_saved_game_folder = user_folder
_file_abs_path = os.path.join(base_path(), "default.liberation") _file_abs_path = os.path.join(base_path(), "default.liberation")
def base_path() -> str: def base_path() -> str:
global _dcs_saved_game_folder global _dcs_saved_game_folder
assert _dcs_saved_game_folder assert _dcs_saved_game_folder
return _dcs_saved_game_folder return _dcs_saved_game_folder
def _save_file() -> str:
return os.path.join(base_path(), "default.liberation")
def _temporary_save_file() -> str: def _temporary_save_file() -> str:
return os.path.join(base_path(), "tmpsave.liberation") return os.path.join(base_path(), "tmpsave.liberation")
def _autosave_path() -> str: def _autosave_path() -> str:
return os.path.join(base_path(), "autosave.liberation") return os.path.join(base_path(), "autosave.liberation")
def _save_file_exists() -> bool:
return os.path.exists(_save_file())
def mission_path_for(name: str) -> str: def mission_path_for(name: str) -> str:
return os.path.join(base_path(), "Missions", "{}".format(name)) return os.path.join(base_path(), "Missions", "{}".format(name))
def restore_game():
if not _save_file_exists():
return None
with open(_save_file(), "rb") as f:
try:
save = pickle.load(f)
return save
except Exception:
logging.exception("Invalid Save game")
return None
def load_game(path): def load_game(path):
with open(path, "rb") as f: with open(path, "rb") as f:
try: try:

View File

@ -5,7 +5,7 @@ import logging
import textwrap import textwrap
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING, Type
from game.settings import Settings from game.settings import Settings
@ -22,7 +22,7 @@ class LuaPluginWorkOrder:
self.mnemonic = mnemonic self.mnemonic = mnemonic
self.disable = disable self.disable = disable
def work(self, operation: Operation) -> None: def work(self, operation: Type[Operation]) -> None:
if self.disable: if self.disable:
operation.bypass_plugin_script(self.mnemonic) operation.bypass_plugin_script(self.mnemonic)
else: else:
@ -144,11 +144,11 @@ class LuaPlugin(PluginSettings):
for option in self.definition.options: for option in self.definition.options:
option.set_settings(self.settings) option.set_settings(self.settings)
def inject_scripts(self, operation: Operation) -> None: def inject_scripts(self, operation: Type[Operation]) -> None:
for work_order in self.definition.work_orders: for work_order in self.definition.work_orders:
work_order.work(operation) work_order.work(operation)
def inject_configuration(self, operation: Operation) -> None: def inject_configuration(self, operation: Type[Operation]) -> None:
# inject the plugin options # inject the plugin options
if self.options: if self.options:
option_decls = [] option_decls = []

194
game/procurement.py Normal file
View File

@ -0,0 +1,194 @@
from __future__ import annotations
from dataclasses import dataclass
import math
import random
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
from dcs.task import CAP, CAS
from dcs.unittype import FlyingType, UnitType, VehicleType
from game import db
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from gen.flights.ai_flight_planner_db import (
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
if TYPE_CHECKING:
from game import Game
@dataclass(frozen=True)
class AircraftProcurementRequest:
near: MissionTarget
range: int
task_capability: FlightType
number: int
class ProcurementAi:
def __init__(self, game: Game, for_player: bool, faction: Faction,
manage_runways: bool, manage_front_line: bool,
manage_aircraft: bool) -> None:
self.game = game
self.is_player = for_player
self.faction = faction
self.manage_runways = manage_runways
self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft
def spend_budget(
self, budget: int,
aircraft_requests: List[AircraftProcurementRequest]) -> int:
if self.manage_runways:
budget = self.repair_runways(budget)
if self.manage_front_line:
armor_budget = math.ceil(budget / 2)
budget -= armor_budget
budget += self.reinforce_front_line(armor_budget)
if self.manage_aircraft:
budget = self.purchase_aircraft(budget, aircraft_requests)
return budget
def repair_runways(self, budget: int) -> int:
for control_point in self.owned_points:
if budget < db.RUNWAY_REPAIR_COST:
break
if control_point.runway_can_be_repaired:
control_point.begin_runway_repair()
budget -= db.RUNWAY_REPAIR_COST
if self.is_player:
self.game.message(
"OPFOR has begun repairing the runway at "
f"{control_point}"
)
else:
self.game.message(
"We have begun repairing the runway at "
f"{control_point}"
)
return budget
def random_affordable_ground_unit(
self, budget: int) -> Optional[Type[VehicleType]]:
affordable_units = [u for u in self.faction.frontline_units if
db.PRICES[u] <= budget]
if not affordable_units:
return None
return random.choice(affordable_units)
def reinforce_front_line(self, budget: int) -> int:
if not self.faction.frontline_units:
return budget
while budget > 0:
candidates = self.front_line_candidates()
if not candidates:
break
cp = random.choice(candidates)
unit = self.random_affordable_ground_unit(budget)
if unit is None:
# Can't afford any more units.
break
budget -= db.PRICES[unit]
assert cp.pending_unit_deliveries is not None
cp.pending_unit_deliveries.deliver({unit: 1})
return budget
def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint,
number: int, max_price: int) -> Optional[Type[FlyingType]]:
unit_pool = [u for u in self.faction.aircrafts if u in types]
affordable_units = [
u for u in unit_pool
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
]
if not affordable_units:
return None
return random.choice(affordable_units)
def affordable_aircraft_for(
self, request: AircraftProcurementRequest,
airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]:
aircraft = self._affordable_aircraft_of_types(
preferred_aircraft_for_task(request.task_capability),
airbase, request.number, budget)
if aircraft is not None:
return aircraft
return self._affordable_aircraft_of_types(
capable_aircraft_for_task(request.task_capability),
airbase, request.number, budget)
def purchase_aircraft(
self, budget: int,
aircraft_requests: List[AircraftProcurementRequest]) -> int:
unit_pool = [u for u in self.faction.aircrafts
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
if not unit_pool:
return budget
for request in aircraft_requests:
for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget)
if unit is None:
# Can't afford any aircraft capable of performing the
# required mission that can operate from this airbase. We
# might be able to afford aircraft at other airbases though,
# in the case where the airbase we attempted to use is only
# able to operate expensive aircraft.
continue
budget -= db.PRICES[unit] * request.number
assert airbase.pending_unit_deliveries is not None
airbase.pending_unit_deliveries.deliver({unit: request.number})
return budget
@property
def owned_points(self) -> List[ControlPoint]:
if self.is_player:
return self.game.theater.player_points()
else:
return self.game.theater.enemy_points()
def best_airbases_for(
self,
request: AircraftProcurementRequest) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
request.near
)
for cp in distance_cache.airfields_within(request.range):
if not cp.is_friendly(self.is_player):
continue
if not cp.runway_is_operational():
continue
if cp.unclaimed_parking(self.game) < request.number:
continue
yield cp
def front_line_candidates(self) -> List[ControlPoint]:
candidates = []
# Prefer to buy front line units at active front lines that are not
# already overloaded.
for cp in self.owned_points:
if cp.base.total_armor >= 30:
# Control point is already sufficiently defended.
continue
for connected in cp.connected_points:
if not connected.is_friendly(to_player=self.is_player):
candidates.append(cp)
if not candidates:
# Otherwise buy them anywhere valid.
candidates = [p for p in self.owned_points
if p.can_deploy_ground_units]
return candidates

View File

@ -1,52 +1,55 @@
from typing import Dict from dataclasses import dataclass, field
from typing import Dict, Optional
from dcs.forcedoptions import ForcedOptions
@dataclass
class Settings: class Settings:
def __init__(self): # Difficulty settings
# Generator settings player_skill: str = "Good"
self.inverted = False enemy_skill: str = "Average"
self.do_not_generate_carrier = False # TODO : implement enemy_vehicle_skill: str = "Average"
self.do_not_generate_lha = False # TODO : implement map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
self.do_not_generate_player_navy = True # TODO : implement labels: str = "Full"
self.do_not_generate_enemy_navy = True # TODO : implement only_player_takeoff: bool = True # Legacy parameter do not use
night_disabled: bool = False
external_views_allowed: bool = True
supercarrier: bool = False
generate_marks: bool = True
manpads: bool = True
cold_start: bool = False # Legacy parameter do not use
version: Optional[str] = None
player_income_multiplier: float = 1.0
enemy_income_multiplier: float = 1.0
# Difficulty settings # Campaign management
self.player_skill = "Good" automate_runway_repair: bool = False
self.enemy_skill = "Average" automate_front_line_reinforcements: bool = False
self.enemy_vehicle_skill = "Average" automate_aircraft_reinforcements: bool = False
self.map_coalition_visibility = "All Units"
self.labels = "Full"
self.only_player_takeoff = True # Legacy parameter do not use
self.night_disabled = False
self.external_views_allowed = True
self.supercarrier = False
self.multiplier = 1
self.generate_marks = True
self.sams = True # Legacy parameter do not use
self.cold_start = False # Legacy parameter do not use
self.version = None
# Performance oriented # Performance oriented
self.perf_red_alert_state = True perf_red_alert_state: bool = True
self.perf_smoke_gen = True perf_smoke_gen: bool = True
self.perf_artillery = True perf_artillery: bool = True
self.perf_moving_units = True perf_moving_units: bool = True
self.perf_infantry = True perf_infantry: bool = True
self.perf_ai_parking_start = True perf_ai_parking_start: bool = True
self.perf_destroyed_units = True perf_destroyed_units: bool = True
# Performance culling # Performance culling
self.perf_culling = False perf_culling: bool = False
self.perf_culling_distance = 100 perf_culling_distance: int = 100
perf_do_not_cull_carrier = True
# LUA Plugins system # LUA Plugins system
self.plugins: Dict[str, bool] = {} plugins: Dict[str, bool] = field(default_factory=dict)
# Cheating # Cheating
self.show_red_ato = False show_red_ato: bool = False
self.never_delay_player_flights = False never_delay_player_flights: bool = False
@staticmethod @staticmethod
def plugin_settings_key(identifier: str) -> str: def plugin_settings_key(identifier: str) -> str:

View File

@ -1,5 +1,5 @@
from .base import * from .base import *
from .conflicttheater import * from .conflicttheater import *
from .controlpoint import * from .controlpoint import *
from .frontline import FrontLine
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import SamGroundObject

View File

@ -4,9 +4,8 @@ import math
import typing import typing
from typing import Dict, Type from typing import Dict, Type
from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import UnitType, VehicleType from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor from dcs.vehicles import AirDefence, Armor
from game import db from game import db
@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base: class Base:
aircraft = {} # type: typing.Dict[PlaneType, int]
armor = {} # type: typing.Dict[VehicleType, int]
aa = {} # type: typing.Dict[AirDefence, int]
strength = 1 # type: float
def __init__(self): def __init__(self):
self.aircraft = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor = {} self.armor: Dict[Type[VehicleType], int] = {}
self.aa = {} self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {} self.commision_points: Dict[Type, float] = {}
self.strength = 1 self.strength = 1
@property @property
def total_planes(self) -> int: def total_aircraft(self) -> int:
return sum(self.aircraft.values()) return sum(self.aircraft.values())
@property @property
@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result return result
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]: def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count) return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@ -155,7 +150,7 @@ class Base:
if task: if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else: else:
count = self.total_planes count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
@ -167,18 +162,18 @@ class Base:
# previous logic removed because we always want the full air defense capabilities. # previous logic removed because we always want the full air defense capabilities.
return self.total_aa return self.total_aa
def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]: def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self): def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base # return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts) # (but not more than 20 - that's just nuts)
return self._find_best_planes(CAP, min(self.total_planes, 20)) return self._find_best_planes(CAP, min(self.total_aircraft, 20))
def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]: def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS)) return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]: def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]: def assemble_attack(self) -> typing.Dict[Armor, int]:

View File

@ -0,0 +1,905 @@
from __future__ import annotations
import itertools
import json
import logging
from dataclasses import dataclass
from functools import cached_property
from itertools import tee
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
from dcs import Mission
from dcs.countries import (
CombinedJointTaskForcesBlue,
CombinedJointTaskForcesRed,
)
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import F_15C
from dcs.ships import (
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
USS_Arleigh_Burke_IIa,
)
from dcs.statics import Fortification
from dcs.terrain import (
caucasus,
nevada,
normandy,
persiangulf,
syria,
thechannel,
)
from dcs.terrain.terrain import Airport, Terrain
from dcs.unitgroup import (
FlyingGroup,
Group,
ShipGroup,
StaticGroup,
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from gen.flights.flight import FlightType
from .controlpoint import (
Airfield,
Carrier,
ControlPoint,
Lha,
MissionTarget,
OffMapSpawn,
Fob,
)
from .landmap import Landmap, load_landmap, poly_contains
from ..utils import nm_to_meter
Numeric = Union[int, float]
SIZE_TINY = 150
SIZE_SMALL = 600
SIZE_REGULAR = 1000
SIZE_BIG = 2000
SIZE_LARGE = 3000
IMPORTANCE_LOW = 1
IMPORTANCE_MEDIUM = 1.2
IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
class MizCampaignLoader:
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
RED_COUNTRY = CombinedJointTaskForcesRed()
OFF_MAP_UNIT_TYPE = F_15C.id
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
# Multiple options for the required SAMs so campaign designers can more
# accurately see the coverage of their IADS for the expected type.
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN_M901.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
}
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_LN_SM_90.id,
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
}
BASE_DEFENSE_RADIUS = nm_to_meter(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater
self.mission = Mission()
self.mission.load_file(str(miz))
self.control_point_id = itertools.count(1000)
# If there are no red carriers there usually aren't red units. Make sure
# both countries are initialized so we don't have to deal with None.
if self.mission.country(self.BLUE_COUNTRY.name) is None:
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
if self.mission.country(self.RED_COUNTRY.name) is None:
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
@staticmethod
def control_point_from_airport(airport: Airport) -> ControlPoint:
# The wiki says this is a legacy property and to just use regular.
size = SIZE_REGULAR
# The importance is taken from the periodicity of the airport's
# warehouse divided by 10. 30 is the default, and out of range (valid
# values are between 1.0 and 1.4). If it is used, pick the default
# importance.
if airport.periodicity == 30:
importance = IMPORTANCE_MEDIUM
else:
importance = airport.periodicity / 10
cp = Airfield(airport, size, importance)
cp.captured = airport.is_blue()
# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
cp.captured_invert = airport.unlimited_aircrafts
return cp
def country(self, blue: bool) -> Country:
country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
# Should be guaranteed because we initialized them.
assert country
return country
@property
def blue(self) -> Country:
return self.country(blue=True)
@property
def red(self) -> Country:
return self.country(blue=False)
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE:
yield group
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE:
yield group
@property
def ships(self) -> Iterator[ShipGroup]:
for group in self.blue.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.EWR_UNIT_TYPE:
yield group
@property
def sams(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.SAM_UNIT_TYPE:
yield group
@property
def garrisons(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.GARRISON_UNIT_TYPE:
yield group
@property
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group
@property
def coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group
@property
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group
@cached_property
def control_points(self) -> Dict[int, ControlPoint]:
control_points = {}
for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red():
control_point = self.control_point_from_airport(airport)
control_points[control_point.id] = control_point
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.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))
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.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.fobs(blue):
control_point = Fob(
str(group.name), 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
return control_points
@property
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group
@cached_property
def front_lines(self) -> Dict[str, ComplexFrontLine]:
# Dict of front line ID to a front line.
front_lines = {}
for group in self.front_line_path_groups:
# The unit will have its first waypoint at the source CP and the
# 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])
if origin is None:
raise RuntimeError(
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}")
# 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])
return front_lines
def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
closest = self.theater.closest_control_point(group.position)
distance = closest.position.distance_to_point(group.position)
return closest, distance
def add_preset_locations(self) -> None:
for group in self.garrisons:
closest, distance = self.objective_info(group)
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}")
for group in self.sams:
closest, distance = self.objective_info(group)
if distance < self.BASE_DEFENSE_RADIUS:
closest.preset_locations.base_air_defense.append(group.position)
else:
closest.preset_locations.strike_locations.append(group.position)
for group in self.ewrs:
closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append(group.position)
for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append(
group.position)
for group in self.ships:
closest, distance = self.objective_info(group)
closest.preset_locations.ships.append(group.position)
for group in self.missile_sites:
closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append(group.position)
for group in self.coastal_defenses:
closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(group.position)
for group in self.required_long_range_sams:
closest, distance = self.objective_info(group)
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
)
def populate_theater(self) -> None:
for control_point in self.control_points.values():
self.theater.add_controlpoint(control_point)
self.add_preset_locations()
self.theater.set_frontline_data(self.front_lines)
@dataclass
class ReferencePoint:
world_coordinates: Point
image_coordinates: Point
class ConflictTheater:
terrain: Terrain
reference_points: Tuple[ReferencePoint, ReferencePoint]
overview_image: str
landmap: Optional[Landmap]
"""
land_poly = None # type: Polygon
"""
daytime_map: Dict[str, Tuple[int, int]]
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self):
self.controlpoints: List[ControlPoint] = []
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
"""
self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]:
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
"""
@property
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
if self._frontline_data is None:
self.load_frontline_data_from_file()
return self._frontline_data
def load_frontline_data_from_file(self) -> None:
if self._frontline_data is not None:
logging.warning("Replacing existing frontline data from file")
self._frontline_data = FrontLine.load_json_frontlines(self)
if self._frontline_data is None:
self._frontline_data = {}
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
if self._frontline_data is not None:
logging.warning("Replacing existing frontline data")
self._frontline_data = data
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:
point.connect(to=connected_point)
self.controlpoints.append(point)
def find_ground_objects_by_obj_name(self, obj_name):
found = []
for cp in self.controlpoints:
for g in cp.ground_objects:
if g.obj_name == obj_name:
found.append(g)
return found
def is_in_sea(self, point: Point) -> bool:
if not self.landmap:
return False
if self.is_on_land(point):
return False
for exclusion_zone in self.landmap[1]:
if poly_contains(point.x, point.y, exclusion_zone):
return False
for sea in self.landmap[2]:
if poly_contains(point.x, point.y, sea):
return True
return False
def is_on_land(self, point: Point) -> bool:
if not self.landmap:
return True
is_point_included = False
for inclusion_zone in self.landmap[0]:
if poly_contains(point.x, point.y, inclusion_zone):
is_point_included = True
if not is_point_included:
return False
for exclusion_zone in self.landmap[1]:
if poly_contains(point.x, point.y, exclusion_zone):
return False
return True
def player_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if point.captured]
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]:
yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured]
def closest_control_point(self, point: Point) -> ControlPoint:
closest = self.controlpoints[0]
closest_distance = point.distance_to_point(closest.position)
for control_point in self.controlpoints[1:]:
distance = point.distance_to_point(control_point.position)
if distance < closest_distance:
closest = control_point
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.
(player_cp, enemy_cp)
"""
all_cp_min_distances = {}
for idx, control_point in enumerate(self.controlpoints):
distances = {}
closest_distance = None
for i, cp in enumerate(self.controlpoints):
if i != idx and cp.captured is not control_point.captured:
dist = cp.position.distance_to_point(control_point.position)
if not closest_distance:
closest_distance = dist
distances[cp.id] = dist
if dist < closest_distance:
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]
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]
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)))
def find_control_point_by_id(self, id: int) -> ControlPoint:
for i in self.controlpoints:
if i.id == id:
return i
raise RuntimeError(f"Cannot find ControlPoint with ID {id}")
def add_json_cp(self, theater, p: dict) -> ControlPoint:
cp: ControlPoint
if p["type"] == "airbase":
airbase = theater.terrain.airports[p["id"]]
if "size" in p.keys():
size = p["size"]
else:
size = SIZE_REGULAR
if "importance" in p.keys():
importance = p["importance"]
else:
importance = IMPORTANCE_MEDIUM
cp = Airfield(airbase, size, importance)
elif p["type"] == "carrier":
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
else:
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
if "captured_invert" in p.keys():
cp.captured_invert = p["captured_invert"]
else:
cp.captured_invert = False
return cp
@staticmethod
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
theaters = {
"Caucasus": CaucasusTheater,
"Nevada": NevadaTheater,
"Persian Gulf": PersianGulfTheater,
"Normandy": NormandyTheater,
"The Channel": TheChannelTheater,
"Syria": SyriaTheater,
}
theater = theaters[data["theater"]]
t = theater()
miz = data.get("miz", None)
if miz is not None:
MizCampaignLoader(directory / miz, t).populate_theater()
return t
cps = {}
for p in data["player_points"]:
cp = t.add_json_cp(theater, p)
cp.captured = True
cps[p["id"]] = cp
t.add_controlpoint(cp)
for p in data["enemy_points"]:
cp = t.add_json_cp(theater, p)
cps[p["id"]] = cp
t.add_controlpoint(cp)
for l in data["links"]:
cps[l[0]].connect(cps[l[1]])
cps[l[1]].connect(cps[l[0]])
return t
class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus()
overview_image = "caumap.gif"
reference_points = (
ReferencePoint(caucasus.Gelendzhik.position, Point(176, 298)),
ReferencePoint(caucasus.Batumi.position, Point(1307, 1205)),
)
landmap = load_landmap("resources\\caulandmap.p")
daytime_map = {
"dawn": (6, 9),
"day": (9, 18),
"dusk": (18, 20),
"night": (0, 5),
}
class PersianGulfTheater(ConflictTheater):
terrain = persiangulf.PersianGulf()
overview_image = "persiangulf.gif"
reference_points = (
ReferencePoint(persiangulf.Jiroft_Airport.position,
Point(1692, 1343)),
ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
)
landmap = load_landmap("resources\\gulflandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (8, 16),
"dusk": (16, 18),
"night": (0, 5),
}
class NevadaTheater(ConflictTheater):
terrain = nevada.Nevada()
overview_image = "nevada.gif"
reference_points = (
ReferencePoint(nevada.Mina_Airport_3Q0.position, Point(252, 295)),
ReferencePoint(nevada.Laughlin_Airport.position, Point(844, 909)),
)
landmap = load_landmap("resources\\nevlandmap.p")
daytime_map = {
"dawn": (4, 6),
"day": (6, 17),
"dusk": (17, 18),
"night": (0, 5),
}
class NormandyTheater(ConflictTheater):
terrain = normandy.Normandy()
overview_image = "normandy.gif"
reference_points = (
ReferencePoint(normandy.Needs_Oar_Point.position, Point(515, 329)),
ReferencePoint(normandy.Evreux.position, Point(2029, 1709)),
)
landmap = load_landmap("resources\\normandylandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (10, 17),
"dusk": (17, 18),
"night": (0, 5),
}
class TheChannelTheater(ConflictTheater):
terrain = thechannel.TheChannel()
overview_image = "thechannel.gif"
reference_points = (
ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
ReferencePoint(thechannel.Detling.position, Point(706, 382))
)
landmap = load_landmap("resources\\channellandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (10, 17),
"dusk": (17, 18),
"night": (0, 5),
}
class SyriaTheater(ConflictTheater):
terrain = syria.Syria()
overview_image = "syria.gif"
reference_points = (
ReferencePoint(syria.Eyn_Shemer.position, Point(564, 1289)),
ReferencePoint(syria.Tabqa.position, Point(1329, 491)),
)
landmap = load_landmap("resources\\syrialandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (8, 16),
"dusk": (16, 18),
"night": (0, 5),
}
@dataclass
class ComplexFrontLine:
"""
Stores data necessary for building a multi-segment frontline.
"points" should be ordered from closest to farthest distance originating from start_cp.position
"""
start_cp: ControlPoint
points: List[Point]
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@property
def attack_heading(self) -> Numeric:
"""The heading of the frontline segment from player to enemy control point"""
return self.point_a.heading_between_point(self.point_b)
@property
def attack_distance(self) -> Numeric:
"""Length of the segment"""
return self.point_a.distance_to_point(self.point_b)
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
"""
def __init__(
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater
) -> None:
self.control_point_a = control_point_a
self.control_point_b = control_point_b
self.segments: List[FrontLineSegment] = []
self.theater = theater
self._build_segments()
self.name = f"Front line {control_point_a}/{control_point_b}"
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]
yield from super().mission_types(for_player)
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
@property
def attack_distance(self):
"""The total distance of all segments"""
return sum(i.attack_distance for i in self.segments)
@property
def attack_heading(self):
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading
@property
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
def point_from_a(self, distance: Numeric) -> Point:
"""
Returns a point {distance} away from control_point_a along the frontline segments.
"""
if distance < self.segments[0].attack_distance:
return self.control_point_a.position.point_from_heading(
self.segments[0].attack_heading, distance
)
remaining_dist = distance
for segment in self.segments:
if remaining_dist < segment.attack_distance:
return segment.point_a.point_from_heading(
segment.attack_heading, remaining_dist
)
else:
remaining_dist -= segment.attack_distance
@property
def _position_distance(self) -> float:
"""
The distance from point "a" where the conflict should occur
according to the current strength of each control point
"""
total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength
)
if self.control_point_a.base.strength == 0:
return self._adjust_for_min_dist(0)
if self.control_point_b.base.strength == 0:
return self._adjust_for_min_dist(self.attack_distance)
strength_pct = self.control_point_a.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
"""
Ensures the frontline conflict is never located within the minimum distance
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance
def _build_segments(self) -> None:
"""Create line segments for the frontline"""
control_point_ids = "|".join(
[str(self.control_point_a.id), str(self.control_point_b.id)]
) # from_cp.id|to_cp.id
reversed_cp_ids = "|".join(
[str(self.control_point_b.id), str(self.control_point_a.id)]
)
complex_frontlines = self.theater.frontline_data
if (complex_frontlines) and (
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
):
# The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after.
# First, check if control point id pair matches in order, and create segments if a match is found.
if control_point_ids in complex_frontlines:
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# Check the reverse order and build in reverse if found.
elif reversed_cp_ids in complex_frontlines:
point_pairs = pairwise(
reversed(complex_frontlines[reversed_cp_ids].points)
)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# If no complex frontline has been configured, fall back to the old straight line method.
else:
self.segments.append(
FrontLineSegment(
self.control_point_a.position, self.control_point_b.position
)
)
@staticmethod
def load_json_frontlines(
theater: ConflictTheater
) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json"""
try:
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
with open(path, "r") as file:
logging.debug(f"Loading frontline from {path}...")
data = json.load(file)
return {
frontline: ComplexFrontLine(
data[frontline]["start_cp"],
[Point(i[0], i[1]) for i in data[frontline]["points"]],
)
for frontline in data
}
except OSError:
logging.warning(
f"Unable to load preset frontlines for {theater.terrain.name}"
)
return None

View File

@ -0,0 +1,721 @@
from __future__ import annotations
import itertools
import logging
import random
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.ships import (
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
LHA_1_Tarawa,
Type_071_Amphibious_Transport_Dock,
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unittype import FlyingType
from game import db
from gen.runways import RunwayAssigner, RunwayData
from gen.ground_forces.combat_stance import CombatStance
from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
SamGroundObject,
TheaterGroundObject,
VehicleGroupGroundObject, GenericCarrierGroundObject,
)
from ..weather import Conditions
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
from ..event import UnitsDeliveryEvent
class ControlPointType(Enum):
#: An airbase with slots for everything.
AIRBASE = 0
#: A group with a Stennis type carrier (F/A-18, F-14 compatible).
AIRCRAFT_CARRIER_GROUP = 1
#: A group with a Tarawa carrier (Helicopters & Harrier).
LHA_GROUP = 2
#: A FARP, with slots for helicopters
FARP = 4
#: A FOB (ground units only)
FOB = 5
OFF_MAP = 6
class LocationType(Enum):
BaseAirDefense = "base air defense"
Coastal = "coastal defense"
Ewr = "EWR"
Garrison = "garrison"
MissileSite = "missile site"
OffshoreStrikeTarget = "offshore strike target"
Sam = "SAM"
Ship = "ship"
Shorad = "SHORAD"
StrikeTarget = "strike target"
@dataclass
class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases.
base_garrisons: List[Point] = field(default_factory=list)
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
#: and SHORADs.
base_air_defense: List[Point] = field(default_factory=list)
#: Locations used by EWRs.
ewrs: List[Point] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[Point] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[Point] = field(default_factory=list)
#: Locations used by ground based strike objectives.
strike_locations: List[Point] = field(default_factory=list)
#: Locations used by offshore strike objectives.
offshore_strike_locations: List[Point] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[Point] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[Point] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned.
required_medium_range_sams: List[Point] = field(default_factory=list)
@staticmethod
def _random_from(points: List[Point]) -> Optional[Point]:
"""Finds, removes, and returns a random position from the given list."""
if not points:
return None
point = random.choice(points)
points.remove(point)
return point
def random_for(self, location_type: LocationType) -> Optional[Point]:
"""Returns a position suitable for the given location type.
The location, if found, will be claimed by the caller and not available
to subsequent calls.
"""
if location_type == LocationType.BaseAirDefense:
return self._random_from(self.base_air_defense)
if location_type == LocationType.Coastal:
return self._random_from(self.coastal_defenses)
if location_type == LocationType.Ewr:
return self._random_from(self.ewrs)
if location_type == LocationType.Garrison:
return self._random_from(self.base_garrisons)
if location_type == LocationType.MissileSite:
return self._random_from(self.missile_sites)
if location_type == LocationType.OffshoreStrikeTarget:
return self._random_from(self.offshore_strike_locations)
if location_type == LocationType.Sam:
return self._random_from(self.strike_locations)
if location_type == LocationType.Ship:
return self._random_from(self.ships)
if location_type == LocationType.Shorad:
return self._random_from(self.base_garrisons)
if location_type == LocationType.StrikeTarget:
return self._random_from(self.strike_locations)
logging.error(f"Unknown location type: {location_type}")
return None
@dataclass(frozen=True)
class PendingOccupancy:
present: int
ordered: int
transferring: int
@property
def total(self) -> int:
return self.present + self.ordered + self.transferring
@dataclass
class RunwayStatus:
damaged: bool = False
repair_turns_remaining: Optional[int] = None
def damage(self) -> None:
self.damaged = True
# If the runway is already under repair and is damaged again, progress
# is reset.
self.repair_turns_remaining = None
def begin_repair(self) -> None:
if self.repair_turns_remaining is not None:
logging.error("Runway already under repair. Restarting.")
self.repair_turns_remaining = 4
def process_turn(self) -> None:
if self.repair_turns_remaining is not None:
if self.repair_turns_remaining == 1:
self.repair_turns_remaining = None
self.damaged = False
else:
self.repair_turns_remaining -= 1
@property
def needs_repair(self) -> bool:
return self.damaged and self.repair_turns_remaining is None
def __str__(self) -> str:
if not self.damaged:
return "Runway operational"
turns_remaining = self.repair_turns_remaining
if turns_remaining is None:
return "Runway damaged"
return f"Runway repairing, {turns_remaining} turns remaining"
class ControlPoint(MissionTarget, ABC):
position = None # type: Point
name = None # type: str
captured = False
has_frontline = True
alt = 0
# 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):
super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
# TODO: Should be Airbase specific.
self.id = cp_id
self.full_name = name
self.at = at
self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations()
# TODO: Should be Airbase specific.
self.size = size
self.importance = importance
self.captured = False
self.captured_invert = False
# TODO: Should be Airbase specific.
self.has_frontline = has_frontline
self.connected_points: List[ControlPoint] = []
self.base: Base = Base()
self.cptype = cptype
# TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {}
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
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))
@property
@abstractmethod
def heading(self) -> int:
...
def __str__(self):
return self.name
@property
def is_global(self):
return not self.connected_points
@property
def is_carrier(self):
"""
:return: Whether this control point is an aircraft carrier
"""
return False
@property
def is_fleet(self):
"""
:return: Whether this control point is a boat (mobile)
"""
return False
@property
def is_lha(self):
"""
:return: Whether this control point is an LHA
"""
return False
@property
def moveable(self) -> bool:
"""
:return: Whether this control point can be moved around
"""
return False
@property
@abstractmethod
def can_deploy_ground_units(self) -> bool:
...
@property
@abstractmethod
def total_aircraft_parking(self):
"""
:return: The maximum number of aircraft that can be stored in this
control point
"""
...
# TODO: Should be Airbase specific.
def connect(self, to: ControlPoint) -> None:
self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE
@abstractmethod
def runway_is_operational(self) -> bool:
"""
Check whether this control point supports taking offs and landings.
:return:
"""
...
# TODO: Should be naval specific.
def get_carrier_group_name(self):
"""
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]:
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]:
return group.name
elif g.dcs_identifier == "LHA":
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
return group.name
return None
# TODO: Should be Airbase specific.
def is_connected(self, to) -> bool:
return to in self.connected_points
def find_ground_objects_by_obj_name(self, obj_name):
found = []
for g in self.ground_objects:
if g.obj_name == obj_name:
found.append(g)
return found
def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player
# TODO: Should be Airbase specific.
def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses:
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)
elif isinstance(base_defense, VehicleGroupGroundObject):
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)
self.base_defenses = []
# TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None:
if for_player:
self.captured = True
else:
self.captured = False
self.base.set_strength_to_minimum()
self.base.aircraft = {}
self.base.armor = {}
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
@abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
...
def aircraft_transferring(self, game: Game) -> int:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
total = 0
for package in ato.packages:
for flight in package.flights:
if flight.departure == flight.arrival:
continue
if flight.departure == self:
total -= flight.count
elif flight.arrival == self:
total += flight.count
return total
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
assert self.pending_unit_deliveries
on_order = 0
for unit_bought in self.pending_unit_deliveries.units:
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))
def unclaimed_parking(self, game: Game) -> int:
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:
...
@property
def parking_slots(self) -> Iterator[ParkingSlot]:
yield from []
@property
@abstractmethod
def runway_status(self) -> RunwayStatus:
...
@property
def runway_can_be_repaired(self) -> bool:
return self.runway_status.needs_repair
def begin_runway_repair(self) -> None:
if not self.runway_can_be_repaired:
logging.error(f"Cannot repair runway at {self}")
return
self.runway_status.begin_repair()
def process_turn(self) -> None:
runway_status = self.runway_status
if runway_status is not None:
runway_status.process_turn()
# Process movements for ships control points group
if self.target_position is not None:
delta = self.target_position - self.position
self.position = self.target_position
self.target_position = None
# Move the linked unit groups
for ground_object in self.ground_objects:
if isinstance(ground_object, GenericCarrierGroundObject):
for group in ground_object.groups:
for u in group.units:
u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y
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)
self.airport = airport
self._runway_status = RunwayStatus()
def can_operate(self, aircraft: FlyingType) -> bool:
# TODO: Allow helicopters.
# Need to implement ground spawns so the helos don't use the runway.
# TODO: Allow harrier.
# Needs ground spawns just like helos do, but also need to be able to
# limit takeoff weight to ~20500 lbs or it won't be able to take off.
return self.runway_is_operational()
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
# TODO: FlightType.LOGISTICS
]
else:
yield from [
FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY,
]
yield from super().mission_types(for_player)
@property
def total_aircraft_parking(self) -> int:
return len(self.airport.parking_slots)
@property
def heading(self) -> int:
return self.airport.runways[0].heading
def runway_is_operational(self) -> bool:
return not self.runway_status.damaged
@property
def runway_status(self) -> RunwayStatus:
return self._runway_status
def damage_runway(self) -> None:
self.runway_status.damage()
def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport)
@property
def parking_slots(self) -> Iterator[ParkingSlot]:
yield from self.airport.parking_slots
@property
def can_deploy_ground_units(self) -> bool:
return True
class NavalControlPoint(ControlPoint, ABC):
@property
def is_fleet(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
]
else:
yield FlightType.ANTISHIP
@property
def heading(self) -> int:
return 0 # TODO compute heading
def runway_is_operational(self) -> bool:
# Necessary because it's possible for the carrier itself to have sunk
# while its escorts are still alive.
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
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]:
return True
return False
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)
@property
def runway_status(self) -> RunwayStatus:
return RunwayStatus(damaged=not self.runway_is_operational())
@property
def runway_can_be_repaired(self) -> bool:
return False
@property
def moveable(self) -> bool:
return True
@property
def can_deploy_ground_units(self) -> bool:
return False
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)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured")
@property
def is_carrier(self):
return True
def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft in db.CARRIER_CAPABLE
@property
def total_aircraft_parking(self) -> int:
return 90
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)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("LHAs cannot be captured")
@property
def is_lha(self) -> bool:
return True
def can_operate(self, aircraft: FlyingType) -> bool:
return aircraft in db.LHA_CAPABLE
@property
def total_aircraft_parking(self) -> int:
return 20
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)
def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from []
@property
def total_aircraft_parking(self) -> int:
return 1000
def can_operate(self, aircraft: FlyingType) -> bool:
return True
@property
def heading(self) -> int:
return 0
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="")
@property
def runway_status(self) -> RunwayStatus:
return RunwayStatus()
@property
def can_deploy_ground_units(self) -> bool:
return False
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)
self.name = name
def runway_is_operational(self) -> bool:
return False
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="")
@property
def runway_status(self) -> RunwayStatus:
return RunwayStatus()
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,
# TODO: FlightType.LOGISTICS
]
else:
yield from [
FlightType.STRIKE,
FlightType.SWEEP,
FlightType.ESCORT,
FlightType.SEAD,
]
@property
def total_aircraft_parking(self) -> int:
return 0
def can_operate(self, aircraft: FlyingType) -> bool:
return False
@property
def heading(self) -> int:
return 0
@property
def can_deploy_ground_units(self) -> bool:
return True

30
game/theater/landmap.py Normal file
View File

@ -0,0 +1,30 @@
import pickle
from typing import Collection, Optional, Tuple
import logging
from shapely import geometry
Zone = Collection[Tuple[float, float]]
Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.Polygon]]
def load_landmap(filename: str) -> Optional[Landmap]:
try:
with open(filename, "rb") as f:
return pickle.load(f)
except:
logging.exception(f"Failed to load landmap {filename}")
return None
def poly_contains(x, y, poly:geometry.Polygon):
return poly.contains(geometry.Point(x, y))
def poly_centroid(poly) -> Tuple[float, float]:
x_list = [vertex[0] for vertex in poly]
y_list = [vertex[1] for vertex in poly]
x = sum(x_list) / len(poly)
y = sum(y_list) / len(poly)
return (x, y)

View File

@ -0,0 +1,43 @@
from __future__ import annotations
from typing import Iterator, TYPE_CHECKING
from dcs.mapping import Point
if TYPE_CHECKING:
from gen.flights.flight import FlightType
class MissionTarget:
def __init__(self, name: str, position: Point) -> None:
"""Initializes a mission target.
Args:
name: The name of the mission target.
position: The location of the mission target.
"""
self.name = name
self.position = position
def distance_to(self, other: MissionTarget) -> int:
"""Computes the distance to the given mission target."""
return self.position.distance_to_point(other.position)
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
raise NotImplementedError
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:
yield from [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.SEAD,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,
# TODO: FlightType.RECON,
]

View File

@ -0,0 +1,741 @@
from __future__ import annotations
import logging
import math
import pickle
import random
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Set
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
EwrGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
VehicleGroupGroundObject,
)
from game.version import VERSION
from gen import namegen
from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import (
generate_carrier_group,
generate_lha_group,
generate_ship_group,
)
from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.sam_group_generator import (
generate_anti_air_group,
generate_ewr_group,
)
from . import (
ConflictTheater,
ControlPoint,
ControlPointType,
Fob,
OffMapSpawn,
)
from ..settings import Settings
GroundObjectTemplates = Dict[str, Dict[str, Any]]
UNIT_VARIETY = 6
UNIT_AMOUNT_FACTOR = 16
UNIT_COUNT_IMPORTANCE_LOG = 1.3
COUNT_BY_TASK = {
PinpointStrike: 12,
CAP: 8,
CAS: 4,
AirDefence: 1,
}
@dataclass(frozen=True)
class GeneratorSettings:
start_date: datetime
player_budget: int
enemy_budget: int
midgame: bool
inverted: bool
no_carrier: bool
no_lha: bool
no_player_navy: bool
no_enemy_navy: bool
class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings) -> None:
self.player = player
self.enemy = enemy
self.theater = theater
self.settings = settings
self.generator_settings = generator_settings
def generate(self) -> Game:
# Reset name generator
namegen.reset()
self.prepare_theater()
game = Game(
player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
start_date=self.generator_settings.start_date,
settings=self.settings,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget
)
GroundObjectGenerator(game, self.generator_settings).generate()
game.settings.version = VERSION
return game
def prepare_theater(self) -> None:
to_remove: List[ControlPoint] = []
# 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]:
control_point.captured = True
# Remove carrier and lha, invert situation if needed
for cp in self.theater.controlpoints:
if isinstance(cp, Carrier) and self.generator_settings.no_carrier:
to_remove.append(cp)
elif isinstance(cp, Lha) and self.generator_settings.no_lha:
to_remove.append(cp)
if self.generator_settings.inverted:
cp.captured = cp.captured_invert
# do remove
for cp in to_remove:
self.theater.controlpoints.remove(cp)
# TODO: Fix this. This captures all bases for blue.
# reapply midgame inverted if needed
if self.generator_settings.midgame and self.generator_settings.inverted:
for i, cp in enumerate(reversed(self.theater.controlpoints)):
if i > len(self.theater.controlpoints):
break
else:
cp.captured = True
class LocationFinder:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations(
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)
position = self.random_from_miz_data(
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)
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)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[Point]:
if offshore:
locations = self.miz_data.offshore_locations
else:
locations = self.miz_data.ashore_locations
if self.miz_data.offshore_locations:
preset = random.choice(locations)
locations.remove(preset)
return preset.position
return None
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)
is_base_defense = location_type in {
LocationType.BaseAirDefense,
LocationType.Garrison,
LocationType.Shorad,
}
on_land = location_type not in {
LocationType.OffshoreStrikeTarget,
LocationType.Ship,
}
avoid_others = location_type not in {
LocationType.Garrison,
LocationType.MissileSite,
LocationType.Sam,
LocationType.Ship,
LocationType.Shorad,
}
if is_base_defense:
min_range = 400
max_range = 3200
elif location_type == LocationType.Ship:
min_range = 5000
max_range = 40000
elif location_type == LocationType.MissileSite:
min_range = 2500
max_range = 40000
else:
min_range = 10000
max_range = 40000
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)
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]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param min_range: Minimal range from point
:param max_range: Max range from point
:param is_base_defense: True if the location is for base defense.
:return:
"""
near = self.control_point.position
others = self.control_point.ground_objects
def is_valid(point: Optional[Point]) -> bool:
if point is None:
return False
if on_ground and not self.game.theater.is_on_land(point):
return False
elif not on_ground and not self.game.theater.is_in_sea(point):
return False
if avoid_others:
for other in others:
if other.position.distance_to_point(point) < 10000:
return False
if is_base_defense:
# If it's a base defense we don't care how close it is to other
# points.
return True
# Else verify that it's not too close to another control point.
for control_point in self.game.theater.controlpoints:
if control_point != self.control_point:
if control_point.position.distance_to_point(point) < 30000:
return False
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
return False
return True
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max_range, min_range)
if is_valid(p):
return p
return None
class ControlPointGroundObjectGenerator:
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
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
self.generate_navy()
return True
def generate_navy(self) -> None:
skip_player_navy = self.generator_settings.no_player_navy
if self.control_point.captured and skip_player_navy:
return
skip_enemy_navy = self.generator_settings.no_enemy_navy
if not self.control_point.captured and skip_enemy_navy:
return
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_ship(self) -> None:
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)
group = generate_ship_group(self.game, g, self.faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
return True
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
if not super().generate():
return False
carrier_names = self.faction.carrier_names
if not carrier_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
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)
group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(carrier_names)
return True
class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
if not super().generate():
return False
lha_names = self.faction.helicopter_carrier_names
if not lha_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
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)
group = generate_lha_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(lha_names)
return True
class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> None:
self.generate_ewr()
self.generate_garrison()
self.generate_base_defenses()
def generate_ewr(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
return
group_id = self.game.next_group_id()
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:
logging.error(f"Could not generate EWR at {self.control_point}")
return
g.groups = [group]
self.control_point.base_defenses.append(g)
def generate_base_defenses(self) -> None:
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
# and a 1/6 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_sam()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
def generate_garrison(self) -> None:
position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
return
group_id = self.game.next_group_id()
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}")
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)
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)
group = generate_anti_air_group(self.game, g, self.faction)
if group is None:
logging.error(f"Could not generate SAM at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
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)
group = generate_anti_air_group(self.game, g, self.faction,
ranges=[{AirDefenseRange.Short}])
if group is None:
logging.error(
f"Could not generate SHORAD group at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
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.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_shorad()
elif i == 0 and random.randint(0, 1) == 0:
self.generate_garrison()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, generator_settings: GeneratorSettings,
control_point: ControlPoint,
templates: GroundObjectTemplates) -> None:
super().__init__(game, generator_settings, control_point)
self.templates = templates
def generate(self) -> bool:
if not super().generate():
return False
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points()
if self.faction.missiles:
self.generate_missile_sites()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
skip_sams = self.generate_required_aa()
if self.control_point.is_global:
return
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site()
else:
self.generate_ground_point()
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
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},
])
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))
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
except IndexError:
logging.exception("Faction has no buildings defined")
return
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
location_type = LocationType.StrikeTarget
# Pick from preset locations
point = self.location_finder.location_for(location_type)
if point is None:
return
object_id = 0
group_id = self.game.next_group_id()
# TODO: Create only one TGO per objective, each with multiple units.
for unit in template:
object_id += 1
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"])
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
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},
])
def generate_aa_at(
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)
group = generate_anti_air_group(self.game, g, self.faction, ranges)
if group is None:
logging.error("Could not generate air defense group for %s at %s",
g.name, self.control_point)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
return
group_id = self.game.next_group_id()
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:
g.groups.append(group)
self.control_point.connected_objectives.append(g)
return
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_required_aa()
return True
def generate_fob(self) -> None:
try:
category = self.faction.building_set[self.faction.building_set.index('fob')]
except IndexError:
logging.exception("Faction has no fob buildings defined")
return
obj_name = self.control_point.name
template = random.choice(list(self.templates[category].values()))
point = self.control_point.position
# Pick from preset locations
object_id = 0
group_id = self.game.next_group_id()
# TODO: Create only one TGO per objective, each with multiple units.
for unit in template:
object_id += 1
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)
self.control_point.connected_objectives.append(g)
class GroundObjectGenerator:
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:
self.templates: GroundObjectTemplates = pickle.load(f)
def generate(self) -> None:
# Copied so we can remove items from the original list without breaking
# the iterator.
control_points = list(self.game.theater.controlpoints)
for control_point in control_points:
if not self.generate_for_control_point(control_point):
self.game.theater.controlpoints.remove(control_point)
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator(
self.game, self.generator_settings, control_point)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(
self.game, self.generator_settings, control_point)
elif isinstance(control_point, OffMapSpawn):
generator = NoOpGroundObjectGenerator(
self.game, self.generator_settings, control_point)
elif isinstance(control_point, Fob):
generator = FobGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
else:
generator = AirbaseGroundObjectGenerator(
self.game, self.generator_settings, control_point,
self.templates)
return generator.generate()

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import List, TYPE_CHECKING from typing import Iterator, List, TYPE_CHECKING
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
@ -9,6 +9,8 @@ from dcs.unitgroup import Group
if TYPE_CHECKING: if TYPE_CHECKING:
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from gen.flights.flight import FlightType
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
NAME_BY_CATEGORY = { NAME_BY_CATEGORY = {
@ -117,11 +119,36 @@ class TheaterGroundObject(MissionTarget):
def faction_color(self) -> str: def faction_color(self) -> str:
return "BLUE" if self.control_point.captured else "RED" return "BLUE" if self.control_point.captured else "RED"
def is_friendly(self, to_player: bool) -> bool:
return self.control_point.is_friendly(to_player)
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
# TODO: FlightType.TROOP_TRANSPORT
]
else:
yield from [
FlightType.STRIKE,
FlightType.BAI,
]
yield from super().mission_types(for_player)
@property
def alive_unit_count(self) -> int:
return sum(len(g.units) for g in self.groups)
@property
def might_have_aa(self) -> bool:
return False
class BuildingGroundObject(TheaterGroundObject): class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(self, name: str, category: str, group_id: int, object_id: int,
position: Point, heading: int, control_point: ControlPoint, position: Point, heading: int, control_point: ControlPoint,
dcs_identifier: str) -> None: dcs_identifier: str, airbase_group=False) -> None:
super().__init__( super().__init__(
name=name, name=name,
category=category, category=category,
@ -130,7 +157,7 @@ class BuildingGroundObject(TheaterGroundObject):
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=False, airbase_group=airbase_group,
sea_object=False sea_object=False
) )
self.object_id = object_id self.object_id = object_id
@ -145,7 +172,19 @@ class BuildingGroundObject(TheaterGroundObject):
return f"{super().waypoint_name} #{self.object_id}" return f"{super().waypoint_name} #{self.object_id}"
class GenericCarrierGroundObject(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)
@property
def might_have_aa(self) -> bool:
return True
class GenericCarrierGroundObject(NavalGroundObject):
pass pass
@ -216,8 +255,8 @@ class BaseDefenseGroundObject(TheaterGroundObject):
# TODO: Differentiate types. # TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# armor garrisons at airbases. These should each be split into their own types. # be split into their own types.
class SamGroundObject(BaseDefenseGroundObject): class SamGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None: control_point: ControlPoint, for_airbase: bool) -> None:
@ -245,6 +284,32 @@ class SamGroundObject(BaseDefenseGroundObject):
else: else:
return super().group_name return super().group_name
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)
@property
def might_have_aa(self) -> bool:
return True
class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
)
class EwrGroundObject(BaseDefenseGroundObject): class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(self, name: str, group_id: int, position: Point,
@ -266,8 +331,18 @@ class EwrGroundObject(BaseDefenseGroundObject):
# Prefix the group names with the side color so Skynet can find them. # Prefix the group names with the side color so Skynet can find them.
return f"{self.faction_color}|{super().group_name}" return f"{self.faction_color}|{super().group_name}"
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)
class ShipGroundObject(TheaterGroundObject): @property
def might_have_aa(self) -> bool:
return True
class ShipGroundObject(NavalGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None: control_point: ControlPoint) -> None:
super().__init__( super().__init__(

133
game/unitmap.py Normal file
View File

@ -0,0 +1,133 @@
"""Maps generated units back to their Liberation types."""
from dataclasses import dataclass
from typing import Dict, Optional, Type
from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType
from game import db
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject
from gen.flights.flight import Flight
@dataclass(frozen=True)
class FrontLineUnit:
unit_type: Type[VehicleType]
origin: ControlPoint
@dataclass(frozen=True)
class GroundObjectUnit:
ground_object: TheaterGroundObject
group: Group
unit: Unit
@dataclass(frozen=True)
class Building:
ground_object: BuildingGroundObject
class UnitMap:
def __init__(self) -> None:
self.aircraft: Dict[str, Flight] = {}
self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
self.buildings: Dict[str, Building] = {}
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}")
self.aircraft[name] = flight
def flight(self, unit_name: str) -> Optional[Flight]:
return self.aircraft.get(unit_name, None)
def add_airfield(self, airfield: Airfield) -> None:
if airfield.name in self.airfields:
raise RuntimeError(f"Duplicate airfield: {airfield.name}")
self.airfields[airfield.name] = airfield
def airfield(self, name: str) -> Optional[Airfield]:
return self.airfields.get(name, None)
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.front_line_units:
raise RuntimeError(f"Duplicate front line unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
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")
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:
"""Adds a group associated with a TGO to the unit map.
Args:
ground_object: The TGO the group is associated with.
persistence_group: The Group tracked by the TGO itself.
miz_group: The Group spawned for the miz to match persistence_group.
"""
# Deaths for units at TGOs are recorded in the Group that is contained
# by the TGO, but when groundobjectsgen populates the miz it creates new
# groups based on that template, so the units and groups in the miz are
# not a direct match for the units and groups that persist in the TGO.
#
# This means that we need to map the spawned unit names back to the
# original TGO units, not the ones in the miz.
if len(persistence_group.units) != len(miz_group.units):
raise ValueError("Persistent group does not match generated group")
unit_pairs = zip(persistence_group.units, miz_group.units)
for persistent_unit, miz_unit in unit_pairs:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(miz_unit.name)
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)
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:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(group.name)
if name in self.buildings:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
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]
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.buildings:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def building_or_fortification(self, name: str) -> Optional[Building]:
return self.buildings.get(name, None)

View File

@ -1,14 +1,75 @@
def meter_to_feet(value_in_meter: float) -> int: def meter_to_feet(value_in_meter: float) -> int:
"""Converts meters to feets
:arg value_in_meter Value in meters
"""
return int(3.28084 * value_in_meter) return int(3.28084 * value_in_meter)
def feet_to_meter(value_in_feet: float) -> int: def feet_to_meter(value_in_feet: float) -> int:
"""Converts feets to meters
:arg value_in_feet Value in feets
"""
return int(value_in_feet / 3.28084) return int(value_in_feet / 3.28084)
def meter_to_nm(value_in_meter: float) -> int: def meter_to_nm(value_in_meter: float) -> int:
"""Converts meters to nautic miles
:arg value_in_meter Value in meters
"""
return int(value_in_meter / 1852) return int(value_in_meter / 1852)
def nm_to_meter(value_in_nm: float) -> int: def nm_to_meter(value_in_nm: float) -> int:
"""Converts nautic miles to meters
:arg value_in_nm Value in nautic miles
"""
return int(value_in_nm * 1852) return int(value_in_nm * 1852)
def knots_to_kph(value_in_knots: float) -> int:
"""Converts Knots to Kilometer Per Hour
:arg value_in_knots Knots
"""
return int(value_in_knots * 1.852)
def mps_to_knots(value_in_mps: float) -> int:
"""Converts Meters Per Second To Knots
:arg value_in_mps Meters Per Second
"""
return int(value_in_mps * 1.943)
def mps_to_kph(speed: float) -> int:
"""Converts meters per second to kilometers per hour.
:arg speed Speed in m/s.
"""
return int(speed * 3.6)
def kph_to_mps(speed: float) -> int:
"""Converts kilometers per hour to meters per second.
:arg speed Speed in KPH.
"""
return int(speed / 3.6)
def heading_sum(h, a) -> int:
h += a
if h > 360:
return h - 360
elif h < 0:
return 360 + h
else:
return h
def opposite_heading(h):
return heading_sum(h, 180)

View File

@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str: def _build_version_string() -> str:
components = ["2.2.0"] components = ["2.3.0"]
build_number_path = Path("resources/buildnumber") build_number_path = Path("resources/buildnumber")
if build_number_path.exists(): if build_number_path.exists():
with build_number_path.open("r") as build_number_file: with build_number_path.open("r") as build_number_file:

View File

@ -5,12 +5,14 @@ import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, TYPE_CHECKING
from dcs.weather import Weather as PydcsWeather, Wind from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings from game.settings import Settings
from theater import ConflictTheater
if TYPE_CHECKING:
from game.theater import ConflictTheater
class TimeOfDay(Enum): class TimeOfDay(Enum):

View File

@ -5,7 +5,7 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup from dcs.action import AITaskPush, ActivateGroup
@ -13,17 +13,22 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
B_17G, B_17G,
B_52H,
Bf_109K_4, Bf_109K_4,
C_101EB,
C_101CC,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
F_14B, F_14B,
I_16, I_16,
JF_17, JF_17,
Ju_88A4, Ju_88A4,
PlaneType,
P_47D_30, P_47D_30,
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
@ -31,34 +36,37 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_33, A_20G, Tu_22M3, B_52H, Su_33,
Tu_22M3,
) )
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
AntishipStrike, AntishipStrike,
AttackGroup, AttackGroup,
Bombing, Bombing,
BombingRunway,
CAP, CAP,
CAS, CAS,
ControlledTask, ControlledTask,
EPLRS, EPLRS,
EngageTargets, EngageTargets,
EngageTargetsInZone, EngageTargetsInZone,
FighterSweep,
GroundAttack, GroundAttack,
OptROE, OptROE,
OptRTBOnBingoFuel, OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo, OptRTBOnOutOfAmmo,
OptReactOnThreat, OptReactOnThreat,
OptRestrictAfterburner,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
PinpointStrike, RunwayAttack,
SEAD, SEAD,
StartCommand, StartCommand,
Targets, Targets,
Task, WeaponType, Task,
WeaponType,
) )
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
@ -66,11 +74,22 @@ from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.factions.faction import Faction
from game.settings import Settings from game.settings import Settings
from game.utils import nm_to_meter from game.theater.controlpoint import (
Airfield,
ControlPoint,
ControlPointType,
NavalControlPoint,
OffMapSpawn,
)
from game.theater.theatergroundobject import TheaterGroundObject
from game.unitmap import UnitMap
from game.utils import knots_to_kph, nm_to_meter
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import ( from gen.flights.flight import (
Flight, Flight,
FlightType, FlightType,
@ -79,17 +98,14 @@ from gen.flights.flight import (
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from gen.conflictgen import FRONTLINE_LENGTH
from dcs.mapping import Point
from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.flightplan import ( from .flights.flightplan import (
CasFlightPlan, CasFlightPlan,
FormationFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
SweepFlightPlan,
) )
from .flights.traveltime import TotEstimator from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen from .naming import namegen
from .runways import RunwayAssigner from .runways import RunwayAssigner
@ -281,12 +297,19 @@ class FlightData:
#: Map of radio frequencies to their assigned radio and channel, if any. #: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment] frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
#: Bingo fuel value in lbs.
bingo_fuel: Optional[int]
joker_fuel: Optional[int]
def __init__(self, package: Package, flight_type: FlightType, def __init__(self, package: Package, flight_type: FlightType,
units: List[FlyingUnit], size: int, friendly: bool, units: List[FlyingUnit], size: int, friendly: bool,
departure_delay: timedelta, departure: RunwayData, departure_delay: timedelta, departure: RunwayData,
arrival: RunwayData, divert: Optional[RunwayData], arrival: RunwayData, divert: Optional[RunwayData],
waypoints: List[FlightWaypoint], waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None: intra_flight_channel: RadioFrequency,
bingo_fuel: Optional[int],
joker_fuel: Optional[int]) -> None:
self.package = package self.package = package
self.flight_type = flight_type self.flight_type = flight_type
self.units = units self.units = units
@ -299,6 +322,8 @@ class FlightData:
self.waypoints = waypoints self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {} self.frequency_to_channel_map = {}
self.bingo_fuel = bingo_fuel
self.joker_fuel = joker_fuel
self.callsign = create_group_callsign_from_unit(self.units[0]) self.callsign = create_group_callsign_from_unit(self.units[0])
@property @property
@ -640,13 +665,13 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator: class AircraftConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, def __init__(self, mission: Mission, settings: Settings, game: Game,
game: Game, radio_registry: RadioRegistry): radio_registry: RadioRegistry, unit_map: UnitMap) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
self.settings = settings self.settings = settings
self.conflict = conflict
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
@cached_property @cached_property
@ -739,25 +764,15 @@ class AircraftConflictGenerator:
if unit_type is F_14B: if unit_type is F_14B:
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True) unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
channel = self.get_intra_flight_channel(unit_type) channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz) group.set_frequency(channel.mhz)
# TODO: Support for different departure/arrival airfields. divert = None
cp = flight.from_cp if flight.divert is not None:
fallback_runway = RunwayData(cp.full_name, runway_heading=0, divert = flight.divert.active_runway(self.game.conditions,
runway_name="") dynamic_runways)
if cp.cptype == ControlPointType.AIRBASE:
assigner = RunwayAssigner(self.game.conditions)
departure_runway = assigner.get_preferred_runway(
flight.from_cp.airport)
elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else:
logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway
self.flights.append(FlightData( self.flights.append(FlightData(
package=package, package=package,
@ -767,26 +782,25 @@ class AircraftConflictGenerator:
friendly=flight.from_cp.captured, friendly=flight.from_cp.captured,
# Set later. # Set later.
departure_delay=timedelta(), departure_delay=timedelta(),
departure=departure_runway, departure=flight.departure.active_runway(self.game.conditions,
arrival=departure_runway, dynamic_runways),
# TODO: Support for divert airfields. arrival=flight.arrival.active_runway(self.game.conditions,
divert=None, dynamic_runways),
divert=divert,
# Waypoints are added later, after they've had their TOTs set. # Waypoints are added later, after they've had their TOTs set.
waypoints=[], waypoints=[],
intra_flight_channel=channel intra_flight_channel=channel,
bingo_fuel=flight.flight_plan.bingo_fuel,
joker_fuel=flight.flight_plan.joker_fuel
)) ))
# Special case so Su 33 carrier take off # Special case so Su 33 and C101 can take off
if unit_type is Su_33: if unit_type in [Su_33, C_101EB, C_101CC]:
if flight.flight_type is not CAP: self.set_reduced_fuel(flight, group, unit_type)
for unit in group.units:
unit.fuel = Su_33.fuel_max / 2.2
else:
for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8
def _generate_at_airport(self, name: str, side: Country, def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int, start_type: str, unit_type: Type[FlyingType], count: int,
start_type: str,
airport: Optional[Airport] = None) -> FlyingGroup: airport: Optional[Airport] = None) -> FlyingGroup:
assert count > 0 assert count > 0
@ -801,35 +815,42 @@ class AircraftConflictGenerator:
group_size=count, group_size=count,
parking_slots=None) parking_slots=None)
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup: def _generate_inflight(self, name: str, side: Country, flight: Flight,
assert count > 0 origin: ControlPoint) -> FlyingGroup:
assert flight.count > 0
at = origin.position
if unit_type in helicopters.helicopter_map.values(): alt_type = "RADIO"
if isinstance(origin, OffMapSpawn):
alt = flight.flight_plan.waypoints[0].alt
alt_type = flight.flight_plan.waypoints[0].alt_type
elif flight.unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT alt = WARM_START_HELI_ALT
speed = WARM_START_HELI_AIRSPEED
else: else:
alt = WARM_START_ALTITUDE alt = WARM_START_ALTITUDE
speed = WARM_START_AIRSPEED
speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000)) pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed)) logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
group = self.m.flight_group( group = self.m.flight_group(
country=side, country=side,
name=name, name=name,
aircraft_type=unit_type, aircraft_type=flight.unit_type,
airport=None, airport=None,
position=pos, position=pos,
altitude=alt, altitude=alt,
speed=speed, speed=speed,
maintask=None, maintask=None,
group_size=count) group_size=flight.count)
group.points[0].alt_type = "RADIO" group.points[0].alt_type = alt_type
return group return group
def _generate_at_group(self, name: str, side: Country, def _generate_at_group(self, name: str, side: Country,
unit_type: FlyingType, count: int, start_type: str, unit_type: Type[FlyingType], count: int,
start_type: str,
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup: at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
assert count > 0 assert count > 0
@ -875,7 +896,6 @@ class AircraftConflictGenerator:
else: else:
assert False assert False
def _setup_custom_payload(self, flight, group:FlyingGroup): def _setup_custom_payload(self, flight, group:FlyingGroup):
if flight.use_custom_loadout: if flight.use_custom_loadout:
@ -895,13 +915,11 @@ class AircraftConflictGenerator:
def clear_parking_slots(self) -> None: def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.airport is not None: for parking_slot in cp.parking_slots:
for parking_slot in cp.airport.parking_slots: parking_slot.unit_id = None
parking_slot.unit_id = None
def generate_flights(self, country, ato: AirTaskingOrder, def generate_flights(self, country, ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
self.clear_parking_slots()
for package in ato.packages: for package in ato.packages:
if not package.flights: if not package.flights:
@ -914,9 +932,59 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}") logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country, group = self.generate_planned_flight(flight.from_cp, country,
flight) flight)
self.unit_map.add_aircraft(group, flight)
self.setup_flight_group(group, package, flight, dynamic_runways) self.setup_flight_group(group, package, flight, dynamic_runways)
self.create_waypoints(group, package, flight) self.create_waypoints(group, package, flight)
def spawn_unused_aircraft(self, player_country: Country,
enemy_country: Country) -> None:
inventories = self.game.aircraft_inventory.inventories
for control_point, inventory in inventories.items():
if not isinstance(control_point, Airfield):
continue
if control_point.captured:
country = player_country
faction = self.game.player_faction
else:
country = enemy_country
faction = self.game.enemy_faction
for aircraft, available in inventory.all_aircraft:
try:
self._spawn_unused_at(control_point, country, faction, aircraft,
available)
except NoParkingSlotError:
# If we run out of parking, stop spawning aircraft.
return
def _spawn_unused_at(self, control_point: Airfield, country: Country, faction: Faction,
aircraft: Type[FlyingType], number: int) -> None:
for _ in range(number):
# Creating a flight even those this isn't a fragged mission lets us
# reuse the existing debriefing code.
# TODO: Special flight type?
flight = Flight(Package(control_point), aircraft, 1,
FlightType.BARCAP, "Cold", departure=control_point,
arrival=control_point, divert=None)
group = self._generate_at_airport(
name=namegen.next_unit_name(country, control_point.id,
aircraft),
side=country,
unit_type=aircraft,
count=1,
start_type="Cold",
airport=control_point.airport)
if aircraft in faction.liveries_overrides:
livery = random.choice(faction.liveries_overrides[aircraft])
for unit in group.units:
unit.livery_id = livery
group.uncontrolled = True
self.unit_map.add_aircraft(group, flight)
def set_activation_time(self, flight: Flight, group: FlyingGroup, def set_activation_time(self, flight: Flight, group: FlyingGroup,
delay: timedelta) -> None: delay: timedelta) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the # Note: Late activation causes the waypoint TOTs to look *weird* in the
@ -971,10 +1039,9 @@ class AircraftConflictGenerator:
group = self._generate_inflight( group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, flight=flight,
count=flight.count, origin=cp)
at=cp.position) elif isinstance(cp, NavalControlPoint):
elif cp.is_fleet:
group_name = cp.get_carrier_group_name() group_name = cp.get_carrier_group_name()
group = self._generate_at_group( group = self._generate_at_group(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
@ -984,8 +1051,12 @@ class AircraftConflictGenerator:
start_type=flight.start_type, start_type=flight.start_type,
at=self.m.find_group(group_name)) at=self.m.find_group(group_name))
else: else:
if not isinstance(cp, Airfield):
raise RuntimeError(
f"Attempted to spawn at airfield for non-airfield {cp}")
group = self._generate_at_airport( group = self._generate_at_airport(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id,
flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, unit_type=flight.unit_type,
count=flight.count, count=flight.count,
@ -999,13 +1070,26 @@ class AircraftConflictGenerator:
group = self._generate_inflight( group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type), name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country, side=country,
unit_type=flight.unit_type, flight=flight,
count=flight.count, origin=cp)
at=cp.position)
group.points[0].alt = 1500 group.points[0].alt = 1500
return group return group
@staticmethod
def set_reduced_fuel(flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]) -> None:
if unit_type is Su_33:
for unit in group.units:
if flight.flight_type is not CAP:
unit.fuel = Su_33.fuel_max / 2.2
else:
unit.fuel = Su_33.fuel_max * 0.8
elif unit_type in [C_101EB, C_101CC]:
for unit in group.units:
unit.fuel = unit_type.fuel_max * 0.5
else:
raise RuntimeError(f"No reduced fuel case for type {unit_type}")
@staticmethod @staticmethod
def configure_behavior( def configure_behavior(
group: FlyingGroup, group: FlyingGroup,
@ -1046,8 +1130,18 @@ class AircraftConflictGenerator:
self.configure_behavior(group, rtb_winchester=ammo_type) self.configure_behavior(group, rtb_winchester=ammo_type)
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), def configure_sweep(self, group: FlyingGroup, package: Package,
targets=[Targets.All.Air])) flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = FighterSweep.name
self._setup_group(group, FighterSweep, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
self.configure_behavior(group, rtb_winchester=ammo_type)
def configure_cas(self, group: FlyingGroup, package: Package, def configure_cas(self, group: FlyingGroup, package: Package,
flight: Flight, flight: Flight,
@ -1108,6 +1202,28 @@ class AircraftConflictGenerator:
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True) restrict_jettison=True)
def configure_runway_attack(
self, group: FlyingGroup, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = RunwayAttack.name
self._setup_group(group, RunwayAttack, package, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_oca_strike(
self, group: FlyingGroup, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = CAS.name
self._setup_group(group, CAS, package, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_escort(self, group: FlyingGroup, package: Package, def configure_escort(self, group: FlyingGroup, package: Package,
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
@ -1121,7 +1237,7 @@ class AircraftConflictGenerator:
def configure_unknown_task(self, group: FlyingGroup, def configure_unknown_task(self, group: FlyingGroup,
flight: Flight) -> None: flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type.name}") logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(group) self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, package: Package, def setup_flight_group(self, group: FlyingGroup, package: Package,
@ -1131,18 +1247,25 @@ class AircraftConflictGenerator:
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]: FlightType.INTERCEPTION]:
self.configure_cap(group, package, flight, dynamic_runways) self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type == FlightType.SWEEP:
self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.DEAD, ]: elif flight_type == FlightType.DEAD:
self.configure_dead(group, package, flight, dynamic_runways) self.configure_dead(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.SEAD, ]: elif flight_type == FlightType.SEAD:
self.configure_sead(group, package, flight, dynamic_runways) self.configure_sead(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.STRIKE]: elif flight_type == FlightType.STRIKE:
self.configure_strike(group, package, flight, dynamic_runways) self.configure_strike(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.ANTISHIP]: elif flight_type == FlightType.ANTISHIP:
self.configure_anti_ship(group, package, flight, dynamic_runways) self.configure_anti_ship(group, package, flight, dynamic_runways)
elif flight_type == FlightType.ESCORT: elif flight_type == FlightType.ESCORT:
self.configure_escort(group, package, flight, dynamic_runways) self.configure_escort(group, package, flight, dynamic_runways)
elif flight_type == FlightType.OCA_RUNWAY:
self.configure_runway_attack(group, package, flight,
dynamic_runways)
elif flight_type == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, package, flight, dynamic_runways)
else: else:
self.configure_unknown_task(group, flight) self.configure_unknown_task(group, flight)
@ -1258,10 +1381,13 @@ class PydcsWaypointBuilder:
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt) Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt,
name=self.mission.string(self.waypoint.name))
if self.waypoint.flyover:
waypoint.type = PointAction.FlyOverPoint.value
waypoint.alt_type = self.waypoint.alt_type waypoint.alt_type = self.waypoint.alt_type
waypoint.name = String(self.waypoint.name)
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint) tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
if tot is not None: if tot is not None:
self.set_waypoint_tot(waypoint, tot) self.set_waypoint_tot(waypoint, tot)
@ -1279,13 +1405,18 @@ class PydcsWaypointBuilder:
package: Package, flight: Flight, package: Package, flight: Flight,
mission: Mission) -> PydcsWaypointBuilder: mission: Mission) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder, FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder, FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
FlightWaypointType.INGRESS_OCA_AIRCRAFT: OcaAircraftIngressBuilder,
FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.JOIN: JoinPointBuilder,
FlightWaypointType.LANDING_POINT: LandingPointBuilder, FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder, FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL: RaceTrackEndBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
@ -1323,7 +1454,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt, altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle pattern=OrbitAction.OrbitPattern.Circle
)) ))
if not isinstance(self.flight.flight_plan, FormationFlightPlan): if not isinstance(self.flight.flight_plan, LoiterFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__ flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error( logging.error(
f"Cannot configure hold for for {self.flight} because " f"Cannot configure hold for for {self.flight} because "
@ -1338,6 +1469,32 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class BaiIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
# Match search is used due to TheaterGroundObject.name not matching
# the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_name,
search="match")
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
logging.error("Could not find group for BAI mission %s",
target_group.group_name)
else:
logging.error("Unexpected target type for BAI mission: %s",
target_group.__class__.__name__)
return waypoint
class CasIngressBuilder(PydcsWaypointBuilder): class CasIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
@ -1371,14 +1528,16 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target target_group = self.package.target
if isinstance(target_group, TheaterGroundObject): if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes. # the Mission group name because of SkyNet prefixes.
task = AttackGroup(tgroup.id) tgroup = self.mission.find_group(target_group.group_name,
search="match")
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
task.params["expend"] = "All" task.params["expend"] = "All"
task.params["attackQtyLimit"] = False task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False task.params["altitudeEnabled"] = False
task.params["weaponType"] = 268402702 # Guided Weapons
task.params["groupAttack"] = True task.params["groupAttack"] = True
waypoint.tasks.append(task) waypoint.tasks.append(task)
else: else:
@ -1387,14 +1546,59 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target = self.package.target
if not isinstance(target, Airfield):
logging.error(
"Unexpected target type for OCA Strike mission: %s",
target.__class__.__name__)
return waypoint
task = EngageTargetsInZone(
position=target.position,
# Al Dhafra is 4 nm across at most. Add a little wiggle room in case
# the airport position from DCS is not centered.
radius=nm_to_meter(3),
targets=[Targets.All.Air]
)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint
class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target = self.package.target
if not isinstance(target, Airfield):
logging.error(
"Unexpected target type for runway bombing mission: %s",
target.__class__.__name__)
return waypoint
waypoint.tasks.append(
BombingRunway(airport_id=target.airport.id, group_attack=True))
return waypoint
class SeadIngressBuilder(PydcsWaypointBuilder): class SeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
target_group = self.package.target target_group = self.package.target
if isinstance(target_group, TheaterGroundObject): if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes. # the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_name,
search="match")
if tgroup is not None:
waypoint.add_task(EngageTargetsInZone( waypoint.add_task(EngageTargetsInZone(
position=tgroup.position, position=tgroup.position,
radius=nm_to_meter(30), radius=nm_to_meter(30),
@ -1467,6 +1671,24 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class SweepIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if not isinstance(self.flight.flight_plan, SweepFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create sweep for {self.flight} because "
f"{flight_plan_type} is not a sweep flight plan.")
return waypoint
waypoint.tasks.append(EngageTargets(
max_distance=nm_to_meter(50),
targets=[Targets.All.Air.Planes.Fighters]))
return waypoint
class JoinPointBuilder(PydcsWaypointBuilder): class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
@ -1541,4 +1763,29 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack.stop_after_time( racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds())) int(self.flight.flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
# TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might
# be good to make this usable for things like BAI when we add that
# later.
cap_types = {FlightType.BARCAP, FlightType.TARCAP}
if self.flight.flight_type in cap_types:
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
return waypoint
class RaceTrackEndBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create race track for {self.flight} because "
f"{flight_plan_type} does not define a patrol.")
return waypoint
self.waypoint.departure_time = self.flight.flight_plan.patrol_end_time
return waypoint return waypoint

View File

@ -1,3 +1,4 @@
import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Type from typing import List, Type
@ -67,7 +68,7 @@ class AirSupportConflictGenerator:
def support_tasks(cls) -> List[Type[MainTask]]: def support_tasks(cls) -> List[Type[MainTask]]:
return [Refueling, AWACS] return [Refueling, AWACS]
def generate(self, is_awacs_enabled): def generate(self):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
fallback_tanker_number = 0 fallback_tanker_number = 0
@ -120,26 +121,28 @@ class AirSupportConflictGenerator:
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)) self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
if is_awacs_enabled: possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
try:
freq = self.radio_registry.alloc_uhf()
awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
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)),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) if len(possible_awacs) > 0:
awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
self.air_support.awacs.append(AwacsInfo( awacs_flight = self.mission.awacs_flight(
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq)) country=self.mission.country(self.game.player_country),
except: name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
print("No AWACS for faction") plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(AwacsInfo(
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
else:
logging.warning("No AWACS for faction")

View File

@ -1,7 +1,9 @@
from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import TYPE_CHECKING, List, Optional, Tuple
from dcs import Mission from dcs import Mission
from dcs.action import AITaskPush from dcs.action import AITaskPush
@ -10,31 +12,28 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction from dcs.point import PointAction
from dcs.task import ( from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
AttackGroup, GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
ControlledTask, SetInvisibleCommand)
EPLRS,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game import db from game import db
from .naming import namegen from game.unitmap import UnitMap
from gen.ground_forces.ai_ground_planner import ( from game.utils import heading_sum, opposite_heading
CombatGroupRole, from game.theater.controlpoint import ControlPoint
DISTANCE_FROM_FRONTLINE,
) from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
CombatGroup, CombatGroupRole)
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
from game.plugins import LuaPluginManager from .naming import namegen
if TYPE_CHECKING:
from game import Game
SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1 SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@ -65,79 +64,87 @@ class JtacInfo:
class GroundConflictGenerator: class GroundConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): 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 self.mission = mission
self.conflict = conflict self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance) self.player_stance = CombatStance(player_stance)
self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE]) self.enemy_stance = self._enemy_stance()
self.game = game self.game = game
self.unit_map = unit_map
self.jtacs: List[JtacInfo] = [] self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point: 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):
return random.choice(
[
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.AGGRESSIVE,
CombatStance.ELIMINATION,
CombatStance.BREAKTHROUGH
]
)
else:
return random.choice(
[
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
CombatStance.AGGRESSIVE
]
)
@staticmethod
def _group_point(point: Point, base_distance) -> Point:
distance = random.randint( distance = random.randint(
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]), int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
) )
return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR) return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
def generate(self): def generate(self):
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
player_groups = [] frontline_vector = Conflict.frontline_vector(
enemy_groups = [] self.conflict.from_cp,
self.conflict.to_cp,
combat_width = self.conflict.distance/2 self.game.theater
if combat_width > 500000: )
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp)
# Create player groups at random position # Create player groups at random position
for group in self.player_planned_combat_groups: player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
else:
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline)
if final_position is not None:
g = self._generate_group(
side=self.mission.country(self.game.player_country),
unit=group.units[0],
heading=self.conflict.heading+90,
count=len(group.units),
at=final_position)
g.set_skill(self.game.settings.player_skill)
player_groups.append((g,group))
self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90)
# Create enemy groups at random position # Create enemy groups at random position
for group in self.enemy_planned_combat_groups: enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
else:
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline)
if final_position is not None:
g = self._generate_group(
side=self.mission.country(self.game.enemy_country),
unit=group.units[0],
heading=self.conflict.heading - 90,
count=len(group.units),
at=final_position)
g.set_skill(self.game.settings.enemy_vehicle_skill)
enemy_groups.append((g, group))
self.gen_infantry_group_for_group(g, False, self.mission.country(self.game.enemy_country), self.conflict.heading - 90)
# Plan combat actions for groups # Plan combat actions for groups
self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp) self.plan_action_for_groups(
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp) self.player_stance,
player_groups,
enemy_groups,
self.conflict.heading + 90,
self.conflict.from_cp,
self.conflict.to_cp
)
self.plan_action_for_groups(
self.enemy_stance,
enemy_groups,
player_groups,
self.conflict.heading - 90,
self.conflict.to_cp,
self.conflict.from_cp
)
# Add JTAC # Add JTAC
if self.game.player_faction.has_jtac: if self.game.player_faction.has_jtac:
@ -162,11 +169,13 @@ class GroundConflictGenerator:
callsign = callsign_for_support_unit(jtac) callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code))) self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading): def gen_infantry_group_for_group(
self,
# Disable infantry unit gen if disabled group: VehicleGroup,
if not self.game.settings.perf_infantry: is_player: bool,
return side: Country,
forward_heading: int
) -> None:
infantry_position = group.points[0].position.random_point_within(250, 50) infantry_position = group.points[0].position.random_point_within(250, 50)
@ -180,7 +189,24 @@ class GroundConflictGenerator:
else: else:
faction = self.game.enemy_name faction = self.game.enemy_name
possible_infantry_units = db.find_infantry(faction) # Disable infantry unit gen if disabled
if not self.game.settings.perf_infantry:
if self.game.settings.manpads:
# 50% of armored units protected by manpad
if random.choice([True, False]):
manpads = db.find_manpad(faction)
if len(manpads) > 0:
u = random.choice(manpads)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp, u), u,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
return
possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads)
if len(possible_infantry_units) == 0: if len(possible_infantry_units) == 0:
return return
@ -204,125 +230,191 @@ class GroundConflictGenerator:
heading=forward_heading, heading=forward_heading,
move_formation=PointAction.OffRoad) move_formation=PointAction.OffRoad)
def _plan_artillery_action(
self,
stance: CombatStance,
gen_group: CombatGroup,
dcs_group: VehicleGroup,
forward_heading: int,
target: Point
) -> bool:
"""
Handles adding the DCS tasks for artillery groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance.
"""
if stance != CombatStance.RETREAT:
hold_task = Hold()
hold_task.number = 1
dcs_group.add_trigger_action(hold_task)
def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp): # Artillery strike random start
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)
fire_task.number = 2 if stance != CombatStance.RETREAT else 1
dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
self.mission.triggerrules.triggers.append(artillery_trigger)
# Artillery will fall back when under attack
if stance != CombatStance.RETREAT:
# Hold position
dcs_group.points[0].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)
dcs_group.points[1].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
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:
artillery_fallback.add_condition(Or())
hold_2 = Hold()
hold_2.number = 3
dcs_group.add_trigger_action(hold_2)
retreat_task = GoToWaypoint(to_index=3)
retreat_task.number = 4
dcs_group.add_trigger_action(retreat_task)
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:
u.initial = True
u.heading = forward_heading + random.randint(-5, 5)
return True
return False
def _plan_tank_ifv_action(
self,
stance: CombatStance,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
dcs_group: VehicleGroup,
forward_heading: int,
to_cp: ControlPoint,
) -> bool:
"""
Handles adding the DCS tasks for tank and IFV groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance.
"""
if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any
# Then move forward OR Attack enemy base if it is not too far away
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
)
)
dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
dcs_group.points[1].tasks.append(AttackGroup(target.id))
if (
to_cp.position.distance_to_point(dcs_group.points[0].position)
<=
AGGRESIVE_MOVE_DISTANCE
):
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(
dcs_group,
forward_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:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_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
)
)
dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
dcs_group.points[i].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
dcs_group.add_waypoint(attack_point)
if stance != CombatStance.RETREAT:
self.add_morale_trigger(dcs_group, forward_heading)
return True
return False
def _plan_apc_atgm_action(
self,
stance: CombatStance,
dcs_group: VehicleGroup,
forward_heading: int,
to_cp: ControlPoint,
) -> bool:
"""
Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
Returns True if tasking was added, returns False if the stance was not a combat stance.
"""
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 = to_cp.position.random_point_within(500, 0)
else:
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:
self.add_morale_trigger(dcs_group, forward_heading)
return True
return False
def plan_action_for_groups(
self, stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int,
from_cp: ControlPoint,
to_cp: ControlPoint
) -> None:
if not self.game.settings.perf_moving_units: if not self.game.settings.perf_moving_units:
return return
for dcs_group, group in ally_groups: for dcs_group, group in ally_groups:
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
if hasattr(group.units[0], 'eplrs'): dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.units[0].eplrs:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
# Fire on any ennemy in range
if self.game.settings.perf_artillery: if self.game.settings.perf_artillery:
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups) target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
if target is not None: if target is not None:
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
if stance != CombatStance.RETREAT:
hold_task = Hold()
hold_task.number = 1
dcs_group.add_trigger_action(hold_task)
# Artillery strike random start
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45)* 60))
fire_task = FireAtPoint(target, len(group.units) * 10, 100)
if stance != CombatStance.RETREAT:
fire_task.number = 2
else:
fire_task.number = 1
dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
self.mission.triggerrules.triggers.append(artillery_trigger)
# Artillery will fall back when under attack
if stance != CombatStance.RETREAT:
# Hold position
dcs_group.points[0].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)
dcs_group.points[1].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
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:
artillery_fallback.add_condition(Or())
hold_2 = Hold()
hold_2.number = 3
dcs_group.add_trigger_action(hold_2)
retreat_task = GoToWaypoint(toIndex=3)
retreat_task.number = 4
dcs_group.add_trigger_action(retreat_task)
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:
u.initial = True
u.heading = forward_heading + random.randint(-5,5)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]: elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
if stance == CombatStance.AGGRESSIVE: self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
# Attack nearest enemy if any
# Then move forward OR Attack enemy base if it is not too far away
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))
dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
dcs_group.points[1].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
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:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
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)
i = 1
for target in targets:
rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
dcs_group.points[i].tasks.append(AttackGroup(target.id))
i = i + 1
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
dcs_group.add_waypoint(attack_point)
if stance != CombatStance.RETREAT:
self.add_morale_trigger(dcs_group, forward_heading)
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]: elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
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 = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
if stance != CombatStance.RETREAT:
self.add_morale_trigger(dcs_group, forward_heading)
if stance == CombatStance.RETREAT: if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back # In retreat mode, the units will fall back
@ -332,11 +424,10 @@ class GroundConflictGenerator:
else: else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading) retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OnRoad) dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad) dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None:
def add_morale_trigger(self, dcs_group, forward_heading):
""" """
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
""" """
@ -353,10 +444,13 @@ class GroundConflictGenerator:
dcs_group.manualHeading = True dcs_group.manualHeading = True
# We add a new retreat waypoint # We add a new retreat waypoint
dcs_group.add_waypoint(self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), PointAction.OffRoad) dcs_group.add_waypoint(
self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)),
PointAction.OffRoad
)
# Fallback task # Fallback task
fallback = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points))) fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
fallback.enabled = False fallback.enabled = False
dcs_group.add_trigger_action(Hold()) dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(fallback) dcs_group.add_trigger_action(fallback)
@ -372,8 +466,12 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(fallback) self.mission.triggerrules.triggers.append(fallback)
@staticmethod
def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE): def find_retreat_point(
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int = RETREAT_DISTANCE
) -> Point:
""" """
Find a point to retreat to Find a point to retreat to
:param dcs_group: DCS mission group we are searching a retreat point for :param dcs_group: DCS mission group we are searching a retreat point for
@ -382,7 +480,12 @@ class GroundConflictGenerator:
""" """
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance) return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
def find_offensive_point(self, dcs_group, frontline_heading, distance): @staticmethod
def find_offensive_point(
dcs_group: VehicleGroup,
frontline_heading: int,
distance: int
) -> Point:
""" """
Find a point to attack Find a point to attack
:param dcs_group: DCS mission group we are searching an attack point for :param dcs_group: DCS mission group we are searching an attack point for
@ -392,24 +495,36 @@ class GroundConflictGenerator:
""" """
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance) return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
def find_n_nearest_enemy_groups(self, player_group, enemy_groups, n): @staticmethod
def find_n_nearest_enemy_groups(
player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
n: int
) -> List[VehicleGroup]:
""" """
Return the neaarest enemy group for the player group Return the nearest enemy group for the player group
@param group Group for which we should find the nearest ennemies @param group Group for which we should find the nearest ennemies
@param enemy_groups Potential enemy groups @param enemy_groups Potential enemy groups
@param n number of nearby groups to take @param n number of nearby groups to take
""" """
targets = [] 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)) sorted_list = sorted(
enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position)
)
for i in range(n): for i in range(n):
# TODO: Is this supposed to return no groups if enemy_groups is less than n?
if len(sorted_list) <= i: if len(sorted_list) <= i:
break break
else: else:
targets.append(sorted_list[i][0]) targets.append(sorted_list[i][0])
return targets return targets
@staticmethod
def find_nearest_enemy_group(self, player_group, enemy_groups): def find_nearest_enemy_group(
player_group: VehicleGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
) -> Optional[VehicleGroup]:
""" """
Search the enemy groups for a potential target suitable to armored assault Search the enemy groups for a potential target suitable to armored assault
@param group Group for which we should find the nearest ennemy @param group Group for which we should find the nearest ennemy
@ -417,29 +532,33 @@ class GroundConflictGenerator:
""" """
min_distance = 99999999 min_distance = 99999999
target = None target = None
for dcs_group, group in enemy_groups: for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position) dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
if dist < min_distance: if dist < min_distance:
min_distance = dist min_distance = dist
target = dcs_group target = dcs_group
return target return target
@staticmethod
def get_artillery_target_in_range(self, dcs_group, group, enemy_groups): def get_artillery_target_in_range(
dcs_group: VehicleGroup,
group: CombatGroup,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
) -> Optional[Point]:
""" """
Search the enemy groups for a potential target suitable to an artillery unit Search the enemy groups for a potential target suitable to an artillery unit
""" """
# TODO: Update to return a list of groups instead of a single point
rng = group.units[0].threat_range rng = group.units[0].threat_range
if len(enemy_groups) == 0: if not enemy_groups:
return None return None
for o in range(10): for _ in range(10):
potential_target = random.choice(enemy_groups)[0] potential_target = random.choice(enemy_groups)[0]
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position) distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
if distance_to_target < rng: if distance_to_target < rng:
return potential_target.points[0].position return potential_target.points[0].position
return None return None
def get_artilery_group_distance_from_frontline(self, group): def get_artilery_group_distance_from_frontline(self, group):
""" """
For artilery group, decide the distance from frontline with the range of the unit For artilery group, decide the distance from frontline with the range of the unit
@ -451,23 +570,85 @@ class GroundConflictGenerator:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100 rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
return rg return rg
def get_valid_position_for_group(
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline): self,
conflict_position: Point,
combat_width: int,
distance_from_frontline: int,
heading: int,
spawn_heading: int
):
i = 0 i = 0
while i < 25: # 25 attempt for valid position while i < 1000:
heading_diff = -90 if isplayer else 90 shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
shifted = conflict_position[0].point_from_heading(self.conflict.heading, final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline)
random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
if self.conflict.theater.is_on_land(final_position): if self.conflict.theater.is_on_land(final_position):
return final_position return final_position
else: i += 1
i = i + 1 continue
continue
return None return None
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0): def _generate_groups(
self,
groups: List[CombatGroup],
frontline_vector: Tuple[Point, int, int],
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))
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)
else:
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
final_position = self.get_valid_position_for_group(
position,
combat_width,
distance_from_frontline,
heading,
spawn_heading
)
if final_position is not None:
g = self._generate_group(
self.mission.country(country),
group.units[0],
len(group.units),
final_position,
distance_from_frontline,
heading=opposite_heading(spawn_heading),
)
if is_player:
g.set_skill(self.game.settings.player_skill)
else:
g.set_skill(self.game.settings.enemy_vehicle_skill)
positioned_groups.append((g, group))
self.gen_infantry_group_for_group(
g,
is_player,
self.mission.country(country),
opposite_heading(spawn_heading)
)
else:
logging.warning(f"Unable to get valid position for {group}")
return positioned_groups
def _generate_group(
self,
side: Country,
unit: VehicleType,
count: int,
at: Point,
distance_from_frontline,
move_formation: PointAction = PointAction.OffRoad,
heading=0,
) -> VehicleGroup:
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
cp = self.conflict.from_cp cp = self.conflict.from_cp
@ -478,11 +659,13 @@ class GroundConflictGenerator:
group = self.mission.vehicle_group( group = self.mission.vehicle_group(
side, side,
namegen.next_unit_name(side, cp.id, unit), unit, namegen.next_unit_name(side, cp.id, unit), unit,
position=self._group_point(at), position=self._group_point(at, distance_from_frontline),
group_size=count, group_size=count,
heading=heading, heading=heading,
move_formation=move_formation) move_formation=move_formation)
self.unit_map.add_front_line_units(group, cp)
for c in range(count): for c in range(count):
vehicle: Vehicle = group.units[c] vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True vehicle.player_can_drive = True

View File

@ -16,7 +16,7 @@ from typing import Dict, List, Optional
from dcs.mapping import Point from dcs.mapping import Point
from theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from .flights.flight import Flight, FlightType from .flights.flight import Flight, FlightType
from .flights.flightplan import FormationFlightPlan from .flights.flightplan import FormationFlightPlan
@ -147,19 +147,14 @@ class Package:
FlightType.CAS, FlightType.CAS,
FlightType.STRIKE, FlightType.STRIKE,
FlightType.ANTISHIP, FlightType.ANTISHIP,
FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY,
FlightType.BAI, FlightType.BAI,
FlightType.EVAC,
FlightType.TROOP_TRANSPORT,
FlightType.RECON,
FlightType.ELINT,
FlightType.DEAD, FlightType.DEAD,
FlightType.SEAD, FlightType.SEAD,
FlightType.LOGISTICS,
FlightType.INTERCEPTION,
FlightType.TARCAP, FlightType.TARCAP,
FlightType.CAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.EWAR, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]
for task in task_priorities: for task in task_priorities:
@ -178,7 +173,10 @@ class Package:
task = self.primary_task task = self.primary_task
if task is None: if task is None:
return "No mission" return "No mission"
return task.name oca_strike_types = {FlightType.OCA_AIRCRAFT, FlightType.OCA_RUNWAY}
if task in oca_strike_types:
return "OCA Strike"
return str(task)
def __hash__(self) -> int: def __hash__(self) -> int:
# TODO: Far from perfect. Number packages? # TODO: Far from perfect. Number packages?

View File

@ -2,19 +2,20 @@
Briefing generation logic Briefing generation logic
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import random
import logging
from dataclasses import dataclass from dataclasses import dataclass
from theater.frontline import FrontLine from datetime import timedelta
from typing import List, Dict, TYPE_CHECKING from typing import Dict, List, TYPE_CHECKING
from jinja2 import Environment, FileSystemLoader, select_autoescape
from dcs.mission import Mission from dcs.mission import Mission
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.theater import ControlPoint, FrontLine
from .aircraft import FlightData from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo from .armor import JtacInfo
from theater import ControlPoint from .flights.flight import FlightWaypoint
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
@ -119,6 +120,16 @@ class MissionInfoGenerator:
raise NotImplementedError raise NotImplementedError
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
if waypoint.tot is not None:
time = timedelta(seconds=int(waypoint.tot.total_seconds()))
return f"T+{time} "
elif waypoint.departure_time is not None:
time = timedelta(seconds=int(waypoint.departure_time.total_seconds()))
return f"{depart_prefix} T+{time} "
return ""
class BriefingGenerator(MissionInfoGenerator): class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, game: Game): def __init__(self, mission: Mission, game: Game):
@ -134,6 +145,7 @@ class BriefingGenerator(MissionInfoGenerator):
trim_blocks=True, trim_blocks=True,
lstrip_blocks=True, lstrip_blocks=True,
) )
env.filters["waypoint_timing"] = format_waypoint_time
self.template = env.get_template("briefingtemplate_EN.j2") self.template = env.get_template("briefingtemplate_EN.j2")
def generate(self) -> None: def generate(self) -> None:

View File

@ -1,58 +1,16 @@
import logging import logging
import random import random
from typing import Tuple from typing import Tuple, Optional
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from theater import ConflictTheater, ControlPoint from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint
from game.utils import heading_sum, opposite_heading
AIR_DISTANCE = 40000
CAPTURE_AIR_ATTACKERS_DISTANCE = 25000
CAPTURE_AIR_DEFENDERS_DISTANCE = 60000
STRIKE_AIR_ATTACKERS_DISTANCE = 45000
STRIKE_AIR_DEFENDERS_DISTANCE = 25000
CAP_CAS_DISTANCE = 10000, 120000
GROUND_INTERCEPT_SPREAD = 5000
GROUND_DISTANCE_FACTOR = 1.4
GROUND_DISTANCE = 2000
GROUND_ATTACK_DISTANCE = 25000, 13000
TRANSPORT_FRONTLINE_DIST = 1800
INTERCEPT_ATTACKERS_HEADING = -45, 45
INTERCEPT_DEFENDERS_HEADING = -10, 10
INTERCEPT_CONFLICT_DISTANCE = 50000
INTERCEPT_ATTACKERS_DISTANCE = 100000
INTERCEPT_MAX_DISTANCE = 160000
INTERCEPT_MIN_DISTANCE = 100000
NAVAL_INTERCEPT_DISTANCE_FACTOR = 1
NAVAL_INTERCEPT_DISTANCE_MAX = 40000
NAVAL_INTERCEPT_STEP = 5000
FRONTLINE_LENGTH = 80000 FRONTLINE_LENGTH = 80000
FRONTLINE_MIN_CP_DISTANCE = 5000
FRONTLINE_DISTANCE_STRENGTH_FACTOR = 0.7
def _opposite_heading(h):
return h+180
def _heading_sum(h, a) -> int:
h += a
if h > 360:
return h - 360
elif h < 0:
return 360 + h
else:
return h
class Conflict: class Conflict:
def __init__(self, def __init__(self,
@ -64,12 +22,9 @@ class Conflict:
attackers_country: Country, attackers_country: Country,
defenders_country: Country, defenders_country: Country,
position: Point, position: Point,
heading=None, heading: Optional[int] = None,
distance=None, size: Optional[int] = None
ground_attackers_location: Point = None, ):
ground_defenders_location: Point = None,
air_attackers_location: Point = None,
air_defenders_location: Point = None):
self.attackers_side = attackers_side self.attackers_side = attackers_side
self.defenders_side = defenders_side self.defenders_side = defenders_side
@ -81,307 +36,39 @@ class Conflict:
self.theater = theater self.theater = theater
self.position = position self.position = position
self.heading = heading self.heading = heading
self.distance = distance self.size = size
self.size = to_cp.size
self.radials = to_cp.radials
self.ground_attackers_location = ground_attackers_location
self.ground_defenders_location = ground_defenders_location
self.air_attackers_location = air_attackers_location
self.air_defenders_location = air_defenders_location
@property
def center(self) -> Point:
return self.position.point_from_heading(self.heading, self.distance / 2)
@property
def tail(self) -> Point:
return self.position.point_from_heading(self.heading, self.distance)
@property
def is_vector(self) -> bool:
return self.heading is not None
@property
def opposite_heading(self) -> int:
return _heading_sum(self.heading, 180)
@property
def to_size(self):
return self.to_cp.size * GROUND_DISTANCE_FACTOR
def find_insertion_point(self, other_point: Point) -> Point:
if self.is_vector:
dx = self.position.x - self.tail.x
dy = self.position.y - self.tail.y
dr2 = float(dx ** 2 + dy ** 2)
lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2
if lerp < 0:
lerp = 0
elif lerp > 1:
lerp = 1
x = lerp * dx + self.tail.x
y = lerp * dy + self.tail.y
return Point(x, y)
else:
return self.position
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
@classmethod @classmethod
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool: def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline return from_cp.has_frontline and to_cp.has_frontline
@classmethod @classmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position) frontline = FrontLine(from_cp, to_cp, theater)
attack_distance = from_cp.position.distance_to_point(to_cp.position) attack_heading = frontline.attack_heading
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2) position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
return position, opposite_heading(attack_heading)
strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0
position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
return position, _opposite_heading(attack_heading)
@classmethod @classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
""" """
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH) Returns a vector for a valid frontline location avoiding exclusion zones.
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
intersection = probe.intersection(theater.land_poly)
if isinstance(intersection, geometry.LineString):
intersection = intersection
elif isinstance(intersection, geometry.MultiLineString):
intersection = intersection.geoms[0]
else:
print(intersection)
return None
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
""" """
frontline = cls.frontline_position(theater, from_cp, to_cp) center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
center_position, heading = frontline left_heading = heading_sum(heading, -90)
left_position, right_position = None, None right_heading = heading_sum(heading, 90)
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
if not theater.is_on_land(center_position): right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater) distance = int(left_position.distance_to_point(right_position))
if pos: return left_position, right_heading, distance
right_position = pos
center_position = pos
else:
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater)
if pos:
left_position = pos
center_position = pos
if left_position is None:
left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater)
if right_position is None:
right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater)
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
@classmethod
def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for offset in range(0, int(max_distance), 500):
new_pos = initial.point_from_heading(heading, offset)
if theater.is_on_land(new_pos):
pos = new_pos
else:
return pos
return pos
"""
probe_end_point = initial.point_from_heading(heading, max_distance)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)])
intersection = probe.intersection(theater.land_poly)
if intersection is geometry.LineString:
return Point(*intersection.xy[1])
elif intersection is geometry.MultiLineString:
return Point(*intersection.geoms[0].xy[1])
return None
"""
@classmethod
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for _ in range(0, int(max_distance), 500):
if theater.is_on_land(pos):
return pos
pos = pos.point_from_heading(heading, 500)
"""
probe_end_point = initial.point_from_heading(heading, max_distance)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
intersection = probe.intersection(theater.land_poly)
if isinstance(intersection, geometry.LineString):
return Point(*intersection.xy[1])
elif isinstance(intersection, geometry.MultiLineString):
return Point(*intersection.geoms[0].xy[1])
"""
logging.error("Didn't find ground position ({})!".format(initial))
return initial
@classmethod
def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
position = to_cp.position
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
attack_heading = to_cp.find_radial(attack_raw_heading)
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
distance = GROUND_DISTANCE
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater)
defenders_location = position.point_from_heading(defense_heading, 0)
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=attackers_location,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE),
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), CAPTURE_AIR_DEFENDERS_DISTANCE)
)
@classmethod
def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
position = to_cp.position
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
attack_heading = to_cp.find_radial(attack_raw_heading)
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
distance = to_cp.size * GROUND_DISTANCE_FACTOR
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_position(
attackers_location, int(distance * 2),
_heading_sum(attack_heading, 180), theater)
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_position(
defenders_location, int(distance * 2),
_heading_sum(defense_heading, 180), theater)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=attackers_location,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE),
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE)
)
@classmethod
def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point:
raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5
distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE)
heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100))
return from_cp.position.point_from_heading(heading, distance)
@classmethod
def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
heading = from_cp.position.heading_between_point(position)
return cls(
position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE),
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=None,
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
air_defenders_location=position
)
@classmethod
def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
heading = random.choice(to_cp.radials)
initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE)
position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
if not position:
heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=position,
ground_defenders_location=None,
air_attackers_location=None,
air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE),
)
@classmethod
def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater)
if not frontline_position:
assert False
heading = frontline_heading
starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000),
GROUND_INTERCEPT_SPREAD,
_opposite_heading(heading), theater)
if not starting_position:
starting_position = frontline_position
destination_position = frontline_position
else:
destination_position = frontline_position
return cls(
position=destination_position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=starting_position,
air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE),
air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE),
)
@classmethod @classmethod
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
assert cls.has_frontline_between(from_cp, to_cp) assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls(
return cls(
position=position, position=position,
heading=heading, heading=heading,
distance=distance,
theater=theater, theater=theater,
from_cp=from_cp, from_cp=from_cp,
to_cp=to_cp, to_cp=to_cp,
@ -389,114 +76,30 @@ class Conflict:
defenders_side=defender_name, defenders_side=defender_name,
attackers_country=attacker, attackers_country=attacker,
defenders_country=defender, defenders_country=defender,
ground_attackers_location=None, size=distance
ground_defenders_location=None,
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
) )
return conflict
@classmethod @classmethod
def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
assert cls.has_frontline_between(from_cp, to_cp) """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
pos = initial
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) for distance in range(0, int(max_distance), 100):
attack_position = position.point_from_heading(heading, random.randint(0, int(distance))) pos = initial.point_from_heading(heading, distance)
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE) if not theater.is_on_land(pos):
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE)) return initial.point_from_heading(heading, distance - 100)
return pos
return cls(
position=position,
heading=heading,
distance=distance,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
air_attackers_location=attackers_position,
air_defenders_location=defenders_position,
)
@classmethod @classmethod
def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
position = to_cp.position """Finds the nearest valid ground position along a provided heading and it's inverse"""
attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position)) pos = initial
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading) if theater.is_on_land(pos):
return pos
distance = to_cp.size * GROUND_DISTANCE_FACTOR for distance in range(0, int(max_distance), 100):
defenders_location = position.point_from_heading(defense_heading, distance) pos = initial.point_from_heading(heading, distance)
defenders_location = Conflict._find_ground_position( if theater.is_on_land(pos):
defenders_location, int(distance * 2), return pos
_heading_sum(defense_heading, 180), theater) pos = initial.point_from_heading(opposite_heading(heading), distance)
logging.error("Didn't find ground position ({})!".format(initial))
return cls( return initial
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE),
air_defenders_location=position
)
@classmethod
def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
radial = random.choice(to_cp.sea_radials)
initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX)
initial_position = to_cp.position.point_from_heading(radial, initial_distance)
for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
position = initial_position.point_from_heading(_opposite_heading(radial), offset)
if not theater.is_on_land(position):
break
return position
@classmethod
def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
attacker_heading = from_cp.position.heading_between_point(to_cp.position)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=position,
air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
air_defenders_location=position.point_from_heading(_opposite_heading(attacker_heading), AIR_DISTANCE)
)
@classmethod
def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp)
initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST)
dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
if not dest:
radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR)
return cls(
position=dest,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=from_cp.position,
ground_defenders_location=frontline_position,
air_attackers_location=from_cp.position.point_from_heading(0, 100),
air_defenders_location=frontline_position
)

View File

@ -14,7 +14,7 @@ from dcs.ships import (
from game.factions.faction import Faction from game.factions.faction import Faction
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -38,8 +38,8 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
if include_dd: if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer]) dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
self.add_unit(dd_type, "FF1", 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, "FF2", self.position.x + 2400, self.position.y - 900, self.heading) self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
if include_cc: if include_cc:
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy]) cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.factions.faction import Faction from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType from dcs.unittype import ShipType

View File

@ -16,7 +16,7 @@ from dcs.ships import (
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
@ -42,8 +42,8 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
if include_dd: if include_dd:
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky]) dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
self.add_unit(dd_type, "FF1", 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, "FF2", self.position.x + 2400, self.position.y - 900, self.heading) self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
if include_cc: if include_cc:
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy]) cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])

View File

@ -5,25 +5,53 @@ import operator
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type from typing import (
Iterable,
Iterator,
List,
Optional,
Set,
TYPE_CHECKING,
Tuple,
Type,
)
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType
from game import db from game import db
from game.data.radar_db import UNITS_WITH_RADAR from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information from game.infos.information import Information
from game.procurement import AircraftProcurementRequest
from game.theater import (
Airfield,
ControlPoint,
FrontLine,
MissionTarget,
OffMapSpawn,
SamGroundObject,
TheaterGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
from game.theater.theatergroundobject import (
EwrGroundObject,
NavalGroundObject, VehicleGroupGroundObject,
)
from game.utils import nm_to_meter from game.utils import nm_to_meter
from gen import Conflict from gen import Conflict
from gen.ato import Package from gen.ato import Package
from gen.flights.ai_flight_planner_db import ( from gen.flights.ai_flight_planner_db import (
ANTISHIP_CAPABLE,
ANTISHIP_PREFERRED,
CAP_CAPABLE, CAP_CAPABLE,
CAP_PREFERRED, CAP_PREFERRED,
CAS_CAPABLE, CAS_CAPABLE,
CAS_PREFERRED, CAS_PREFERRED,
RUNWAY_ATTACK_CAPABLE,
RUNWAY_ATTACK_PREFERRED,
SEAD_CAPABLE, SEAD_CAPABLE,
SEAD_PREFERRED, SEAD_PREFERRED,
STRIKE_CAPABLE, STRIKE_CAPABLE,
STRIKE_PREFERRED, STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task,
) )
from gen.flights.closestairfields import ( from gen.flights.closestairfields import (
ClosestAirfields, ClosestAirfields,
@ -35,15 +63,7 @@ from gen.flights.flight import (
) )
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from theater import (
ControlPoint,
FrontLine,
MissionTarget,
TheaterGroundObject,
SamGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
@ -68,7 +88,7 @@ class ProposedFlight:
max_distance: int max_distance: int
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.task.name} {self.num_aircraft} ship" return f"{self.task} {self.num_aircraft} ship"
@dataclass(frozen=True) @dataclass(frozen=True)
@ -103,7 +123,7 @@ class AircraftAllocator:
def find_aircraft_for_flight( def find_aircraft_for_flight(
self, flight: ProposedFlight self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, UnitType]]: ) -> Optional[Tuple[ControlPoint, FlyingType]]:
"""Finds aircraft suitable for the given mission. """Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the Searches for aircraft capable of performing the given mission within the
@ -123,50 +143,17 @@ class AircraftAllocator:
responsible for returning them to the inventory. responsible for returning them to the inventory.
""" """
result = self.find_aircraft_of_type( result = self.find_aircraft_of_type(
flight, self.preferred_aircraft_for_task(flight.task) flight, preferred_aircraft_for_task(flight.task)
) )
if result is not None: if result is not None:
return result return result
return self.find_aircraft_of_type( return self.find_aircraft_of_type(
flight, self.capable_aircraft_for_task(flight.task) flight, capable_aircraft_for_task(flight.task)
) )
@staticmethod
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_PREFERRED
elif task == FlightType.CAS:
return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED
elif task == FlightType.STRIKE:
return STRIKE_PREFERRED
elif task == FlightType.ESCORT:
return CAP_PREFERRED
else:
return []
@staticmethod
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.CAS:
return CAS_CAPABLE
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []
def find_aircraft_of_type( def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]], self, flight: ProposedFlight, types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, UnitType]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within( airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance flight.max_distance
) )
@ -175,6 +162,8 @@ class AircraftAllocator:
continue continue
inventory = self.global_inventory.for_control_point(airfield) inventory = self.global_inventory.for_control_point(airfield)
for aircraft, available in inventory.all_aircraft: for aircraft, available in inventory.all_aircraft:
if not airfield.can_operate(aircraft):
continue
if aircraft in types and available >= flight.num_aircraft: if aircraft in types and available >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft) inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, aircraft return airfield, aircraft
@ -190,6 +179,8 @@ class PackageBuilder:
global_inventory: GlobalAircraftInventory, global_inventory: GlobalAircraftInventory,
is_player: bool, is_player: bool,
start_type: str) -> None: start_type: str) -> None:
self.closest_airfields = closest_airfields
self.is_player = is_player
self.package = Package(location) self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory, self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player) is_player)
@ -208,11 +199,32 @@ class PackageBuilder:
if assignment is None: if assignment is None:
return False return False
airfield, aircraft = assignment airfield, aircraft = assignment
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield, if isinstance(airfield, OffMapSpawn):
plan.task, self.start_type) start_type = "In Flight"
else:
start_type = self.start_type
flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
start_type, departure=airfield, arrival=airfield,
divert=self.find_divert_field(aircraft, airfield))
self.package.add_flight(flight) self.package.add_flight(flight)
return True return True
def find_divert_field(self, aircraft: FlyingType,
arrival: ControlPoint) -> Optional[ControlPoint]:
divert_limit = nm_to_meter(150)
for airfield in self.closest_airfields.airfields_within(divert_limit):
if airfield.captured != self.is_player:
continue
if airfield == arrival:
continue
if not airfield.can_operate(aircraft):
continue
if isinstance(airfield, OffMapSpawn):
continue
return airfield
return None
def build(self) -> Package: def build(self) -> Package:
"""Returns the built package.""" """Returns the built package."""
return self.package return self.package
@ -243,7 +255,9 @@ class ObjectiveFinder:
found_targets: Set[str] = set() found_targets: Set[str] = set()
for cp in self.enemy_control_points(): for cp in self.enemy_control_points():
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if not isinstance(ground_object, SamGroundObject): is_ewr = isinstance(ground_object, EwrGroundObject)
is_sam = isinstance(ground_object, SamGroundObject)
if not is_ewr and not is_sam:
continue continue
if ground_object.is_dead: if ground_object.is_dead:
@ -262,22 +276,66 @@ class ObjectiveFinder:
yield ground_object yield ground_object
found_targets.add(ground_object.name) found_targets.add(ground_object.name)
def threatening_sams(self) -> Iterator[TheaterGroundObject]: def threatening_sams(self) -> Iterator[MissionTarget]:
"""Iterates over enemy SAMs in threat range of friendly control points. """Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet). point (airfield or fleet).
""" """
sams: List[Tuple[TheaterGroundObject, int]] = [] return self._targets_by_range(self.enemy_sams())
for sam in self.enemy_sams():
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
"""Iterates over all enemy vehicle groups."""
for cp in self.enemy_control_points():
for ground_object in cp.ground_objects:
if not isinstance(ground_object, VehicleGroupGroundObject):
continue
if ground_object.is_dead:
continue
yield ground_object
def threatening_vehicle_groups(self) -> Iterator[MissionTarget]:
"""Iterates over enemy vehicle groups near friendly control points.
Groups are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
return self._targets_by_range(self.enemy_vehicle_groups())
def enemy_ships(self) -> Iterator[NavalGroundObject]:
for cp in self.enemy_control_points():
for ground_object in cp.ground_objects:
if not isinstance(ground_object, NavalGroundObject):
continue
if ground_object.is_dead:
continue
yield ground_object
def threatening_ships(self) -> Iterator[MissionTarget]:
"""Iterates over enemy ships near friendly control points.
Groups are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
return self._targets_by_range(self.enemy_ships())
def _targets_by_range(
self,
targets: Iterable[MissionTarget]) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets:
ranges: List[int] = [] ranges: List[int] = []
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
ranges.append(sam.distance_to(cp)) ranges.append(target.distance_to(cp))
sams.append((sam, min(ranges))) target_ranges.append((target, min(ranges)))
sams = sorted(sams, key=operator.itemgetter(1)) target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
for sam, _range in sams: for target, _range in target_ranges:
yield sam yield target
def strike_targets(self) -> Iterator[TheaterGroundObject]: def strike_targets(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy strike targets. """Iterates over enemy strike targets.
@ -286,11 +344,17 @@ class ObjectiveFinder:
point (airfield or fleet). point (airfield or fleet).
""" """
targets: List[Tuple[TheaterGroundObject, int]] = [] targets: List[Tuple[TheaterGroundObject, int]] = []
# Control points might have the same ground object several times, for # Building objectives are made of several individual TGOs (one per
# some reason. # building).
found_targets: Set[str] = set() found_targets: Set[str] = set()
for enemy_cp in self.enemy_control_points(): for enemy_cp in self.enemy_control_points():
for ground_object in enemy_cp.ground_objects: for ground_object in enemy_cp.ground_objects:
if isinstance(ground_object, VehicleGroupGroundObject):
# BAI target, not strike target.
continue
if isinstance(ground_object, NavalGroundObject):
# Anti-ship target, not strike target.
continue
if ground_object.is_dead: if ground_object.is_dead:
continue continue
if ground_object.name in found_targets: if ground_object.name in found_targets:
@ -321,7 +385,7 @@ class ObjectiveFinder:
continue continue
if Conflict.has_frontline_between(cp, connected): if Conflict.has_frontline_between(cp, connected):
yield FrontLine(cp, connected) yield FrontLine(cp, connected, self.game.theater)
def vulnerable_control_points(self) -> Iterator[ControlPoint]: def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs. """Iterates over friendly CPs that are vulnerable to enemy CPs.
@ -330,6 +394,9 @@ class ObjectiveFinder:
CP. CP.
""" """
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
# Off-map spawn locations don't need protection.
continue
airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within( airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE self.AIRFIELD_THREAT_RANGE
@ -339,6 +406,15 @@ class ObjectiveFinder:
yield cp yield cp
break break
def oca_targets(self, min_aircraft: int) -> Iterator[MissionTarget]:
airfields = []
for control_point in self.enemy_control_points():
if not isinstance(control_point, Airfield):
continue
if control_point.base.total_aircraft >= min_aircraft:
airfields.append(control_point)
return self._targets_by_range(airfields)
def friendly_control_points(self) -> Iterator[ControlPoint]: def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points.""" """Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if return (c for c in self.game.theater.controlpoints if
@ -393,6 +469,9 @@ class CoalitionMissionPlanner:
# TODO: Merge into doctrine, also limit by aircraft. # TODO: Merge into doctrine, also limit by aircraft.
MAX_CAP_RANGE = nm_to_meter(100) MAX_CAP_RANGE = nm_to_meter(100)
MAX_CAS_RANGE = nm_to_meter(50) MAX_CAS_RANGE = nm_to_meter(50)
MAX_ANTISHIP_RANGE = nm_to_meter(150)
MAX_BAI_RANGE = nm_to_meter(150)
MAX_OCA_RANGE = nm_to_meter(150)
MAX_SEAD_RANGE = nm_to_meter(150) MAX_SEAD_RANGE = nm_to_meter(150)
MAX_STRIKE_RANGE = nm_to_meter(150) MAX_STRIKE_RANGE = nm_to_meter(150)
@ -401,6 +480,7 @@ class CoalitionMissionPlanner:
self.is_player = is_player self.is_player = is_player
self.objective_finder = ObjectiveFinder(self.game, self.is_player) self.objective_finder = ObjectiveFinder(self.game, self.is_player)
self.ato = self.game.blue_ato if is_player else self.game.red_ato self.ato = self.game.blue_ato if is_player else self.game.red_ato
self.procurement_requests: List[AircraftProcurementRequest] = []
def propose_missions(self) -> Iterator[ProposedMission]: def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order.""" """Identifies and iterates over potential mission in priority order."""
@ -410,7 +490,7 @@ class CoalitionMissionPlanner:
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
]) ])
# Find front lines, plan CAP. # Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines(): for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [ yield ProposedMission(front_line, [
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
@ -428,6 +508,29 @@ class CoalitionMissionPlanner:
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE), ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
]) ])
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),
])
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),
])
for target in self.objective_finder.oca_targets(min_aircraft=20):
yield ProposedMission(target, [
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE),
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE),
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE),
])
# Plan strike missions. # Plan strike missions.
for target in self.objective_finder.strike_targets(): for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [ yield ProposedMission(target, [
@ -470,6 +573,12 @@ class CoalitionMissionPlanner:
for proposed_flight in mission.flights: for proposed_flight in mission.flights:
if not builder.plan_flight(proposed_flight): if not builder.plan_flight(proposed_flight):
missing_types.add(proposed_flight.task) missing_types.add(proposed_flight.task)
self.procurement_requests.append(AircraftProcurementRequest(
near=mission.location,
range=proposed_flight.max_distance,
task_capability=proposed_flight.task,
number=proposed_flight.num_aircraft
))
if missing_types: if missing_types:
missing_types_str = ", ".join( missing_types_str = ", ".join(
@ -496,7 +605,11 @@ class CoalitionMissionPlanner:
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error)) yield timedelta(minutes=max(0, time + error))
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION) dca_types = {
FlightType.BARCAP,
FlightType.INTERCEPTION,
FlightType.TARCAP,
}
non_dca_packages = [p for p in self.ato.packages if non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types] p.primary_task not in dca_types]

View File

@ -1,3 +1,6 @@
import logging
from typing import List, Type
from dcs.helicopters import ( from dcs.helicopters import (
AH_1W, AH_1W,
AH_64A, AH_64A,
@ -36,7 +39,6 @@ from dcs.planes import (
F_4E, F_4E,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
F_A_18C,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
@ -79,19 +81,24 @@ from dcs.planes import (
Tu_22M3, Tu_22M3,
Tu_95MS, Tu_95MS,
WingLoong_I, WingLoong_I,
I_16
) )
from dcs.unittype import FlyingType
from gen.flights.flight import FlightType
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57
# TODO: These lists really ought to be era (faction) dependent. # TODO: These lists really ought to be era (faction) dependent.
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but # Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
# factions that also have F-4s should not. # factions that also have F-4s should not.
from pydcs_extensions.su57.su57 import Su_57
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
INTERCEPT_CAPABLE = [ INTERCEPT_CAPABLE = [
MiG_21Bis, MiG_21Bis,
MiG_25PD, MiG_25PD,
@ -100,7 +107,11 @@ INTERCEPT_CAPABLE = [
MiG_29A, MiG_29A,
MiG_29G, MiG_29G,
MiG_29K, MiG_29K,
JF_17,
J_11A,
Su_27,
Su_30,
Su_33,
M_2000C, M_2000C,
Mirage_2000_5, Mirage_2000_5,
Rafale_M, Rafale_M,
@ -108,6 +119,9 @@ INTERCEPT_CAPABLE = [
F_14A_135_GR, F_14A_135_GR,
F_14B, F_14B,
F_15C, F_15C,
F_16A,
F_16C_50,
FA_18C_hornet,
] ]
@ -144,6 +158,7 @@ CAP_CAPABLE = [
F_16A, F_16A,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_22A,
C_101CC, C_101CC,
L_39ZA, L_39ZA,
@ -154,6 +169,8 @@ CAP_CAPABLE = [
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
I_16,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
SpitfireLFMkIX, SpitfireLFMkIX,
@ -170,14 +187,13 @@ CAP_PREFERRED = [
MiG_19P, MiG_19P,
MiG_21Bis, MiG_21Bis,
MiG_23MLD, MiG_23MLD,
MiG_25PD,
MiG_29A, MiG_29A,
MiG_29G, MiG_29G,
MiG_29S, MiG_29S,
MiG_31,
Su_27, Su_27,
J_11A, J_11A,
JF_17,
Su_30, Su_30,
Su_33, Su_33,
Su_57, Su_57,
@ -189,6 +205,8 @@ CAP_PREFERRED = [
F_14A_135_GR, F_14A_135_GR,
F_14B, F_14B,
F_15C, F_15C,
F_16C_50,
F_22A,
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
@ -196,6 +214,8 @@ CAP_PREFERRED = [
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
SpitfireLFMkIX, SpitfireLFMkIX,
I_16,
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
@ -217,6 +237,7 @@ CAS_CAPABLE = [
Su_25, Su_25,
Su_25T, Su_25T,
Su_25TM, Su_25TM,
Su_30,
Su_34, Su_34,
JF_17, JF_17,
@ -230,14 +251,11 @@ CAS_CAPABLE = [
F_86F_Sabre, F_86F_Sabre,
F_5E_3, F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_15E,
B_1B, F_22A,
Tornado_IDS, Tornado_IDS,
Tornado_GR4, Tornado_GR4,
@ -272,12 +290,15 @@ CAS_CAPABLE = [
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
SpitfireLFMkIX, SpitfireLFMkIX,
I_16,
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
A_4E_C, A_4E_C,
Rafale_A_S, Rafale_A_S,
Rafale_B,
WingLoong_I, WingLoong_I,
MQ_9_Reaper, MQ_9_Reaper,
@ -291,17 +312,14 @@ CAS_PREFERRED = [
Su_25, Su_25,
Su_25T, Su_25T,
Su_25TM, Su_25TM,
Su_30,
Su_34, Su_34,
JF_17,
A_10A, A_10A,
A_10C, A_10C,
A_10C_2, A_10C_2,
AV8BNA, AV8BNA,
F_15E,
Tornado_GR4, Tornado_GR4,
C_101CC, C_101CC,
@ -317,9 +335,6 @@ CAS_PREFERRED = [
AH_64D, AH_64D,
AH_1W, AH_1W,
UH_1H,
Mi_8MT,
Mi_28N, Mi_28N,
Mi_24V, Mi_24V,
Ka_50, Ka_50,
@ -328,9 +343,11 @@ CAS_PREFERRED = [
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
A_20G, A_20G,
I_16,
A_4E_C, A_4E_C,
Rafale_A_S, Rafale_A_S,
Rafale_B,
WingLoong_I, WingLoong_I,
MQ_9_Reaper, MQ_9_Reaper,
@ -341,7 +358,7 @@ CAS_PREFERRED = [
SEAD_CAPABLE = [ SEAD_CAPABLE = [
F_4E, F_4E,
FA_18C_hornet, FA_18C_hornet,
F_15E,
F_16C_50, F_16C_50,
AV8BNA, AV8BNA,
JF_17, JF_17,
@ -358,18 +375,26 @@ SEAD_CAPABLE = [
Tornado_GR4, Tornado_GR4,
A_4E_C, A_4E_C,
Rafale_A_S Rafale_A_S,
Rafale_B
] ]
SEAD_PREFERRED = [ SEAD_PREFERRED = [
F_4E, F_4E,
Su_25T, Su_25T,
Su_25TM,
Tornado_IDS, Tornado_IDS,
F_16C_50,
FA_18C_hornet,
Su_30,
Su_34,
Su_24M,
] ]
# Aircraft used for Strike mission # Aircraft used for Strike mission
STRIKE_CAPABLE = [ STRIKE_CAPABLE = [
MiG_15bis, MiG_15bis,
MiG_21Bis,
MiG_27K, MiG_27K,
MB_339PAN, MB_339PAN,
@ -378,7 +403,15 @@ STRIKE_CAPABLE = [
Su_24MR, Su_24MR,
Su_25, Su_25,
Su_25T, Su_25T,
Su_25TM,
Su_27,
Su_33,
Su_30,
Su_34, Su_34,
MiG_29A,
MiG_29G,
MiG_29K,
MiG_29S,
Tu_160, Tu_160,
Tu_22M3, Tu_22M3,
@ -388,13 +421,13 @@ STRIKE_CAPABLE = [
M_2000C, M_2000C,
A_10A,
A_10C, A_10C,
A_10C_2, A_10C_2,
AV8BNA, AV8BNA,
F_86F_Sabre, F_86F_Sabre,
F_5E_3, F_5E_3,
F_14A_135_GR, F_14A_135_GR,
F_14B, F_14B,
F_15E, F_15E,
@ -429,7 +462,8 @@ STRIKE_CAPABLE = [
FW_190A8, FW_190A8,
A_4E_C, A_4E_C,
Rafale_A_S Rafale_A_S,
Rafale_B
] ]
@ -441,6 +475,10 @@ STRIKE_PREFERRED = [
B_52H, B_52H,
F_117A, F_117A,
F_15E, F_15E,
Su_24M,
Su_30,
Su_34,
Tornado_IDS,
Tornado_GR4, Tornado_GR4,
Tu_160, Tu_160,
Tu_22M3, Tu_22M3,
@ -448,27 +486,101 @@ STRIKE_PREFERRED = [
] ]
ANTISHIP_CAPABLE = [ ANTISHIP_CAPABLE = [
AJS37,
C_101CC,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
F_A_18C, FA_18C_hornet,
F_15E,
AV8BNA, AV8BNA,
JF_17, JF_17,
F_16A,
F_16C_50, Su_30,
A_10C, Su_34,
A_10C_2, Tu_22M3,
A_10A,
Tornado_IDS, Tornado_IDS,
Tornado_GR4, Tornado_GR4,
Ju_88A4, Ju_88A4,
Rafale_A_S Rafale_A_S,
Rafale_B
] ]
ANTISHIP_PREFERRED = [
AJS37,
C_101CC,
FA_18C_hornet,
JF_17,
Rafale_A_S,
Rafale_B,
Su_24M,
Su_30,
Su_34,
Tu_22M3,
Ju_88A4
]
RUNWAY_ATTACK_PREFERRED = [
JF_17,
Su_30,
Su_34,
Tornado_IDS,
]
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
DRONES = [ DRONES = [
MQ_9_Reaper, MQ_9_Reaper,
RQ_1A_Predator, RQ_1A_Predator,
WingLoong_I WingLoong_I
] ]
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_PREFERRED
elif task == FlightType.ANTISHIP:
return ANTISHIP_PREFERRED
elif task == FlightType.BAI:
return CAS_CAPABLE
elif task == FlightType.CAS:
return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED
elif task == FlightType.OCA_AIRCRAFT:
return CAS_PREFERRED
elif task == FlightType.OCA_RUNWAY:
return RUNWAY_ATTACK_PREFERRED
elif task == FlightType.STRIKE:
return STRIKE_PREFERRED
elif task == FlightType.ESCORT:
return CAP_PREFERRED
else:
return []
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.ANTISHIP:
return ANTISHIP_CAPABLE
elif task == FlightType.BAI:
return CAS_CAPABLE
elif task == FlightType.CAS:
return CAS_CAPABLE
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
return CAS_CAPABLE
elif task == FlightType.OCA_RUNWAY:
return RUNWAY_ATTACK_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []

View File

@ -1,7 +1,7 @@
"""Objective adjacency lists.""" """Objective adjacency lists."""
from typing import Dict, Iterator, List, Optional from typing import Dict, Iterator, List, Optional
from theater import ConflictTheater, ControlPoint, MissionTarget from game.theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields: class ClosestAirfields:

View File

@ -2,14 +2,14 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game import db from game import db
from theater.controlpoint import ControlPoint, MissionTarget from game.theater.controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING: if TYPE_CHECKING:
from gen.ato import Package from gen.ato import Package
@ -17,26 +17,22 @@ if TYPE_CHECKING:
class FlightType(Enum): class FlightType(Enum):
CAP = 0 # Do not use. Use BARCAP or TARCAP. TARCAP = "TARCAP"
TARCAP = 1 BARCAP = "BARCAP"
BARCAP = 2 CAS = "CAS"
CAS = 3 INTERCEPTION = "Intercept"
INTERCEPTION = 4 STRIKE = "Strike"
STRIKE = 5 ANTISHIP = "Anti-ship"
ANTISHIP = 6 SEAD = "SEAD"
SEAD = 7 DEAD = "DEAD"
DEAD = 8 ESCORT = "Escort"
ESCORT = 9 BAI = "BAI"
BAI = 10 SWEEP = "Fighter sweep"
OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft"
# Helos def __str__(self) -> str:
TROOP_TRANSPORT = 11 return self.value
LOGISTICS = 12
EVAC = 13
ELINT = 14
RECON = 15
EWAR = 16
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
@ -61,6 +57,11 @@ class FlightWaypointType(Enum):
LOITER = 18 LOITER = 18
INGRESS_ESCORT = 19 INGRESS_ESCORT = 19
INGRESS_DEAD = 20 INGRESS_DEAD = 20
INGRESS_SWEEP = 21
INGRESS_BAI = 22
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
class FlightWaypoint: class FlightWaypoint:
@ -87,6 +88,7 @@ class FlightWaypoint:
self.obj_name = "" self.obj_name = ""
self.pretty_name = "" self.pretty_name = ""
self.only_for_player = False self.only_for_player = False
self.flyover = False
# These are set very late by the air conflict generator (part of mission # These are set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes # generation). We do it late so that we don't need to propagate changes
@ -128,13 +130,16 @@ class FlightWaypoint:
class Flight: class Flight:
def __init__(self, package: Package, unit_type: FlyingType, count: int, def __init__(self, package: Package, unit_type: Type[FlyingType],
from_cp: ControlPoint, flight_type: FlightType, count: int, flight_type: FlightType, start_type: str,
start_type: str) -> None: departure: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint]) -> None:
self.package = package self.package = package
self.unit_type = unit_type self.unit_type = unit_type
self.count = count self.count = count
self.from_cp = from_cp self.departure = departure
self.arrival = arrival
self.divert = divert
self.flight_type = flight_type self.flight_type = flight_type
# TODO: Replace with FlightPlan. # TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = [] self.targets: List[MissionTarget] = []
@ -153,10 +158,14 @@ class Flight:
custom_waypoints=[] custom_waypoints=[]
) )
@property
def from_cp(self) -> ControlPoint:
return self.departure
@property @property
def points(self) -> List[FlightWaypoint]: def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]
def __repr__(self): def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \ name = db.unit_type_name(self.unit_type)
+ " (" + str(len(self.points)) + " wpt)" return f"[{self.flight_type}] {self.count} x {name}"

View File

@ -7,20 +7,28 @@ generating the waypoints for the mission.
""" """
from __future__ import annotations from __future__ import annotations
import math
from datetime import timedelta
from functools import cached_property
import logging import logging
import math
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.utils import nm_to_meter from game.theater import (
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject Airfield,
ControlPoint,
FrontLine,
MissionTarget,
SamGroundObject,
TheaterGroundObject,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.utils import nm_to_meter, meter_to_nm
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@ -31,7 +39,6 @@ if TYPE_CHECKING:
from game import Game from game import Game
from gen.ato import Package from gen.ato import Package
INGRESS_TYPES = { INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT, FlightWaypointType.INGRESS_ESCORT,
@ -47,10 +54,9 @@ class PlanningError(RuntimeError):
class InvalidObjectiveLocation(PlanningError): class InvalidObjectiveLocation(PlanningError):
"""Raised when the objective location is invalid for the mission type.""" """Raised when the objective location is invalid for the mission type."""
def __init__(self, task: FlightType, location: MissionTarget) -> None: def __init__(self, task: FlightType, location: MissionTarget) -> None:
super().__init__( super().__init__(f"{location.name} is not valid for {task} missions.")
f"{location.name} is not valid for {task.name} missions."
)
@dataclass(frozen=True) @dataclass(frozen=True)
@ -61,6 +67,10 @@ class FlightPlan:
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
"""A list of all waypoints in the flight plan, in order.""" """A list of all waypoints in the flight plan, in order."""
return list(self.iter_waypoints())
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError raise NotImplementedError
@property @property
@ -105,6 +115,47 @@ class FlightPlan:
""" """
raise NotImplementedError raise NotImplementedError
@cached_property
def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan
"""
distance_to_arrival = meter_to_nm(self.max_distance_from(self.flight.arrival))
bingo = 1000 # Minimum Emergency Fuel
bingo += 500 # Visual Traffic
bingo += 15 * distance_to_arrival
# TODO: Per aircraft tweaks.
if self.flight.divert is not None:
bingo += 10 * meter_to_nm(self.max_distance_from(self.flight.divert))
return round(bingo / 100) * 100
@cached_property
def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan
"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> int:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from.
"""
if not self.waypoints:
return 0
return max([cp.position.distance_to_point(w.position) for w in self.waypoints])
@property
def tot_offset(self) -> timedelta:
"""This flight's offset from the package's TOT.
Positive values represent later TOTs. An offset of -2 minutes is used
for a flight that has a TOT 2 minutes before the rest of the package.
"""
return timedelta()
# Not cached because changes to the package might alter the formation speed. # Not cached because changes to the package might alter the formation speed.
@property @property
def travel_time_to_target(self) -> Optional[timedelta]: def travel_time_to_target(self) -> Optional[timedelta]:
@ -147,13 +198,36 @@ class FlightPlan:
@dataclass(frozen=True) @dataclass(frozen=True)
class FormationFlightPlan(FlightPlan): class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint hold: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
raise NotImplementedError
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
raise NotImplementedError
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
@property
def push_time(self) -> timedelta:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan):
join: FlightWaypoint join: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -215,12 +289,6 @@ class FormationFlightPlan(FlightPlan):
return self.split_time return self.split_time
return None return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property @property
def push_time(self) -> timedelta: def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points( return self.join_time - TravelTime.between_points(
@ -260,8 +328,7 @@ class PatrollingFlightPlan(FlightPlan):
return self.patrol_end_time return self.patrol_end_time
return None return None
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError raise NotImplementedError
@property @property
@ -277,15 +344,17 @@ class PatrollingFlightPlan(FlightPlan):
class BarCapFlightPlan(PatrollingFlightPlan): class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@dataclass(frozen=True) @dataclass(frozen=True)
@ -293,16 +362,18 @@ class CasFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
target: FlightWaypoint target: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.target, self.target,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
def request_escort_at(self) -> Optional[FlightWaypoint]: def request_escort_at(self) -> Optional[FlightWaypoint]:
return self.patrol_start return self.patrol_start
@ -312,18 +383,25 @@ class CasFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True) @dataclass(frozen=True)
class FrontLineCapFlightPlan(PatrollingFlightPlan): class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
lead_time: timedelta
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint( def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]: self, waypoint: FlightWaypoint) -> Optional[timedelta]:
@ -335,8 +413,8 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan):
def patrol_start_time(self) -> timedelta: def patrol_start_time(self) -> timedelta:
start = self.package.escort_start_time start = self.package.escort_start_time
if start is not None: if start is not None:
return start return start + self.tot_offset
return super().patrol_start_time return super().patrol_start_time + self.tot_offset
@property @property
def patrol_end_time(self) -> timedelta: def patrol_end_time(self) -> timedelta:
@ -356,26 +434,30 @@ class StrikeFlightPlan(FormationFlightPlan):
egress: FlightWaypoint egress: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from [
return [
self.takeoff, self.takeoff,
self.hold, self.hold,
self.join, self.join,
self.ingress self.ingress
] + self.targets + [ ]
yield from self.targets
yield from [
self.egress, self.egress,
self.split, self.split,
self.land, self.land,
] ]
if self.divert is not None:
yield self.divert
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> Set[FlightWaypoint]:
return { return {
self.ingress, self.ingress,
self.egress, self.egress,
self.split, self.split,
} | set(self.targets) } | set(self.targets)
def speed_between_waypoints(self, a: FlightWaypoint, def speed_between_waypoints(self, a: FlightWaypoint,
@ -461,13 +543,72 @@ class StrikeFlightPlan(FormationFlightPlan):
return super().tot_for_waypoint(waypoint) return super().tot_for_waypoint(waypoint)
@dataclass(frozen=True)
class SweepFlightPlan(LoiterFlightPlan):
takeoff: FlightWaypoint
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield from [
self.takeoff,
self.hold,
self.sweep_start,
self.sweep_end,
self.land,
]
if self.divert is not None:
yield self.divert
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.sweep_end
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.sweep_start:
return self.sweep_start_time
if waypoint == self.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
return self.sweep_end_time - TravelTime.between_points(
self.hold.position,
self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
)
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@property def iter_waypoints(self) -> Iterator[FlightWaypoint]:
def waypoints(self) -> List[FlightWaypoint]: yield from self.custom_waypoints
return self.custom_waypoints
@property @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
@ -521,20 +662,18 @@ class FlightPlanBuilder:
raise RuntimeError("Flight must be a part of the package") raise RuntimeError("Flight must be a part of the package")
if self.package.waypoints is None: if self.package.waypoints is None:
self.regenerate_package_waypoints() self.regenerate_package_waypoints()
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
try:
flight_plan = self.generate_flight_plan(flight, custom_targets)
except PlanningError:
logging.exception(f"Could not create flight plan")
return
flight.flight_plan = flight_plan
def generate_flight_plan( def generate_flight_plan(
self, flight: Flight, self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> FlightPlan: custom_targets: Optional[List[Unit]]) -> FlightPlan:
# TODO: Flesh out mission types. # TODO: Flesh out mission types.
task = flight.flight_type task = flight.flight_type
if task == FlightType.BARCAP: if task == FlightType.ANTISHIP:
return self.generate_anti_ship(flight)
elif task == FlightType.BAI:
return self.generate_bai(flight)
elif task == FlightType.BARCAP:
return self.generate_barcap(flight) return self.generate_barcap(flight)
elif task == FlightType.CAS: elif task == FlightType.CAS:
return self.generate_cas(flight) return self.generate_cas(flight)
@ -542,18 +681,20 @@ class FlightPlanBuilder:
return self.generate_dead(flight, custom_targets) return self.generate_dead(flight, custom_targets)
elif task == FlightType.ESCORT: elif task == FlightType.ESCORT:
return self.generate_escort(flight) return self.generate_escort(flight)
elif task == FlightType.OCA_AIRCRAFT:
return self.generate_oca_strike(flight)
elif task == FlightType.OCA_RUNWAY:
return self.generate_runway_attack(flight)
elif task == FlightType.SEAD: elif task == FlightType.SEAD:
return self.generate_sead(flight, custom_targets) return self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:
return self.generate_strike(flight) return self.generate_strike(flight)
elif task == FlightType.SWEEP:
return self.generate_sweep(flight)
elif task == FlightType.TARCAP: elif task == FlightType.TARCAP:
return self.generate_frontline_cap(flight) return self.generate_tarcap(flight)
elif task == FlightType.TROOP_TRANSPORT:
logging.error(
"Troop transport flight plan generation not implemented"
)
raise PlanningError( raise PlanningError(
f"{task.name} flight plan generation not implemented") f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
ingress_point = self._ingress_point() ingress_point = self._ingress_point()
@ -603,7 +744,54 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(building.category, building)) targets.append(StrikeTarget(building.category, building))
return self.strike_flightplan(flight, location, targets) return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_STRIKE,
targets)
def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a BAI flight plan.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if isinstance(location, ControlPoint):
if location.is_fleet:
# The first group generated will be the carrier group itself.
location = location.ground_objects[0]
else:
raise InvalidObjectiveLocation(flight.flight_type, location)
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan: def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
"""Generate a BARCAP flight at a given location. """Generate a BARCAP flight at a given location.
@ -616,11 +804,56 @@ class FlightPlanBuilder:
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location)
patrol_alt = random.randint( patrol_alt = random.randint(
self.doctrine.min_patrol_altitude, self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude self.doctrine.max_patrol_altitude
) )
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
start, end = builder.race_track(start, end, patrol_alt)
return BarCapFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.departure),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
target = self.package.target.position
heading = self._heading_to_package_airfield(target)
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
return SweepFlightPlan(
package=self.package,
flight=flight,
lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
sweep_start=start,
sweep_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
)
def racetrack_for_objective(self,
location: MissionTarget) -> Tuple[Point, Point]:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.closest_airfields: for airfield in closest_cache.closest_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next* # If the mission is a BARCAP of an enemy airfield, find the *next*
@ -656,34 +889,11 @@ class FlightPlanBuilder:
self.doctrine.cap_max_track_length self.doctrine.cap_max_track_length
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
return start, end
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) def racetrack_for_frontline(self,
start, end = builder.race_track(start, end, patrol_alt) front_line: FrontLine) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points
return BarCapFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.from_cp)
)
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ally_cp, enemy_cp = location.control_points
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude)
# Find targets waypoints # Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
@ -700,26 +910,46 @@ class FlightPlanBuilder:
if combat_width < 35000: if combat_width < 35000:
combat_width = 35000 combat_width = 35000
radius = combat_width*1.25 radius = combat_width * 1.25
orbit0p = orbit_center.point_from_heading(heading, radius) orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
return orbit0p, orbit1p
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude)
# Create points # Create points
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return FrontLineCapFlightPlan( if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(location)
else:
orbit0p, orbit1p = self.racetrack_for_objective(location)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
return TarCapFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
lead_time=timedelta(minutes=2),
# Note that this duration only has an effect if there are no # Note that this duration only has an effect if there are no
# flights in the package that have requested escort. If the package # flights in the package that have requested escort. If the package
# requests an escort the CAP flight will remain on station for the # requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo. # duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
patrol_start=start, patrol_start=start,
patrol_end=end, patrol_end=end,
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
def generate_dead(self, flight: Flight, def generate_dead(self, flight: Flight,
@ -732,8 +962,11 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
if not isinstance(location, TheaterGroundObject): is_ewr = isinstance(location, EwrGroundObject)
logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}") 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=}")
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these. # TODO: Unify these.
@ -745,7 +978,42 @@ class FlightPlanBuilder:
for target in custom_targets: for target in custom_targets:
targets.append(StrikeTarget(location.name, target)) targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, 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.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for OCA Strike flight "
f"{flight=} at {location=}.")
raise InvalidObjectiveLocation(flight.flight_type, location)
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.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for runway bombing flight "
f"{flight=} at {location=}.")
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_OCA_RUNWAY)
def generate_sead(self, flight: Flight, def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan: custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
@ -757,9 +1025,6 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these. # TODO: Unify these.
# There doesn't seem to be any reason to treat the UI fragged missions # There doesn't seem to be any reason to treat the UI fragged missions
# different from the automatic missions. # different from the automatic missions.
@ -769,7 +1034,8 @@ class FlightPlanBuilder:
for target in custom_targets: for target in custom_targets:
targets.append(StrikeTarget(location.name, target)) targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, targets) return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_SEAD, targets)
def generate_escort(self, flight: Flight) -> StrikeFlightPlan: def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None assert self.package.waypoints is not None
@ -782,14 +1048,15 @@ class FlightPlanBuilder:
return StrikeFlightPlan( return StrikeFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=[target], targets=[target],
egress=egress, egress=egress,
split=builder.split(self.package.waypoints.split), split=builder.split(self.package.waypoints.split),
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
def generate_cas(self, flight: Flight) -> CasFlightPlan: def generate_cas(self, flight: Flight) -> CasFlightPlan:
@ -816,17 +1083,21 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
patrol_duration=self.doctrine.cas_duration, patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
patrol_start=builder.ingress_cas(ingress, location), patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
ingress, location),
target=builder.cas(center), target=builder.cas(center),
patrol_end=builder.egress(egress, location), patrol_end=builder.egress(egress, location),
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
@staticmethod @staticmethod
def target_waypoint(flight: Flight, builder: WaypointBuilder, def target_waypoint(flight: Flight, builder: WaypointBuilder,
target: StrikeTarget) -> FlightWaypoint: target: StrikeTarget) -> FlightWaypoint:
if flight.flight_type == FlightType.DEAD: if flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
return builder.bai_group(target)
elif flight.flight_type == FlightType.DEAD:
return builder.dead_point(target) return builder.dead_point(target)
elif flight.flight_type == FlightType.SEAD: elif flight.flight_type == FlightType.SEAD:
return builder.sead_point(target) return builder.sead_point(target)
@ -840,12 +1111,14 @@ class FlightPlanBuilder:
return builder.dead_area(location) return builder.dead_area(location)
elif flight.flight_type == FlightType.SEAD: elif flight.flight_type == FlightType.SEAD:
return builder.sead_area(location) return builder.sead_area(location)
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
return builder.oca_strike_area(location)
else: else:
return builder.strike_area(location) return builder.strike_area(location)
def _hold_point(self, flight: Flight) -> Point: def _hold_point(self, flight: Flight) -> Point:
assert self.package.waypoints is not None assert self.package.waypoints is not None
origin = flight.from_cp.position origin = flight.departure.position
target = self.package.target.position target = self.package.target.position
join = self.package.waypoints.join join = self.package.waypoints.join
origin_to_target = origin.distance_to_point(target) origin_to_target = origin.distance_to_point(target)
@ -902,22 +1175,12 @@ class FlightPlanBuilder:
return builder.land(arrival) return builder.land(arrival)
def strike_flightplan( def strike_flightplan(
self, flight: Flight, location: TheaterGroundObject, self, flight: Flight, location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan: targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine, builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
targets) targets)
# sead_types = {FlightType.DEAD, FlightType.SEAD}
if flight.flight_type is FlightType.SEAD:
ingress = builder.ingress_sead(self.package.waypoints.ingress,
location)
elif flight.flight_type is FlightType.DEAD:
ingress = builder.ingress_dead(self.package.waypoints.ingress,
location)
else:
ingress = builder.ingress_strike(self.package.waypoints.ingress,
location)
target_waypoints: List[FlightWaypoint] = [] target_waypoints: List[FlightWaypoint] = []
if targets is not None: if targets is not None:
@ -931,14 +1194,16 @@ class FlightPlanBuilder:
return StrikeFlightPlan( return StrikeFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location),
targets=target_waypoints, targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location), egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split), split=builder.split(self.package.waypoints.split),
land=builder.land(flight.from_cp) land=builder.land(flight.arrival),
divert=builder.divert(flight.divert)
) )
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
@ -951,8 +1216,8 @@ class FlightPlanBuilder:
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target.""" """Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition) heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading(heading, return attack_transition.point_from_heading(
-self.doctrine.join_distance) heading, -self.doctrine.join_distance)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point( transition_target_distance = attack_transition.distance_to_point(
@ -1014,7 +1279,7 @@ class FlightPlanBuilder:
) )
for airfield in cache.closest_airfields: for airfield in cache.closest_airfields:
for flight in self.package.flights: for flight in self.package.flights:
if flight.from_cp == airfield: if flight.departure == airfield:
return airfield return airfield
raise RuntimeError( raise RuntimeError(
"Could not find any airfield assigned to this package" "Could not find any airfield assigned to this package"

View File

@ -45,20 +45,21 @@ class GroundSpeed:
return int(cls.from_mach(mach, altitude)) # knots return int(cls.from_mach(mach, altitude)) # knots
@staticmethod @staticmethod
def from_mach(mach: float, altitude: int) -> float: def from_mach(mach: float, altitude_m: int) -> float:
"""Returns the ground speed in knots for the given mach and altitude. """Returns the ground speed in knots for the given mach and altitude.
Args: Args:
mach: The mach number to convert to ground speed. mach: The mach number to convert to ground speed.
altitude: The altitude in feet. altitude_m: The altitude in meters.
Returns: Returns:
The ground speed corresponding to the given altitude and mach number The ground speed corresponding to the given altitude and mach number
in knots. in knots.
""" """
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html # https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
if altitude <= 36152: altitude_ft = altitude_m * 3.28084
temperature_f = 59 - 0.00356 * altitude if altitude_ft <= 36152:
temperature_f = 59 - 0.00356 * altitude_ft
else: else:
# There's another formula for altitudes over 82k feet, but we better # There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high... # not be planning waypoints that high...
@ -86,6 +87,7 @@ class TravelTime:
return timedelta(hours=distance / speed * error_factor) return timedelta(hours=distance / speed * error_factor)
# TODO: Most if not all of this should move into FlightPlan.
class TotEstimator: class TotEstimator:
# An extra five minutes given as wiggle room. Expected to be spent at the # An extra five minutes given as wiggle room. Expected to be spent at the
# hold point performing any last minute configuration. # hold point performing any last minute configuration.
@ -135,7 +137,14 @@ class TotEstimator:
f"time for {flight} will be immediate.") f"time for {flight} will be immediate.")
return None return None
else: else:
tot = self.package.time_over_target tot_waypoint = flight.flight_plan.tot_waypoint
if tot_waypoint is None:
tot = self.package.time_over_target
else:
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
if tot is None:
logging.error(f"TOT waypoint for {flight} has no TOT")
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME return tot - travel_time - self.HOLD_TIME
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
@ -172,9 +181,13 @@ class TotEstimator:
# Return 0 so this flight's travel time does not affect the rest # Return 0 so this flight's travel time does not affect the rest
# of the package. # of the package.
return timedelta() return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes
# means the flight's TOT is 2 minutes ahead of the package's so it needs
# an extra two minutes.
offset = -flight.flight_plan.tot_offset
startup = self.estimate_startup(flight) startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight) ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target return startup + ground_ops + time_to_target + offset
@staticmethod @staticmethod
def estimate_startup(flight: Flight) -> timedelta: def estimate_startup(flight: Flight) -> timedelta:

View File

@ -5,17 +5,23 @@ from typing import List, Optional, Tuple, Union
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.theater import (
ControlPoint,
MissionTarget,
OffMapSpawn,
TheaterGroundObject,
)
from game.weather import Conditions from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[TheaterGroundObject, Unit] target: Union[VehicleGroup, TheaterGroundObject, Unit]
class WaypointBuilder: class WaypointBuilder:
@ -31,8 +37,7 @@ class WaypointBuilder:
def is_helo(self) -> bool: def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False) return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
def takeoff(departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier. """Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs Note that the takeoff waypoint will automatically be created by pydcs
@ -43,36 +48,93 @@ class WaypointBuilder:
departure: Departure airfield or carrier. departure: Departure airfield or carrier.
""" """
position = departure.position position = departure.position
waypoint = FlightWaypoint( if isinstance(departure, OffMapSpawn):
FlightWaypointType.TAKEOFF, waypoint = FlightWaypoint(
position.x, FlightWaypointType.NAV,
position.y, position.x,
0 position.y,
) 500 if self.is_helo else self.doctrine.rendezvous_altitude
waypoint.name = "TAKEOFF" )
waypoint.alt_type = "RADIO" waypoint.name = "NAV"
waypoint.description = "Takeoff" waypoint.alt_type = "BARO"
waypoint.pretty_name = "Takeoff" waypoint.description = "Enter theater"
waypoint.pretty_name = "Enter theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
return waypoint return waypoint
@staticmethod def land(self, arrival: ControlPoint) -> FlightWaypoint:
def land(arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier. """Create descent waypoint for the given arrival airfield or carrier.
Args: Args:
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
position = arrival.position position = arrival.position
if isinstance(arrival, OffMapSpawn):
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.name = "NAV"
waypoint.alt_type = "BARO"
waypoint.description = "Exit theater"
waypoint.pretty_name = "Exit theater"
else:
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
def divert(self,
divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
"""Create divert waypoint for the given arrival airfield or carrier.
Args:
divert: Divert airfield or carrier.
"""
if divert is None:
return None
position = divert.position
if isinstance(divert, OffMapSpawn):
if self.is_helo:
altitude = 500
else:
altitude = self.doctrine.rendezvous_altitude
altitude_type = "BARO"
else:
altitude = 0
altitude_type = "RADIO"
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, FlightWaypointType.DIVERT,
position.x, position.x,
position.y, position.y,
0 altitude
) )
waypoint.name = "LANDING" waypoint.alt_type = altitude_type
waypoint.alt_type = "RADIO" waypoint.name = "DIVERT"
waypoint.description = "Land" waypoint.description = "Divert"
waypoint.pretty_name = "Land" waypoint.pretty_name = "Divert"
waypoint.only_for_player = True
return waypoint return waypoint
def hold(self, position: Point) -> FlightWaypoint: def hold(self, position: Point) -> FlightWaypoint:
@ -111,33 +173,8 @@ class WaypointBuilder:
waypoint.name = "SPLIT" waypoint.name = "SPLIT"
return waypoint return waypoint
def ingress_cas(self, position: Point, def ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint: objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
objective)
def ingress_escort(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
objective)
def ingress_dead(self, position:Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
objective)
def ingress_sead(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
objective)
def ingress_strike(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
ingress_type, ingress_type,
position.x, position.x,
@ -163,6 +200,9 @@ class WaypointBuilder:
waypoint.name = "EGRESS" waypoint.name = "EGRESS"
return waypoint return waypoint
def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"ATTACK {target.name}")
def dead_point(self, target: StrikeTarget) -> FlightWaypoint: def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}") return self._target_point(target, f"STRIKE {target.name}")
@ -183,6 +223,7 @@ class WaypointBuilder:
waypoint.description = description waypoint.description = description
waypoint.pretty_name = description waypoint.pretty_name = description
waypoint.name = target.name waypoint.name = target.name
waypoint.alt_type = "RADIO"
# The target waypoints are only for the player's benefit. AI tasks for # The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack # the target are set on the ingress point so they begin their attack
# *before* reaching the target. # *before* reaching the target.
@ -198,8 +239,12 @@ class WaypointBuilder:
def dead_area(self, target: MissionTarget) -> FlightWaypoint: def dead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"DEAD on {target.name}", target) return self._target_area(f"DEAD on {target.name}", target)
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@staticmethod @staticmethod
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint: def _target_area(name: str, location: MissionTarget,
flyover: bool = False) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
location.position.x, location.position.x,
@ -209,10 +254,19 @@ class WaypointBuilder:
waypoint.description = name waypoint.description = name
waypoint.pretty_name = name waypoint.pretty_name = name
waypoint.name = name waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for waypoint.alt_type = "RADIO"
# Most target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack # the target are set on the ingress point so they begin their attack
# *before* reaching the target. # *before* reaching the target.
waypoint.only_for_player = True #
# The exception is for flight plans that require passing over the
# target. For example, OCA strikes need to get close enough to detect
# the targets in their engagement zone or they will RTB immediately.
if flyover:
waypoint.flyover = True
else:
waypoint.only_for_player = True
return waypoint return waypoint
def cas(self, position: Point) -> FlightWaypoint: def cas(self, position: Point) -> FlightWaypoint:
@ -278,6 +332,56 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude), return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude)) self.race_track_end(end, altitude))
@staticmethod
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep start waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP,
position.x,
position.y,
altitude
)
waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft"
waypoint.pretty_name = "Sweep start"
return waypoint
@staticmethod
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep end waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
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: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning of the sweep.
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude),
self.sweep_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \ def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]: Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package. """Creates the waypoints needed to escort the package.
@ -293,8 +397,8 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give # description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target # the escort flights a flight plan including the ingress point, target
# area, and egress point. # area, and egress point.
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target) target)
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,

View File

@ -1,55 +1,44 @@
import logging from __future__ import annotations
import typing
from enum import IntEnum from typing import TYPE_CHECKING
from dcs.mission import Mission
from dcs.forcedoptions import ForcedOptions from dcs.forcedoptions import ForcedOptions
from dcs.mission import Mission
from .conflictgen import * if TYPE_CHECKING:
from game.game import Game
class Labels(IntEnum):
Off = 0
Full = 1
Abbreviated = 2
Dot = 3
class ForcedOptionsGenerator: class ForcedOptionsGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game): def __init__(self, mission: Mission, game: Game) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict
self.game = game self.game = game
def _set_options_view(self): def _set_options_view(self) -> None:
self.mission.forced_options.options_view = self.game.settings.map_coalition_visibility
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All: def _set_external_views(self) -> None:
self.mission.forced_options.options_view = ForcedOptions.Views.All
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.Allies:
self.mission.forced_options.options_view = ForcedOptions.Views.Allies
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyAllies:
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyAllies
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.MyAircraft:
self.mission.forced_options.options_view = ForcedOptions.Views.MyAircraft
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyMap
def _set_external_views(self):
if not self.game.settings.external_views_allowed: if not self.game.settings.external_views_allowed:
self.mission.forced_options.external_views = self.game.settings.external_views_allowed self.mission.forced_options.external_views = self.game.settings.external_views_allowed
def _set_labels(self): def _set_labels(self) -> None:
# TODO: Fix settings to use the real type.
# TODO: Allow forcing "full" and have default do nothing.
if self.game.settings.labels == "Abbreviated": if self.game.settings.labels == "Abbreviated":
self.mission.forced_options.labels = int(Labels.Abbreviated) self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
elif self.game.settings.labels == "Dot Only": elif self.game.settings.labels == "Dot Only":
self.mission.forced_options.labels = int(Labels.Dot) self.mission.forced_options.labels = ForcedOptions.Labels.DotOnly
elif self.game.settings.labels == "Off": elif self.game.settings.labels == "Off":
self.mission.forced_options.labels = int(Labels.Off) self.mission.forced_options.labels = ForcedOptions.Labels.None_
def _set_unrestricted_satnav(self) -> None:
blue = self.game.player_faction
red = self.game.enemy_faction
if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True
def generate(self): def generate(self):
self._set_options_view() self._set_options_view()
self._set_external_views() self._set_external_views()
self._set_labels() self._set_labels()
self._set_unrestricted_satnav()

View File

@ -2,12 +2,12 @@ import random
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
import pydcs_extensions.frenchpack.frenchpack as frenchpack import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
TYPE_TANKS = [ TYPE_TANKS = [
Armor.MBT_T_55, Armor.MBT_T_55,

View File

@ -9,7 +9,7 @@ from __future__ import annotations
import logging import logging
import random import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type
from dcs import Mission from dcs import Mission
from dcs.country import Country from dcs.country import Country
@ -20,20 +20,21 @@ from dcs.task import (
EPLRS, EPLRS,
OptAlarmState, OptAlarmState,
) )
from dcs.unit import Ship, Vehicle, Unit from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import Group, ShipGroup, StaticGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType from dcs.unittype import StaticType, UnitType
from game import db from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name from game.db import unit_type_from_name
from theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject, BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject, LhaGroundObject, ShipGroundObject,
) )
from .conflictgen import Conflict from game.unitmap import UnitMap
from game.utils import knots_to_kph, kph_to_mps, mps_to_kph
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
@ -52,11 +53,12 @@ class GenericGroundObjectGenerator:
Currently used only for SAM and missile (V1/V2) sites. Currently used only for SAM and missile (V1/V2) sites.
""" """
def __init__(self, ground_object: TheaterGroundObject, country: Country, def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission) -> None: game: Game, mission: Mission, unit_map: UnitMap) -> None:
self.ground_object = ground_object self.ground_object = ground_object
self.country = country self.country = country
self.game = game self.game = game
self.m = mission self.m = mission
self.unit_map = unit_map
def generate(self) -> None: def generate(self) -> None:
if self.game.position_culled(self.ground_object.position): if self.game.position_culled(self.ground_object.position):
@ -89,9 +91,10 @@ class GenericGroundObjectGenerator:
self.enable_eplrs(vg, unit_type) self.enable_eplrs(vg, unit_type)
self.set_alarm_state(vg) self.set_alarm_state(vg)
self._register_unit_group(group, vg)
@staticmethod @staticmethod
def enable_eplrs(group: Group, unit_type: UnitType) -> None: def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, 'eplrs'): if hasattr(unit_type, 'eplrs'):
if unit_type.eplrs: if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
@ -102,6 +105,11 @@ class GenericGroundObjectGenerator:
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group,
miz_group: Group) -> None:
self.unit_map.add_ground_object_units(self.ground_object,
persistence_group, miz_group)
class BuildingSiteGenerator(GenericGroundObjectGenerator): class BuildingSiteGenerator(GenericGroundObjectGenerator):
"""Generator for building sites. """Generator for building sites.
@ -133,16 +141,17 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
def generate_vehicle_group(self, unit_type: UnitType) -> None: def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
self.m.vehicle_group( group = self.m.vehicle_group(
country=self.country, country=self.country,
name=self.ground_object.group_name, name=self.ground_object.group_name,
_type=unit_type, _type=unit_type,
position=self.ground_object.position, position=self.ground_object.position,
heading=self.ground_object.heading, heading=self.ground_object.heading,
) )
self._register_fortification(group)
def generate_static(self, static_type: StaticType) -> None: def generate_static(self, static_type: StaticType) -> None:
self.m.static_group( group = self.m.static_group(
country=self.country, country=self.country,
name=self.ground_object.group_name, name=self.ground_object.group_name,
_type=static_type, _type=static_type,
@ -150,6 +159,15 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
heading=self.ground_object.heading, heading=self.ground_object.heading,
dead=self.ground_object.is_dead, dead=self.ground_object.is_dead,
) )
self._register_building(group)
def _register_fortification(self, fortification: VehicleGroup) -> None:
assert isinstance(self.ground_object, BuildingGroundObject)
self.unit_map.add_fortification(self.ground_object, fortification)
def _register_building(self, building: StaticGroup) -> None:
assert isinstance(self.ground_object, BuildingGroundObject)
self.unit_map.add_building(self.ground_object, building)
class GenericCarrierGenerator(GenericGroundObjectGenerator): class GenericCarrierGenerator(GenericGroundObjectGenerator):
@ -161,8 +179,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
control_point: ControlPoint, country: Country, game: Game, control_point: ControlPoint, country: Country, game: Game,
mission: Mission, radio_registry: RadioRegistry, mission: Mission, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int], tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
runways: Dict[str, RunwayData]) -> None: runways: Dict[str, RunwayData], unit_map: UnitMap) -> None:
super().__init__(ground_object, country, game, mission) super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object self.ground_object = ground_object
self.control_point = control_point self.control_point = control_point
self.radio_registry = radio_registry self.radio_registry = radio_registry
@ -187,11 +205,16 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
tacan_callsign = self.tacan_callsign() tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc) icls = next(self.icls_alloc)
# Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can
# count those as the time the carrier uses to move and the mission
# time as the recovery window.
brc = self.steam_into_wind(ship_group) brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls) self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls) self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: Group) -> UnitType: def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type) unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None: if unit_type is None:
raise RuntimeError( raise RuntimeError(
@ -221,12 +244,16 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
return ship return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[int]: def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
brc = self.m.weather.wind_at_ground.direction + 180 wind = self.game.conditions.weather.wind.at_0m
brc = wind.direction + 180
# Aim for 25kts over the deck.
carrier_speed = knots_to_kph(25) - mps_to_kph(wind.speed)
for attempt in range(5): for attempt in range(5):
point = group.points[0].position.point_from_heading( point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000) brc, 100000 - attempt * 20000)
if self.game.theater.is_in_sea(point): if self.game.theater.is_in_sea(point):
group.add_waypoint(point) group.points[0].speed = kph_to_mps(carrier_speed)
group.add_waypoint(point, carrier_speed)
return brc return brc
return None return None
@ -328,8 +355,9 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
self.generate_group(group, unit_type) self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, unit_type: UnitType): def generate_group(self, group_def: Group,
group = self.m.ship_group(self.country, group_def.name, unit_type, first_unit_type: Type[UnitType]) -> None:
group = self.m.ship_group(self.country, group_def.name, first_unit_type,
position=group_def.position, position=group_def.position,
heading=group_def.units[0].heading) heading=group_def.units[0].heading)
group.units[0].name = self.m.string(group_def.units[0].name) group.units[0].name = self.m.string(group_def.units[0].name)
@ -343,6 +371,7 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
ship.heading = unit.heading ship.heading = unit.heading
group.add_unit(ship) group.add_unit(ship)
self.set_alarm_state(group) self.set_alarm_state(group)
self._register_unit_group(group_def, group)
class GroundObjectsGenerator: class GroundObjectsGenerator:
@ -353,40 +382,18 @@ class GroundObjectsGenerator:
locations for spawning ground objects, determining their types, and creating locations for spawning ground objects, determining their types, and creating
the appropriate generators. the appropriate generators.
""" """
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game, def __init__(self, mission: Mission, game: Game,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry): radio_registry: RadioRegistry, tacan_registry: TacanRegistry,
unit_map: UnitMap) -> None:
self.m = mission self.m = mission
self.conflict = conflict
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.unit_map = unit_map
self.icls_alloc = iter(range(1, 21)) self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {} self.runways: Dict[str, RunwayData] = {}
def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
if self.conflict.is_vector:
center = self.conflict.center
heading = self.conflict.heading - 90
else:
center, heading = self.conflict.frontline_position(self.conflict.theater, self.conflict.from_cp, self.conflict.to_cp)
heading -= 90
initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE)
position = self.conflict.find_ground_position(initial_position, heading)
if not position:
position = initial_position
for i, _ in enumerate(range(0, number_of_units, self.FARP_CAPACITY)):
position = position.point_from_heading(0, i * 275)
yield self.m.farp(
country=self.m.country(self.game.player_country),
name="FARP",
position=position,
)
def generate(self): def generate(self):
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.captured: if cp.captured:
@ -397,25 +404,26 @@ class GroundObjectsGenerator:
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject): if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(ground_object, country, generator = BuildingSiteGenerator(
self.game, self.m) ground_object, country, self.game, self.m,
self.unit_map)
elif isinstance(ground_object, CarrierGroundObject): elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(ground_object, cp, country, generator = CarrierGenerator(
self.game, self.m, ground_object, cp, country, self.game, self.m,
self.radio_registry, self.radio_registry, self.tacan_registry,
self.tacan_registry, self.icls_alloc, self.runways, self.unit_map)
self.icls_alloc, self.runways)
elif isinstance(ground_object, LhaGroundObject): elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(ground_object, cp, country, generator = CarrierGenerator(
self.game, self.m, ground_object, cp, country, self.game, self.m,
self.radio_registry, self.radio_registry, self.tacan_registry,
self.tacan_registry, self.icls_alloc, self.runways, self.unit_map)
self.icls_alloc, self.runways)
elif isinstance(ground_object, ShipGroundObject): elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(ground_object, country, generator = ShipObjectGenerator(
self.game, self.m) ground_object, country, self.game, self.m,
self.unit_map)
else: else:
generator = GenericGroundObjectGenerator(ground_object,
country, self.game, generator = GenericGroundObjectGenerator(
self.m) ground_object, country, self.game, self.m,
self.unit_map)
generator.generate() generator.generate()

View File

@ -26,7 +26,7 @@ import datetime
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING, Tuple
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission from dcs.mission import Mission
@ -44,6 +44,8 @@ from .runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
class KneeboardPageWriter: class KneeboardPageWriter:
"""Creates kneeboard images.""" """Creates kneeboard images."""
@ -191,7 +193,15 @@ class FlightPlanBuilder:
waypoint.position waypoint.position
)) ))
duration = (waypoint.tot - last_time).total_seconds() / 3600 duration = (waypoint.tot - last_time).total_seconds() / 3600
return f"{int(distance / duration)} kt" try:
return f"{int(distance / duration)} kt"
except ZeroDivisionError:
# TODO: Improve resolution of unit conversions.
# When waypoints are very close to each other they can end up with
# identical TOTs because our unit conversion functions truncate to
# int. When waypoints have the same TOT the duration will be zero.
# https://github.com/Khopa/dcs_liberation/issues/557
return "-"
def build(self) -> List[List[str]]: def build(self) -> List[List[str]]:
return self.rows return self.rows
@ -230,28 +240,37 @@ class BriefingPage(KneeboardPage):
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure" "#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
]) ])
writer.heading("Comm Ladder") flight_plan_builder
comms = [] 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: for comm in self.comms:
comms.append([comm.name, self.format_frequency(comm.freq)]) comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)])
writer.table(comms, headers=["Name", "UHF"])
writer.heading("AWACS")
awacs = []
for a in self.awacs: for a in self.awacs:
awacs.append([a.callsign, self.format_frequency(a.freq)]) comm_ladder.append([
writer.table(awacs, headers=["Callsign", "UHF"]) a.callsign,
'AWACS',
writer.heading("Tankers") '',
tankers = [] '',
self.format_frequency(a.freq)
])
for tanker in self.tankers: for tanker in self.tankers:
tankers.append([ comm_ladder.append([
tanker.callsign, tanker.callsign,
"Tanker",
tanker.variant, tanker.variant,
str(tanker.tacan), str(tanker.tacan),
self.format_frequency(tanker.freq), self.format_frequency(tanker.freq),
]) ])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"])
writer.heading("JTAC") writer.heading("JTAC")
jtacs = [] jtacs = []

View File

@ -8,7 +8,7 @@ from gen.locations.preset_control_point_locations import PresetControlPointLocat
from gen.locations.preset_locations import PresetLocation from gen.locations.preset_locations import PresetLocation
class PresetLocationFinder: class MizDataLocationFinder:
@staticmethod @staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations: def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:

View File

@ -134,7 +134,7 @@ RADIOS: List[Radio] = [
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis # MiG-21bis
Radio("RSIU-5V", MHz(100), MHz(150), step=MHz(1)), Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50 # Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps. # Note: Also capable of 100MHz-150MHz, but we can't model gaps.

View File

@ -8,7 +8,6 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport
from game.weather import Conditions from game.weather import Conditions
from theater import ControlPoint, ControlPointType
from .airfields import AIRFIELD_DATA from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency from .radios import RadioFrequency
from .tacan import TacanChannel from .tacan import TacanChannel
@ -117,23 +116,3 @@ class RunwayAssigner:
# Otherwise the only difference between the two is the distance from # Otherwise the only difference between the two is the distance from
# parking, which we don't know, so just pick the first one. # parking, which we don't know, so just pick the first one.
return best_runways[0] return best_runways[0]
def takeoff_heading(self, departure: ControlPoint) -> int:
if departure.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(departure.airport).runway_heading
elif departure.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {departure.cptype}")
return 0
def landing_heading(self, arrival: ControlPoint) -> int:
if arrival.cptype == ControlPointType.AIRBASE:
return self.get_preferred_runway(arrival.airport).runway_heading
elif arrival.is_fleet:
# The carrier will be angled into the wind automatically.
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
logging.warning(
f"Unhandled departure control point: {arrival.cptype}")
return 0

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class BoforsGenerator(GroupGenerator): class BoforsGenerator(AirDefenseGroupGenerator):
""" """
This generate a Bofors flak artillery group This generate a Bofors flak artillery group
""" """
@ -26,3 +29,7 @@ class BoforsGenerator(GroupGenerator):
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index), self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index),
self.position.x + spacing*i, self.position.x + spacing*i,
self.position.y + spacing*j, self.heading) self.position.y + spacing*j, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,11 +2,22 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
GFLAK = [AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_Flak_38] GFLAK = [
AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Flak_38,
]
class FlakGenerator(GroupGenerator):
class FlakGenerator(AirDefenseGroupGenerator):
""" """
This generate a German flak artillery group This generate a German flak artillery group
""" """
@ -18,7 +29,7 @@ class FlakGenerator(GroupGenerator):
grid_x = random.randint(2, 3) grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3) grid_y = random.randint(2, 3)
spacing = random.randint(30, 60) spacing = random.randint(20, 35)
index = 0 index = 0
mixed = random.choice([True, False]) mixed = random.choice([True, False])
@ -35,7 +46,7 @@ class FlakGenerator(GroupGenerator):
unit_type = random.choice(GFLAK) unit_type = random.choice(GFLAK)
# Search lights # Search lights
search_pos = self.get_circular_position(random.randint(2,3), 90) search_pos = self.get_circular_position(random.randint(2,3), 80)
for index, pos in enumerate(search_pos): for index, pos in enumerate(search_pos):
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading) self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading)
@ -51,6 +62,10 @@ class FlakGenerator(GroupGenerator):
# Some Opel Blitz trucks # Some Opel Blitz trucks
for i in range(int(max(1,grid_x/2))): for i in range(int(max(1,grid_x/2))):
for j in range(int(max(1,grid_x/2))): for j in range(int(max(1,grid_x/2))):
self.add_unit(Unarmed.Blitz_3_6_6700A, "AAA#" + str(index), self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index),
self.position.x + 200 + 15*i + random.randint(1,5), self.position.x + 125 + 15*i + random.randint(1,5),
self.position.y + 15*j + random.randint(1,5), 90) self.position.y + 15*j + random.randint(1,5), 75)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class Flak18Generator(GroupGenerator): class Flak18Generator(AirDefenseGroupGenerator):
""" """
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
""" """
@ -27,3 +30,7 @@ class Flak18Generator(GroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading) self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,11 +1,14 @@
import random import random
from dcs.vehicles import AirDefence, Unarmed, Armor from dcs.vehicles import AirDefence, Armor, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class AllyWW2FlakGenerator(GroupGenerator): class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
""" """
This generate an ally flak artillery group This generate an ally flak artillery group
""" """
@ -15,15 +18,15 @@ class AllyWW2FlakGenerator(GroupGenerator):
def generate(self): def generate(self):
positions = self.get_circular_position(4, launcher_distance=50, coverage=360) positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
positions = self.get_circular_position(8, launcher_distance=100, coverage=360) positions = self.get_circular_position(8, launcher_distance=60, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2]) self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
positions = self.get_circular_position(8, launcher_distance=150, coverage=360) positions = self.get_circular_position(8, launcher_distance=90, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2]) self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
@ -32,3 +35,7 @@ class AllyWW2FlakGenerator(GroupGenerator):
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#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(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.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZU23InsurgentGenerator(GroupGenerator): class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
""" """
This generate a ZU23 insurgent flak artillery group This generate a ZU23 insurgent flak artillery group
""" """
@ -26,3 +29,7 @@ class ZU23InsurgentGenerator(GroupGenerator):
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index), self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index),
self.position.x + spacing*i, self.position.x + spacing*i,
self.position.y + spacing*j, self.heading) self.position.y + spacing*j, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -0,0 +1,27 @@
from abc import ABC, abstractmethod
from enum import Enum
from game import Game
from gen.sam.group_generator import GroupGenerator
from game.theater.theatergroundobject import SamGroundObject
class AirDefenseRange(Enum):
Short = "short"
Medium = "medium"
Long = "long"
class AirDefenseGroupGenerator(GroupGenerator, ABC):
"""
This is the base for all SAM group generators
"""
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True
super().__init__(game, ground_object)
@classmethod
@abstractmethod
def range(cls) -> AirDefenseRange:
...

View File

@ -2,10 +2,14 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
from gen.sam.group_generator import GroupGenerator from gen.sam.group_generator import GroupGenerator
class EarlyColdWarFlakGenerator(GroupGenerator): class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
""" """
This generator attempt to mimic an early cold-war era flak AAA site. This generator attempt to mimic an early cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection. The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection.
@ -32,14 +36,18 @@ class EarlyColdWarFlakGenerator(GroupGenerator):
# Short range guns # Short range guns
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1", self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
self.position.x - 40, self.position.y - 40, self.heading + 180), self.position.x - 40, self.position.y - 40, self.heading + 180),
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1", self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
# Add a truck # Add a truck
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading) self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short
class ColdWarFlakGenerator(GroupGenerator):
class ColdWarFlakGenerator(AirDefenseGroupGenerator):
""" """
This generator attempt to mimic a cold-war era flak AAA site. This generator attempt to mimic a cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection. The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection.
@ -65,8 +73,12 @@ class ColdWarFlakGenerator(GroupGenerator):
# Short range guns # Short range guns
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1", self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
self.position.x - 40, self.position.y - 40, self.heading + 180), self.position.x - 40, self.position.y - 40, self.heading + 180),
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1", self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading), self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
# Add a P19 Radar for EWR # Add a P19 Radar for EWR
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading) self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,11 +1,12 @@
import random from dcs.vehicles import AirDefence, Infantry, Unarmed
from dcs.vehicles import AirDefence, Unarmed, Infantry from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
from gen.sam.group_generator import GroupGenerator AirDefenseGroupGenerator,
)
class FreyaGenerator(GroupGenerator): class FreyaGenerator(AirDefenseGroupGenerator):
""" """
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
""" """
@ -37,3 +38,7 @@ class FreyaGenerator(GroupGenerator):
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#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#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(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,15 +0,0 @@
from abc import ABC
from game import Game
from gen.sam.group_generator import GroupGenerator
from theater.theatergroundobject import SamGroundObject
class GenericSamGroupGenerator(GroupGenerator, ABC):
"""
This is the base for all SAM group generators
"""
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True
super().__init__(game, ground_object)

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
import random import random
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Type
from dcs import unitgroup from dcs import unitgroup
from dcs.point import PointAction from dcs.point import PointAction
@ -9,7 +9,7 @@ from dcs.unit import Vehicle, Ship
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game.factions.faction import Faction from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -38,7 +38,7 @@ class GroupGenerator:
def get_generated_group(self) -> unitgroup.VehicleGroup: def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg return self.vg
def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float,
pos_y: float, heading: int) -> Vehicle: pos_y: float, heading: int) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), unit = Vehicle(self.game.next_unit_id(),
f"{self.go.group_name}|{name}", unit_type.id) f"{self.go.group_name}|{name}", unit_type.id)

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class AvengerGenerator(GroupGenerator): class AvengerGenerator(AirDefenseGroupGenerator):
""" """
This generate an Avenger group This generate an Avenger group
""" """
@ -20,3 +23,7 @@ class AvengerGenerator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ChaparralGenerator(GroupGenerator): class ChaparralGenerator(AirDefenseGroupGenerator):
""" """
This generate a Chaparral group This generate a Chaparral group
""" """
@ -20,3 +23,7 @@ class ChaparralGenerator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class GepardGenerator(GroupGenerator): class GepardGenerator(AirDefenseGroupGenerator):
""" """
This generate a Gepard group This generate a Gepard group
""" """
@ -19,3 +22,6 @@ class GepardGenerator(GroupGenerator):
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA2", self.position.x, 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) self.add_unit(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,18 +1,26 @@
import random import random
from typing import List, Optional, Type from typing import Dict, Iterable, List, Optional, Sequence, Set, Type
from dcs.vehicles import AirDefence
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from dcs.vehicles import AirDefence
from game import Game, db from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import SamGroundObject
from gen.sam.aaa_bofors import BoforsGenerator from gen.sam.aaa_bofors import BoforsGenerator
from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak import FlakGenerator
from gen.sam.aaa_flak18 import Flak18Generator from gen.sam.aaa_flak18 import Flak18Generator
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseGroupGenerator,
AirDefenseRange,
)
from gen.sam.cold_war_flak import (
ColdWarFlakGenerator,
EarlyColdWarFlakGenerator,
)
from gen.sam.ewrs import ( from gen.sam.ewrs import (
BigBirdGenerator, BigBirdGenerator,
BoxSpringGenerator, BoxSpringGenerator,
@ -25,6 +33,7 @@ from gen.sam.ewrs import (
StraightFlushGenerator, StraightFlushGenerator,
TallRackGenerator, TallRackGenerator,
) )
from gen.sam.freya_ewr import FreyaGenerator
from gen.sam.group_generator import GroupGenerator from gen.sam.group_generator import GroupGenerator
from gen.sam.sam_avenger import AvengerGenerator from gen.sam.sam_avenger import AvengerGenerator
from gen.sam.sam_chaparral import ChaparralGenerator from gen.sam.sam_chaparral import ChaparralGenerator
@ -35,7 +44,11 @@ from gen.sam.sam_linebacker import LinebackerGenerator
from gen.sam.sam_patriot import PatriotGenerator from gen.sam.sam_patriot import PatriotGenerator
from gen.sam.sam_rapier import RapierGenerator from gen.sam.sam_rapier import RapierGenerator
from gen.sam.sam_roland import RolandGenerator from gen.sam.sam_roland import RolandGenerator
from gen.sam.sam_sa10 import SA10Generator from gen.sam.sam_sa10 import (
SA10Generator,
Tier2SA10Generator,
Tier3SA10Generator,
)
from gen.sam.sam_sa11 import SA11Generator from gen.sam.sam_sa11 import SA11Generator
from gen.sam.sam_sa13 import SA13Generator from gen.sam.sam_sa13 import SA13Generator
from gen.sam.sam_sa15 import SA15Generator from gen.sam.sam_sa15 import SA15Generator
@ -50,11 +63,8 @@ from gen.sam.sam_zsu23 import ZSU23Generator
from gen.sam.sam_zu23 import ZU23Generator from gen.sam.sam_zu23 import ZU23Generator
from gen.sam.sam_zu23_ural import ZU23UralGenerator from gen.sam.sam_zu23_ural import ZU23UralGenerator
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
from gen.sam.freya_ewr import FreyaGenerator
from theater import TheaterGroundObject
from theater.theatergroundobject import SamGroundObject
SAM_MAP = { SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
"HawkGenerator": HawkGenerator, "HawkGenerator": HawkGenerator,
"ZU23Generator": ZU23Generator, "ZU23Generator": ZU23Generator,
"ZU23UralGenerator": ZU23UralGenerator, "ZU23UralGenerator": ZU23UralGenerator,
@ -77,6 +87,8 @@ SAM_MAP = {
"SA8Generator": SA8Generator, "SA8Generator": SA8Generator,
"SA9Generator": SA9Generator, "SA9Generator": SA9Generator,
"SA10Generator": SA10Generator, "SA10Generator": SA10Generator,
"Tier2SA10Generator": Tier2SA10Generator,
"Tier3SA10Generator": Tier3SA10Generator,
"SA11Generator": SA11Generator, "SA11Generator": SA11Generator,
"SA13Generator": SA13Generator, "SA13Generator": SA13Generator,
"SA15Generator": SA15Generator, "SA15Generator": SA15Generator,
@ -89,6 +101,7 @@ SAM_MAP = {
"AllyWW2FlakGenerator": AllyWW2FlakGenerator "AllyWW2FlakGenerator": AllyWW2FlakGenerator
} }
SAM_PRICES = { SAM_PRICES = {
AirDefence.SAM_Hawk_PCP: 35, AirDefence.SAM_Hawk_PCP: 35,
AirDefence.AAA_ZU_23_Emplacement: 10, AirDefence.AAA_ZU_23_Emplacement: 10,
@ -137,42 +150,75 @@ EWR_MAP = {
} }
def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerator]]: def get_faction_possible_sams_generator(
faction: Faction) -> List[Type[AirDefenseGroupGenerator]]:
""" """
Return the list of possible SAM generator for the given faction Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for :param faction: Faction name to search units for
""" """
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP] return [SAM_MAP[s] for s in faction.air_defenses]
def get_faction_possible_ewrs_generator(faction: str) -> List[Type[GroupGenerator]]: def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGenerator]]:
""" """
Return the list of possible SAM generator for the given faction Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for :param faction: Faction name to search units for
""" """
return [EWR_MAP[s] for s in db.FACTIONS[faction].ewrs if s in EWR_MAP] return [EWR_MAP[s] for s in faction.ewrs]
def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject, def _generate_anti_air_from(
faction: str) -> Optional[VehicleGroup]: generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game,
ground_object: SamGroundObject) -> Optional[VehicleGroup]:
if not generators:
return None
sam_generator_class = random.choice(generators)
generator = sam_generator_class(game, ground_object)
generator.generate()
return generator.get_generated_group()
def generate_anti_air_group(
game: Game, ground_object: SamGroundObject, faction: Faction,
ranges: Optional[Iterable[Set[AirDefenseRange]]] = None
) -> Optional[VehicleGroup]:
""" """
This generate a SAM group This generate a SAM group
:param game: The Game. :param game: The Game.
:param ground_object: The ground object which will own the sam group. :param ground_object: The ground object which will own the sam group.
:param faction: Owner faction. :param faction: Owner faction.
:param ranges: Optional list of preferred ranges of the air defense to
create. If None, any generator may be used. Otherwise generators
matching the given ranges will be used in order of preference. For
example, when given `[{Long, Medium}, {Short}]`, long and medium range
air defenses will be tried first with no bias, and short range air
defenses will be used if no long or medium range generators are
available to the faction. If instead `[{Long}, {Medium}, {Short}]` had
been used, long range systems would take precedence over medium range
systems. If instead `[{Long, Medium, Short}]` had been used, all types
would be considered with equal preference.
:return: The generated group, or None if one could not be generated. :return: The generated group, or None if one could not be generated.
""" """
possible_sams_generators = get_faction_possible_sams_generator(faction) generators = get_faction_possible_sams_generator(faction)
if len(possible_sams_generators) > 0: if ranges is None:
sam_generator_class = random.choice(possible_sams_generators) ranges = [{
generator = sam_generator_class(game, ground_object) AirDefenseRange.Long,
generator.generate() AirDefenseRange.Medium,
return generator.get_generated_group() AirDefenseRange.Short,
}]
for range_options in ranges:
generators_for_range = [g for g in generators if
g.range() in range_options]
group = _generate_anti_air_from(generators_for_range, game,
ground_object)
if group is not None:
return group
return None return None
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject, def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]: faction: Faction) -> Optional[VehicleGroup]:
"""Generates an early warning radar group. """Generates an early warning radar group.
:param game: The Game. :param game: The Game.
@ -187,16 +233,3 @@ def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
return None return None
def generate_shorad_group(game: Game, ground_object: SamGroundObject,
faction_name: str) -> Optional[VehicleGroup]:
faction = db.FACTIONS[faction_name]
if len(faction.shorads) > 0:
sam = random.choice(faction.shorads)
generator = SAM_MAP[sam](game, ground_object)
generator.generate()
return generator.get_generated_group()
else:
return generate_anti_air_group(game, ground_object, faction_name)

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class HawkGenerator(GenericSamGroupGenerator): class HawkGenerator(AirDefenseGroupGenerator):
""" """
This generate an HAWK group This generate an HAWK group
""" """
@ -26,3 +29,7 @@ class HawkGenerator(GenericSamGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Hawk_LN_M192, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_Hawk_LN_M192, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class HQ7Generator(GenericSamGroupGenerator): class HQ7Generator(AirDefenseGroupGenerator):
""" """
This generate an HQ7 group This generate an HQ7 group
""" """
@ -26,3 +29,7 @@ class HQ7Generator(GenericSamGroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class LinebackerGenerator(GroupGenerator): class LinebackerGenerator(AirDefenseGroupGenerator):
""" """
This generate an m6 linebacker group This generate an m6 linebacker group
""" """
@ -20,3 +23,7 @@ class LinebackerGenerator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180) positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Linebacker_M6, "M6#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_Linebacker_M6, "M6#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class PatriotGenerator(GenericSamGroupGenerator): class PatriotGenerator(AirDefenseGroupGenerator):
""" """
This generate a Patriot group This generate a Patriot group
""" """
@ -15,7 +18,7 @@ class PatriotGenerator(GenericSamGroupGenerator):
def generate(self): def generate(self):
# Command Post # Command Post
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading) self.add_unit(AirDefence.SAM_Patriot_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_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_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_ICC, "ICC", self.position.x + 60, self.position.y, self.heading)
@ -31,3 +34,7 @@ class PatriotGenerator(GenericSamGroupGenerator):
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, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Long

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class RapierGenerator(GenericSamGroupGenerator): class RapierGenerator(AirDefenseGroupGenerator):
""" """
This generate a Rapier Group This generate a Rapier Group
""" """
@ -22,3 +25,7 @@ class RapierGenerator(GenericSamGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,9 +1,12 @@
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class RolandGenerator(GenericSamGroupGenerator): class RolandGenerator(AirDefenseGroupGenerator):
""" """
This generate a Roland group This generate a Roland group
""" """
@ -16,3 +19,6 @@ class RolandGenerator(GenericSamGroupGenerator):
self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, 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(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,16 +2,19 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA10Generator(GenericSamGroupGenerator): class SA10Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-10 group This generate a SA-10 group
""" """
name = "SA-10/S-300PS Battery" name = "SA-10/S-300PS Battery"
price = 450 price = 550
def generate(self): def generate(self):
# Search Radar # Search Radar
@ -38,15 +41,55 @@ class SA10Generator(GenericSamGroupGenerator):
else: else:
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85D, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85D, "LN#" + str(i), position[0], position[1], position[2])
# Then let's add short range protection to this high value site self.generate_defensive_groups()
# Sa-13 Strela are great for that
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "IR#" + str(i), position[0], position[1], position[2])
# And even some AA @classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Long
def generate_defensive_groups(self) -> None:
# AAA for defending against close targets.
num_launchers = random.randint(6, 8) num_launchers = random.randint(6, 8)
positions = self.get_circular_position(num_launchers, launcher_distance=210, coverage=360) positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
position[0], position[1], position[2])
class Tier2SA10Generator(SA10Generator):
def generate_defensive_groups(self) -> None:
# SA-15 for both shorter range targets and point defense.
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
position[0], position[1], position[2])
# AAA for defending against close targets.
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
position[0], position[1], position[2])
class Tier3SA10Generator(SA10Generator):
def generate_defensive_groups(self) -> None:
# SA-15 for both shorter range targets and point defense.
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(
num_launchers, launcher_distance=140, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
position[0], position[1], position[2])
# AAA for defending against close targets.
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(
num_launchers, launcher_distance=210, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "AA#" + str(i),
position[0], position[1], position[2])

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA11Generator(GenericSamGroupGenerator): class SA11Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-11 group This generate a SA-11 group
""" """
@ -22,3 +25,7 @@ class SA11Generator(GenericSamGroupGenerator):
for i, position in enumerate(positions): 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:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA13Generator(GroupGenerator): class SA13Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-13 group This generate a SA-13 group
""" """
@ -21,3 +24,7 @@ class SA13Generator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_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:
return AirDefenseRange.Short

View File

@ -1,9 +1,12 @@
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA15Generator(GroupGenerator): class SA15Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-15 group This generate a SA-15 group
""" """
@ -15,3 +18,7 @@ class SA15Generator(GroupGenerator):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, 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_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(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x + 80, self.position.y, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA19Generator(GroupGenerator): class SA19Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-19 group This generate a SA-19 group
""" """
@ -22,3 +25,7 @@ class SA19Generator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA2Generator(GenericSamGroupGenerator): class SA2Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-2 group This generate a SA-2 group
""" """
@ -22,3 +25,7 @@ class SA2Generator(GenericSamGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_2_LN_SM_90, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_2_LN_SM_90, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA3Generator(GenericSamGroupGenerator): class SA3Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-3 group This generate a SA-3 group
""" """
@ -22,3 +25,7 @@ class SA3Generator(GenericSamGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_3_S_125_LN_5P73, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_3_S_125_LN_5P73, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA6Generator(GenericSamGroupGenerator): class SA6Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-6 group This generate a SA-6 group
""" """
@ -21,3 +24,7 @@ class SA6Generator(GenericSamGroupGenerator):
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_6_Kub_LN_2P25, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_6_Kub_LN_2P25, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -1,11 +1,12 @@
import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA8Generator(GroupGenerator): class SA8Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-8 group This generate a SA-8 group
""" """
@ -16,3 +17,7 @@ class SA8Generator(GroupGenerator):
def generate(self): def generate(self):
self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading) self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Medium

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class SA9Generator(GroupGenerator): class SA9Generator(AirDefenseGroupGenerator):
""" """
This generate a SA-9 group This generate a SA-9 group
""" """
@ -21,3 +24,7 @@ class SA9Generator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "LN#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "LN#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence, Unarmed from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class VulcanGenerator(GroupGenerator): class VulcanGenerator(AirDefenseGroupGenerator):
""" """
This generate a Vulcan group This generate a Vulcan group
""" """
@ -19,3 +22,7 @@ class VulcanGenerator(GroupGenerator):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA2", self.position.x, self.position.y, self.heading) self.add_unit(AirDefence.AAA_Vulcan_M163, "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(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZSU23Generator(GroupGenerator): class ZSU23Generator(AirDefenseGroupGenerator):
""" """
This generate a ZSU 23 group This generate a ZSU 23 group
""" """
@ -19,3 +22,7 @@ class ZSU23Generator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZU23Generator(GroupGenerator): class ZU23Generator(AirDefenseGroupGenerator):
""" """
This generate a ZU23 flak artillery group This generate a ZU23 flak artillery group
""" """
@ -26,3 +29,7 @@ class ZU23Generator(GroupGenerator):
self.add_unit(AirDefence.AAA_ZU_23_Closed, "AAA#" + str(index), self.add_unit(AirDefence.AAA_ZU_23_Closed, "AAA#" + str(index),
self.position.x + spacing*i, self.position.x + spacing*i,
self.position.y + spacing*j, self.heading) self.position.y + spacing*j, self.heading)
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZU23UralGenerator(GroupGenerator): class ZU23UralGenerator(AirDefenseGroupGenerator):
""" """
This generate a Zu23 Ural group This generate a Zu23 Ural group
""" """
@ -19,3 +22,7 @@ class ZU23UralGenerator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360) positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -2,10 +2,13 @@ import random
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
)
class ZU23UralInsurgentGenerator(GroupGenerator): class ZU23UralInsurgentGenerator(AirDefenseGroupGenerator):
""" """
This generate a Zu23 Ural group This generate a Zu23 Ural group
""" """
@ -19,3 +22,8 @@ class ZU23UralInsurgentGenerator(GroupGenerator):
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360) positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360)
for i, position in enumerate(positions): for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_on_Ural_375, "SPAA#" + str(i), position[0], position[1], position[2]) self.add_unit(AirDefence.AAA_ZU_23_Insurgent_on_Ural_375, "SPAA#" + str(i), position[0], position[1], position[2])
@classmethod
def range(cls) -> AirDefenseRange:
return AirDefenseRange.Short

View File

@ -1,12 +1,38 @@
from dcs.action import MarkToAll from __future__ import annotations
from dcs.condition import TimeAfter
from typing import TYPE_CHECKING
from dcs.action import (
MarkToAll,
SetFlag,
DoScript,
ClearFlag
)
from dcs.condition import (
TimeAfter,
AllOfCoalitionOutsideZone,
PartOfCoalitionInZone,
FlagIsFalse,
FlagIsTrue
)
from dcs.unitgroup import FlyingGroup
from dcs.mission import Mission from dcs.mission import Mission
from dcs.task import Option from dcs.task import Option
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce from dcs.triggers import (
Event,
TriggerOnce,
TriggerZone,
TriggerCondition,
)
from dcs.unit import Skill from dcs.unit import Skill
from .conflictgen import Conflict from game.theater import Airfield
from game.theater.controlpoint import Fob
if TYPE_CHECKING:
from game.game import Game
PUSH_TRIGGER_SIZE = 3000 PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25 PUSH_TRIGGER_ACTIVATION_AGL = 25
@ -30,9 +56,11 @@ class Silence(Option):
class TriggersGenerator: class TriggersGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game): capture_zone_types = (Fob, )
capture_zone_flag = 600
def __init__(self, mission: Mission, game: Game):
self.mission = mission self.mission = mission
self.conflict = conflict
self.game = game self.game = game
def _set_allegiances(self, player_coalition: str, enemy_coalition: str): def _set_allegiances(self, player_coalition: str, enemy_coalition: str):
@ -56,9 +84,8 @@ class TriggersGenerator:
airport.operating_level_fuel = 0 airport.operating_level_fuel = 0
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.is_global: if isinstance(cp, Airfield):
continue self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition)
self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition)
def _set_skill(self, player_coalition: str, enemy_coalition: str): def _set_skill(self, player_coalition: str, enemy_coalition: str):
""" """
@ -73,8 +100,9 @@ class TriggersGenerator:
continue continue
for country in coalition.countries.values(): for country in coalition.countries.values():
for plane_group in country.plane_group: flying_groups = country.plane_group + country.helicopter_group # type: FlyingGroup
for plane_unit in plane_group.units: for flying_group in flying_groups:
for plane_unit in flying_group.units:
if plane_unit.skill != Skill.Client and plane_unit.skill != Skill.Player: if plane_unit.skill != Skill.Client and plane_unit.skill != Skill.Player:
plane_unit.skill = Skill(skill_level[0]) plane_unit.skill = Skill(skill_level[0])
@ -103,16 +131,71 @@ class TriggersGenerator:
added.append(ground_object.obj_name) added.append(ground_object.obj_name)
self.mission.triggerrules.triggers.append(mark_trigger) self.mission.triggerrules.triggers.append(mark_trigger)
def _generate_capture_triggers(self, player_coalition: str, enemy_coalition: str) -> None:
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`.
One for the initial capture of a control point, and one if it is recaptured.
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
"""
for cp in self.game.theater.controlpoints:
if isinstance(cp, self.capture_zone_types):
if cp.captured:
attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attacking_coalition = player_coalition
attack_coalition_int = 2
defending_coalition = enemy_coalition
defend_coalition_int = 1
trigger_zone = self.mission.triggers.add_triggerzone(cp.position, radius=3000, hidden=False, name="CAPTURE")
flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
capture_trigger.add_condition(AllOfCoalitionOutsideZone(defending_coalition, trigger_zone.id))
capture_trigger.add_condition(PartOfCoalitionInZone(attacking_coalition, trigger_zone.id, unit_type="GROUND"))
capture_trigger.add_condition(FlagIsFalse(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
)
capture_trigger.add_action(DoScript(
script_string
)
)
capture_trigger.add_action(SetFlag(flag=flag))
self.mission.triggerrules.triggers.append(capture_trigger)
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
recapture_trigger.add_condition(AllOfCoalitionOutsideZone(attacking_coalition, trigger_zone.id))
recapture_trigger.add_condition(PartOfCoalitionInZone(defending_coalition, trigger_zone.id, unit_type="GROUND"))
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
)
recapture_trigger.add_action(DoScript(
script_string
)
)
recapture_trigger.add_action(ClearFlag(flag=flag))
self.mission.triggerrules.triggers.append(recapture_trigger)
def generate(self): def generate(self):
player_coalition = "blue" player_coalition = "blue"
enemy_coalition = "red" enemy_coalition = "red"
self.mission.coalition["blue"].bullseye = {"x": self.conflict.position.x, player_cp, enemy_cp = self.game.theater.closest_opposing_control_points()
"y": self.conflict.position.y} self.mission.coalition["blue"].bullseye = {"x": enemy_cp.position.x,
self.mission.coalition["red"].bullseye = {"x": self.conflict.position.x, "y": enemy_cp.position.y}
"y": self.conflict.position.y} self.mission.coalition["red"].bullseye = {"x": player_cp.position.x,
"y": player_cp.position.y}
self._set_skill(player_coalition, enemy_coalition) self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers() self._gen_markers()
self._generate_capture_triggers(player_coalition, enemy_coalition)
@classmethod
def get_capture_zone_flag(cls):
flag = cls.capture_zone_flag
cls.capture_zone_flag += 1
return flag

View File

@ -92,9 +92,8 @@ def turn_heading(heading, fac):
class VisualGenerator: class VisualGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game: Game): def __init__(self, mission: Mission, game: Game):
self.mission = mission self.mission = mission
self.conflict = conflict
self.game = game self.game = game
def _generate_frontline_smokes(self): def _generate_frontline_smokes(self):
@ -104,15 +103,12 @@ class VisualGenerator:
if from_cp.is_global or to_cp.is_global: if from_cp.is_global or to_cp.is_global:
continue continue
frontline = Conflict.frontline_position(self.game.theater, from_cp, to_cp) plane_start, heading, distance = Conflict.frontline_vector(from_cp, to_cp, self.game.theater)
if not frontline: if not plane_start:
continue continue
point, heading = frontline for offset in range(0, distance, FRONT_SMOKE_SPACING):
plane_start = point.point_from_heading(turn_heading(heading, 90), FRONTLINE_LENGTH / 2) position = plane_start.point_from_heading(heading, offset)
for offset in range(0, FRONTLINE_LENGTH, FRONT_SMOKE_SPACING):
position = plane_start.point_from_heading(turn_heading(heading, - 90), offset)
for k, v in FRONT_SMOKE_TYPE_CHANCES.items(): for k, v in FRONT_SMOKE_TYPE_CHANCES.items():
if random.randint(0, 100) <= k: if random.randint(0, 100) <= k:

View File

@ -10,3 +10,6 @@ ignore_missing_imports = True
[mypy-winreg.*] [mypy-winreg.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-shapely.*]
ignore_missing_imports = True

2
pydcs

@ -1 +1 @@
Subproject commit 2883be31c2eb80834b93efd8d20ca17913986e9b Subproject commit f924289c9cbe6e21a01906bdf11c1933110a32de

View File

@ -0,0 +1,357 @@
from enum import Enum
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
class F_22A(PlaneType):
id = "F-22A"
flyable = True
height = 4.88
width = 13.05
length = 19.1
fuel_max = 6103
max_speed = 2649.996
chaff = 120
flare = 120
charge_total = 240
chaff_charge_size = 1
flare_charge_size = 2
eplrs = True
category = "Interceptor" #{78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
class Liveries:
class USSR(Enum):
default = "default"
class Georgia(Enum):
default = "default"
class Venezuela(Enum):
default = "default"
class Australia(Enum):
default = "default"
class Israel(Enum):
default = "default"
class Combined_Joint_Task_Forces_Blue(Enum):
default = "default"
class Sudan(Enum):
default = "default"
class Norway(Enum):
default = "default"
class Romania(Enum):
default = "default"
class Iran(Enum):
default = "default"
class Ukraine(Enum):
default = "default"
class Libya(Enum):
default = "default"
class Belgium(Enum):
default = "default"
class Slovakia(Enum):
default = "default"
class Greece(Enum):
default = "default"
class UK(Enum):
default = "default"
class Third_Reich(Enum):
default = "default"
class Hungary(Enum):
default = "default"
class Abkhazia(Enum):
default = "default"
class Morocco(Enum):
default = "default"
class United_Nations_Peacekeepers(Enum):
default = "default"
class Switzerland(Enum):
default = "default"
class SouthOssetia(Enum):
default = "default"
class Vietnam(Enum):
default = "default"
class China(Enum):
default = "default"
class Yemen(Enum):
default = "default"
class Kuwait(Enum):
default = "default"
class Serbia(Enum):
default = "default"
class Oman(Enum):
default = "default"
class India(Enum):
default = "default"
class Egypt(Enum):
default = "default"
class TheNetherlands(Enum):
default = "default"
class Poland(Enum):
default = "default"
class Syria(Enum):
default = "default"
class Finland(Enum):
default = "default"
class Kazakhstan(Enum):
default = "default"
class Denmark(Enum):
default = "default"
class Sweden(Enum):
default = "default"
class Croatia(Enum):
default = "default"
class CzechRepublic(Enum):
default = "default"
class GDR(Enum):
default = "default"
class Yugoslavia(Enum):
default = "default"
class Bulgaria(Enum):
default = "default"
class SouthKorea(Enum):
default = "default"
class Tunisia(Enum):
default = "default"
class Combined_Joint_Task_Forces_Red(Enum):
default = "default"
class Lebanon(Enum):
default = "default"
class Portugal(Enum):
default = "default"
class Cuba(Enum):
default = "default"
class Insurgents(Enum):
default = "default"
class SaudiArabia(Enum):
default = "default"
class France(Enum):
default = "default"
class USA(Enum):
default = "default"
class Honduras(Enum):
default = "default"
class Qatar(Enum):
default = "default"
class Russia(Enum):
default = "default"
class United_Arab_Emirates(Enum):
default = "default"
class Italian_Social_Republi(Enum):
default = "default"
class Austria(Enum):
default = "default"
class Bahrain(Enum):
default = "default"
class Italy(Enum):
default = "default"
class Chile(Enum):
default = "default"
class Turkey(Enum):
default = "default"
class Philippines(Enum):
default = "default"
class Algeria(Enum):
default = "default"
class Pakistan(Enum):
default = "default"
class Malaysia(Enum):
default = "default"
class Indonesia(Enum):
default = "default"
class Iraq(Enum):
default = "default"
class Germany(Enum):
default = "default"
class South_Africa(Enum):
default = "default"
class Jordan(Enum):
default = "default"
class Mexico(Enum):
default = "default"
class USAFAggressors(Enum):
default = "default"
class Brazil(Enum):
default = "default"
class Spain(Enum):
default = "default"
class Belarus(Enum):
default = "default"
class Canada(Enum):
default = "default"
class NorthKorea(Enum):
default = "default"
class Ethiopia(Enum):
default = "default"
class Japan(Enum):
default = "default"
class Thailand(Enum):
default = "default"
class Pylon1:
AIM_9X_Sidewinder_IR_AAM = (1, Weapons.AIM_9X_Sidewinder_IR_AAM)
class Pylon2:
Fuel_tank_610_gal = (2, Weapons.Fuel_tank_610_gal)
AIM_9X_Sidewinder_IR_AAM = (2, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_9M_Sidewinder_IR_AAM = (2, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_120C = (2, Weapons.AIM_120C)
Smokewinder___red = (2, Weapons.Smokewinder___red)
Smokewinder___green = (2, Weapons.Smokewinder___green)
Smokewinder___blue = (2, Weapons.Smokewinder___blue)
Smokewinder___white = (2, Weapons.Smokewinder___white)
Smokewinder___yellow = (2, Weapons.Smokewinder___yellow)
CBU_97 = (2, Weapons.CBU_97)
Fuel_tank_370_gal = (2, Weapons.Fuel_tank_370_gal)
LAU_115_2_LAU_127_AIM_9M = (2, Weapons.LAU_115_2_LAU_127_AIM_9M)
LAU_115_2_LAU_127_AIM_9X = (2, Weapons.LAU_115_2_LAU_127_AIM_9X)
LAU_115_2_LAU_127_AIM_120C = (2, Weapons.LAU_115_2_LAU_127_AIM_120C)
class Pylon3:
AIM_9M_Sidewinder_IR_AAM = (3, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (3, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (3, Weapons.AIM_120C)
CBU_97 = (3, Weapons.CBU_97)
class Pylon4:
AIM_9M_Sidewinder_IR_AAM = (4, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (4, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (4, Weapons.AIM_120C)
CBU_97 = (4, Weapons.CBU_97)
class Pylon5:
AIM_9M_Sidewinder_IR_AAM = (5, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (5, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (5, Weapons.AIM_120C)
CBU_97 = (5, Weapons.CBU_97)
class Pylon6:
Smokewinder___red = (6, Weapons.Smokewinder___red)
Smokewinder___green = (6, Weapons.Smokewinder___green)
Smokewinder___blue = (6, Weapons.Smokewinder___blue)
Smokewinder___white = (6, Weapons.Smokewinder___white)
Smokewinder___yellow = (6, Weapons.Smokewinder___yellow)
class Pylon7:
AIM_9M_Sidewinder_IR_AAM = (7, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (7, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (7, Weapons.AIM_120C)
CBU_97 = (7, Weapons.CBU_97)
class Pylon8:
AIM_9M_Sidewinder_IR_AAM = (8, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (8, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (8, Weapons.AIM_120C)
CBU_97 = (8, Weapons.CBU_97)
class Pylon9:
AIM_9M_Sidewinder_IR_AAM = (9, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9X_Sidewinder_IR_AAM = (9, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_120C = (9, Weapons.AIM_120C)
CBU_97 = (9, Weapons.CBU_97)
class Pylon10:
Fuel_tank_610_gal = (10, Weapons.Fuel_tank_610_gal)
AIM_9X_Sidewinder_IR_AAM = (10, Weapons.AIM_9X_Sidewinder_IR_AAM)
AIM_9M_Sidewinder_IR_AAM = (10, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_120C = (10, Weapons.AIM_120C)
Smokewinder___red = (10, Weapons.Smokewinder___red)
Smokewinder___green = (10, Weapons.Smokewinder___green)
Smokewinder___blue = (10, Weapons.Smokewinder___blue)
Smokewinder___white = (10, Weapons.Smokewinder___white)
Smokewinder___yellow = (10, Weapons.Smokewinder___yellow)
CBU_97 = (10, Weapons.CBU_97)
Fuel_tank_370_gal = (10, Weapons.Fuel_tank_370_gal)
LAU_115_2_LAU_127_AIM_9M = (10, Weapons.LAU_115_2_LAU_127_AIM_9M)
LAU_115_2_LAU_127_AIM_9X = (10, Weapons.LAU_115_2_LAU_127_AIM_9X)
LAU_115_2_LAU_127_AIM_120C = (10, Weapons.LAU_115_2_LAU_127_AIM_120C)
class Pylon11:
AIM_9X_Sidewinder_IR_AAM = (11, Weapons.AIM_9X_Sidewinder_IR_AAM)
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
tasks = [task.CAP, task.Escort, task.FighterSweep, task.Intercept, task.Reconnaissance]
task_default = task.CAP

View File

@ -0,0 +1,739 @@
from enum import Enum
from dcs import task
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
class HerculesWeapons:
GAU_23A_Chain_Gun__30mm_ = {"clsid": "{Herc_GAU_23A_Chain_Gun}", "name": "GAU 23A Chain Gun (30mm)", "weight": 595.9426}
Herc_AAA_GEPARD = {"clsid": "Herc_AAA_GEPARD", "name": "AAA GEPARD [34720lb]", "weight": 15782}
Herc_AAA_Vulcan_M163 = {"clsid": "Herc_AAA_Vulcan_M163", "name": "AAA Vulcan M163 [21666lb]", "weight": 9848}
Herc_Ammo_AGM_154C_missiles = {"clsid": "Herc_Ammo_AGM_154C_missiles", "name": "Ammo AGM-154C*10 [10648lb]", "weight": 4960}
Herc_Ammo_AGM_65D_missiles = {"clsid": "Herc_Ammo_AGM_65D_missiles", "name": "Ammo AGM-65D*10 [4800lb]", "weight": 2300}
Herc_Ammo_AGM_65E_missiles = {"clsid": "Herc_Ammo_AGM_65E_missiles", "name": "Ammo AGM-65E*10 [6292lb]", "weight": 2980}
Herc_Ammo_AGM_65G_missiles = {"clsid": "Herc_Ammo_AGM_65G_missiles", "name": "Ammo AGM-65G*10 [6622lb]", "weight": 3130}
Herc_Ammo_AGM_65H_missiles = {"clsid": "Herc_Ammo_AGM_65H_missiles", "name": "Ammo AGM-65H*10 [4570lb]", "weight": 2200}
Herc_Ammo_AGM_65K_missiles = {"clsid": "Herc_Ammo_AGM_65K_missiles", "name": "Ammo AGM-65K*10 [7920lb]", "weight": 3720}
Herc_Ammo_AGM_84A_missiles = {"clsid": "Herc_Ammo_AGM_84A_missiles", "name": "Ammo AGM-84A*8 [11651lb]", "weight": 5408}
Herc_Ammo_AGM_84E_missiles = {"clsid": "Herc_Ammo_AGM_84E_missiles", "name": "Ammo AGM-84E*8 [11651lb]", "weight": 5408}
Herc_Ammo_AGM_88C_missiles = {"clsid": "Herc_Ammo_AGM_88C_missiles", "name": "Ammo AGM-88C*10 [7920lb]", "weight": 3730}
Herc_Ammo_AIM120B_missiles = {"clsid": "Herc_Ammo_AIM120B_missiles", "name": "Ammo AIM-120B*24 [11193lb]", "weight": 5208}
Herc_Ammo_AIM120C_missiles = {"clsid": "Herc_Ammo_AIM120C_missiles", "name": "Ammo AIM-120C*24 [10665lb]", "weight": 5208}
Herc_Ammo_AIM54C_missiles = {"clsid": "Herc_Ammo_AIM54C_missiles", "name": "Ammo AIM-54C*18 [18335lb]", "weight": 8454}
Herc_Ammo_AIM7M_missiles = {"clsid": "Herc_Ammo_AIM7M_missiles", "name": "Ammo AIM-7M*24 [14995lb]", "weight": 6936}
Herc_Ammo_AIM9M_missiles = {"clsid": "Herc_Ammo_AIM9M_missiles", "name": "Ammo AIM-9M*30 [7128lb]", "weight": 4860}
Herc_Ammo_AIM9P5_missiles = {"clsid": "Herc_Ammo_AIM9P5_missiles", "name": "Ammo AIM-9P5*30 [5676lb]", "weight": 2700}
Herc_Ammo_AIM9X_missiles = {"clsid": "Herc_Ammo_AIM9X_missiles", "name": "Ammo AIM-9X*30 [5676lb]", "weight": 2700}
Herc_Ammo_BETAB500SP_bombs = {"clsid": "Herc_Ammo_BETAB500SP_bombs", "name": "Ammo BetAB-500ShP*10 [9328lb]", "weight": 4360}
Herc_Ammo_BETAB500_bombs = {"clsid": "Herc_Ammo_BETAB500_bombs", "name": "Ammo BetAB-500*10 [9460lb]", "weight": 4420}
Herc_Ammo_CBU_103_bombs = {"clsid": "Herc_Ammo_CBU_103_bombs", "name": "Ammo CBU-103*10 [10142lb]", "weight": 4730}
Herc_Ammo_CBU_105_bombs = {"clsid": "Herc_Ammo_CBU_105_bombs", "name": "Ammo CBU-105*10 [11022lb]", "weight": 5130}
Herc_Ammo_CBU_87_bombs = {"clsid": "Herc_Ammo_CBU_87_bombs", "name": "Ammo CBU-87*10 [9460lb]", "weight": 4420}
Herc_Ammo_CBU_97_bombs = {"clsid": "Herc_Ammo_CBU_97_bombs", "name": "Ammo CBU-97*10 [10362lb]", "weight": 4830}
Herc_Ammo_FAB100_bombs = {"clsid": "Herc_Ammo_FAB100_bombs", "name": "Ammo FAB-100*20 [4400lb", "weight": 2120}
Herc_Ammo_FAB250_bombs = {"clsid": "Herc_Ammo_FAB250_bombs", "name": "Ammo FAB-250*20 [11000lb]", "weight": 5120}
Herc_Ammo_FAB500_bombs = {"clsid": "Herc_Ammo_FAB500_bombs", "name": "Ammo FAB-500*10 [11000lb]", "weight": 5120}
Herc_Ammo_GBU_10_bombs = {"clsid": "Herc_Ammo_GBU_10_bombs", "name": "Ammo GBU-10*6 [15340lb]", "weight": 7092}
Herc_Ammo_GBU_12_bombs = {"clsid": "Herc_Ammo_GBU_12_bombs", "name": "Ammo GBU-12*16 [9680lb]", "weight": 4520}
Herc_Ammo_GBU_16_bombs = {"clsid": "Herc_Ammo_GBU_16_bombs", "name": "Ammo GBU-16*10 [12408lb]", "weight": 5760}
Herc_Ammo_GBU_31_V3B_bombs = {"clsid": "Herc_Ammo_GBU_31_V3B_bombs", "name": "Ammo GBU-31V3B*6 [12949lb]", "weight": 6006}
Herc_Ammo_GBU_31_VB_bombs = {"clsid": "Herc_Ammo_GBU_31_VB_bombs", "name": "Ammo GBU-31V/B*6 [12328lb]", "weight": 5724}
Herc_Ammo_GBU_38_bombs = {"clsid": "Herc_Ammo_GBU_38_bombs", "name": "Ammo GBU-38*10 [6028lb]", "weight": 2860}
Herc_Ammo_hydra_HE_rockets = {"clsid": "Herc_Ammo_hydra_HE_rockets", "name": "Ammo M151 Hydra HE*80 [4752lb]", "weight": 2280}
Herc_Ammo_hydra_WP_rockets = {"clsid": "Herc_Ammo_hydra_WP_rockets", "name": "Ammo M156 Hydra WP*80 [4752lb]", "weight": 2280}
Herc_Ammo_KAB500KR_bombs = {"clsid": "Herc_Ammo_KAB500KR_bombs", "name": "Ammo KAB-500kr*10 [12320lb]", "weight": 5720}
Herc_Ammo_KH25ML_missiles = {"clsid": "Herc_Ammo_KH25ML_missiles", "name": "Ammo Kh-25ML*10 [7920lb]", "weight": 3720}
Herc_Ammo_KH25MPU_missiles = {"clsid": "Herc_Ammo_KH25MPU_missiles", "name": "Ammo Kh-25MPU*10 [8140lb]", "weight": 3820}
Herc_Ammo_KH29L_missiles = {"clsid": "Herc_Ammo_KH29L_missiles", "name": "Ammo Kh-29L*10 [16434lb]", "weight": 7590}
Herc_Ammo_KH29T_missiles = {"clsid": "Herc_Ammo_KH29T_missiles", "name": "Ammo Kh-29T*10 [16720lb]", "weight": 7720}
Herc_Ammo_KH58U_missiles = {"clsid": "Herc_Ammo_KH58U_missiles", "name": "Ammo Kh-58U*10 [16060lb]", "weight": 7420}
Herc_Ammo_KMGU296AO25KO_bombs = {"clsid": "Herc_Ammo_KMGU296AO25KO_bombs", "name": "Ammo KMGU-2 - 96 PTAB-2.5KO*10 [11440lb]", "weight": 5320}
Herc_Ammo_KMGU296AO25RT_bombs = {"clsid": "Herc_Ammo_KMGU296AO25RT_bombs", "name": "Ammo KMGU-2 - 96 AO-2.5RT*10 [11440lb]", "weight": 5320}
Herc_Ammo_M117_bombs = {"clsid": "Herc_Ammo_M117_bombs", "name": "Ammo M117*16 [11968lb]", "weight": 5560}
Herc_Ammo_MAGIC2_missiles = {"clsid": "Herc_Ammo_MAGIC2_missiles", "name": "Ammo Magic2*30 [5676lb]", "weight": 2700}
Herc_Ammo_MK20_bombs = {"clsid": "Herc_Ammo_MK20_bombs", "name": "Ammo MK20*20 [9768lb]", "weight": 4560}
Herc_Ammo_Mk_82AIR_bombs = {"clsid": "Herc_Ammo_Mk_82AIR_bombs", "name": "Ammo Mk-82AIR*20 [11044lb]", "weight": 4940}
Herc_Ammo_Mk_82Snake_bombs = {"clsid": "Herc_Ammo_Mk_82Snake_bombs", "name": "Ammo Mk-82Snakeye*20 [11880lb]", "weight": 4940}
Herc_Ammo_Mk_82_bombs = {"clsid": "Herc_Ammo_Mk_82_bombs", "name": "Ammo Mk-82*20 [10560lb]", "weight": 4940}
Herc_Ammo_Mk_83_bombs = {"clsid": "Herc_Ammo_Mk_83_bombs", "name": "Ammo Mk-83*10 [9834lb]", "weight": 4590}
Herc_Ammo_Mk_84_bombs = {"clsid": "Herc_Ammo_Mk_84_bombs", "name": "Ammo Mk-84*8 [15735b]", "weight": 7272}
Herc_Ammo_R27ER_missiles = {"clsid": "Herc_Ammo_R27ER_missiles", "name": "Ammo R-27ER*24 [18480lb]", "weight": 8520}
Herc_Ammo_R27ET_missiles = {"clsid": "Herc_Ammo_R27ET_missiles", "name": "Ammo R-27ET*24 [18480lb", "weight": 8496}
Herc_Ammo_R27R_missiles = {"clsid": "Herc_Ammo_R27R_missiles", "name": "Ammo R-27R*24 [13359lb]", "weight": 6192}
Herc_Ammo_R27T_missiles = {"clsid": "Herc_Ammo_R27T_missiles", "name": "Ammo R-27T*24 [13359lb]", "weight": 6192}
Herc_Ammo_R60M_missiles = {"clsid": "Herc_Ammo_R60M_missiles", "name": "Ammo R-60M*30 [2904lb]", "weight": 1440}
Herc_Ammo_R77_missiles = {"clsid": "Herc_Ammo_R77_missiles", "name": "Ammo R-77*24 [9240lb]", "weight": 4320}
Herc_Ammo_RBK250PTAB25M_bombs = {"clsid": "Herc_Ammo_RBK250PTAB25M_bombs", "name": "Ammo RBK-250 PTAB-2.5M*20 [12012lb]", "weight": 5580}
Herc_Ammo_RBK500255PTAB105_bombs = {"clsid": "Herc_Ammo_RBK500255PTAB105_bombs", "name": "Ammo RBK-500-255 PTAB-10-5*10 [9394lb]", "weight": 4390}
Herc_Ammo_RBK500PTAB1M_bombs = {"clsid": "Herc_Ammo_RBK500PTAB1M_bombs", "name": "Ammo RBK-500 PTAB-1M*10 [9394lb]", "weight": 4390}
Herc_Ammo_S24B_missiles = {"clsid": "Herc_Ammo_S24B_missiles", "name": "Ammo S-24B*20 [10340lb]", "weight": 4820}
Herc_Ammo_S25L_missiles = {"clsid": "Herc_Ammo_S25L_missiles", "name": "Ammo S-25L*10 [11000b]", "weight": 5120}
Herc_Ammo_S25OFM_missiles = {"clsid": "Herc_Ammo_S25OFM_missiles", "name": "Ammo S-25OFM*10 [10890lb]", "weight": 5070}
Herc_Ammo_S530D_missiles = {"clsid": "Herc_Ammo_S530D_missiles", "name": "Ammo Super 530D*24 [6480lb]", "weight": 6600}
Herc_Ammo_SAB100_bombs = {"clsid": "Herc_Ammo_SAB100_bombs", "name": "Ammo SAB-100*20 [11000lb]", "weight": 2120}
Herc_Ammo_Vikhr_missiles = {"clsid": "Herc_Ammo_Vikhr_missiles", "name": "Ammo Vikhr*48 [5808lb]", "weight": 2760}
Herc_APC_BTR_80 = {"clsid": "Herc_APC_BTR_80", "name": "APC BTR-80 [23936lb]", "weight": 10880}
Herc_APC_COBRA = {"clsid": "Herc_APC_COBRA", "name": "APC Cobra [10912lb]", "weight": 4960}
Herc_APC_LAV_25 = {"clsid": "Herc_APC_LAV_25", "name": "APC LAV-25 [22514lb]", "weight": 10234}
Herc_APC_M1025_HMMWV = {"clsid": "Herc_APC_M1025_HMMWV", "name": "M1025 HMMWV [6160lb]", "weight": 2800}
Herc_APC_M1043_HMMWV_Armament = {"clsid": "Herc_APC_M1043_HMMWV_Armament", "name": "APC M1043 HMMWV Armament [7023lb]", "weight": 3192}
Herc_APC_M113 = {"clsid": "Herc_APC_M113", "name": "APC M113 [21624lb]", "weight": 9830}
Herc_APC_MTLB = {"clsid": "Herc_APC_MTLB", "name": "APC MTLB [26000lb]", "weight": 12000}
Herc_ART_GVOZDIKA = {"clsid": "Herc_ART_GVOZDIKA", "name": "ART GVOZDIKA [34720lb]", "weight": 15782}
Herc_ART_NONA = {"clsid": "Herc_ART_NONA", "name": "ART 2S9 NONA [19140lb]", "weight": 8700}
Herc_ARV_BRDM_2 = {"clsid": "Herc_ARV_BRDM_2", "name": "ARV BRDM-2 [12320lb]", "weight": 5600}
Herc_ATGM_M1045_HMMWV_TOW = {"clsid": "Herc_ATGM_M1045_HMMWV_TOW", "name": "ATGM M1045 HMMWV TOW [7183lb]", "weight": 3265}
Herc_ATGM_M1134_Stryker = {"clsid": "Herc_ATGM_M1134_Stryker", "name": "ATGM M1134 Stryker [30337lb]", "weight": 13790}
Herc_BattleStation = {"clsid": "Herc_BattleStation", "name": "Battle Station", "weight": 0}
Herc_Ext_Fuel_Tank = {"clsid": "Herc_Ext_Fuel_Tank", "name": "External Fuel Tank", "weight": 4131}
Herc_GEN_CRATE = {"clsid": "Herc_GEN_CRATE", "name": "Generic Crate [20000lb]", "weight": 9071}
Herc_HEMTT_TFFT = {"clsid": "Herc_HEMTT_TFFT", "name": "HEMTT TFFT [34400lb]", "weight": 15634}
Herc_IFV_BMD1 = {"clsid": "Herc_IFV_BMD1", "name": "IFV BMD-1 [18040lb]", "weight": 8200}
Herc_IFV_BMP_1 = {"clsid": "Herc_IFV_BMP_1", "name": "IFV BMP-1 [23232lb]", "weight": 10560}
Herc_IFV_BMP_2 = {"clsid": "Herc_IFV_BMP_2", "name": "IFV BMP-2 [25168lb]", "weight": 11440}
Herc_IFV_BMP_3 = {"clsid": "Herc_IFV_BMP_3", "name": "IFV BMP-3 [32912lb]", "weight": 14960}
Herc_IFV_BTRD = {"clsid": "Herc_IFV_BTRD", "name": "IFV BTR-D [18040lb]", "weight": 8200}
Herc_IFV_M2A2_Bradley = {"clsid": "Herc_IFV_M2A2_Bradley", "name": "IFV M2A2 Bradley [34720lb]", "weight": 15782}
Herc_IFV_MARDER = {"clsid": "Herc_IFV_MARDER", "name": "IFV MARDER [34720lb]", "weight": 15782}
Herc_IFV_MCV80_Warrior = {"clsid": "Herc_IFV_MCV80_Warrior", "name": "IFV MCV-80 [34720lb]", "weight": 15782}
Herc_IFV_TPZ = {"clsid": "Herc_IFV_TPZ", "name": "IFV TPZ FUCH [33440lb]", "weight": 15200}
Herc_JATO = {"clsid": "Herc_JATO", "name": "JATO", "weight": 0}
Herc_M_818 = {"clsid": "Herc_M_818", "name": "Transport M818 [16000lb]", "weight": 7272}
Herc_SAM_13 = {"clsid": "Herc_SAM_13", "name": "SAM SA-13 STRELA [21624lb]", "weight": 9830}
Herc_SAM_19 = {"clsid": "Herc_SAM_19", "name": "SAM SA-19 Tunguska 2S6 [34720lb]", "weight": 15782}
Herc_SAM_CHAPARRAL = {"clsid": "Herc_SAM_CHAPARRAL", "name": "SAM CHAPARRAL [21624lb]", "weight": 9830}
Herc_SAM_LINEBACKER = {"clsid": "Herc_SAM_LINEBACKER", "name": "SAM LINEBACKER [34720lb]", "weight": 15782}
Herc_SAM_M1097_HMMWV = {"clsid": "Herc_SAM_M1097_HMMWV", "name": "SAM Avenger M1097 [7200lb]", "weight": 3273}
Herc_SAM_ROLAND_ADS = {"clsid": "Herc_SAM_ROLAND_ADS", "name": "SAM ROLAND ADS [34720lb]", "weight": 15782}
Herc_SAM_ROLAND_LN = {"clsid": "Herc_SAM_ROLAND_LN", "name": "SAM ROLAND LN [34720b]", "weight": 15782}
Herc_Soldier_Squad = {"clsid": "Herc_Soldier_Squad", "name": "Squad 30 x Soldier [7950lb]", "weight": 120}
Herc_SPG_M1126_Stryker_ICV = {"clsid": "Herc_SPG_M1126_Stryker_ICV", "name": "APC M1126 Stryker ICV [29542lb]", "weight": 13429}
Herc_SPG_M1128_Stryker_MGS = {"clsid": "Herc_SPG_M1128_Stryker_MGS", "name": "SPG M1128 Stryker MGS [33036lb]", "weight": 15016}
Herc_Tanker_HEMTT = {"clsid": "Herc_Tanker_HEMTT", "name": "Tanker M978 HEMTT [34000lb]", "weight": 15455}
Herc_TIGR_233036 = {"clsid": "Herc_TIGR_233036", "name": "Transport Tigr [15900lb]", "weight": 7200}
Herc_UAZ_469 = {"clsid": "Herc_UAZ_469", "name": "Transport UAZ-469 [3747lb]", "weight": 1700}
Herc_URAL_375 = {"clsid": "Herc_URAL_375", "name": "Transport URAL-375 [14815lb]", "weight": 6734}
Herc_ZSU_23_4 = {"clsid": "Herc_ZSU_23_4", "name": "AAA ZSU-23-4 Shilka [32912lb]", "weight": 14960}
M61_Vulcan_Rotary_Cannon__20mm_ = {"clsid": "{Herc_M61_Vulcan_Rotary_Cannon}", "name": "M61 Vulcan Rotary Cannon (20mm)", "weight": 595.9426}
_105mm_Howitzer = {"clsid": "{Herc_105mm_Howitzer}", "name": "105mm Howitzer", "weight": 595.9426}
class Hercules(PlaneType):
id = "Hercules"
flyable = True
height = 11.84
width = 40.41
length = 34.36
fuel_max = 19759
max_speed = 669.6
chaff = 840
flare = 840
charge_total = 1680
chaff_charge_size = 1
flare_charge_size = 1
category = "Air" #{C168A850-3C0B-436a-95B5-C4A015552560}
radio_frequency = 305
panel_radio = {
1: {
"channels": {
1: 305,
2: 264,
4: 256,
8: 257,
16: 261,
17: 261,
9: 255,
18: 251,
5: 254,
10: 262,
20: 266,
11: 259,
3: 265,
6: 250,
12: 268,
13: 269,
7: 270,
14: 260,
19: 253,
15: 263
},
},
}
class Liveries:
class USSR(Enum):
default = "default"
class Georgia(Enum):
default = "default"
class Venezuela(Enum):
default = "default"
class Australia(Enum):
default = "default"
class Israel(Enum):
default = "default"
class Combined_Joint_Task_Forces_Blue(Enum):
default = "default"
class Sudan(Enum):
default = "default"
class Norway(Enum):
default = "default"
class Romania(Enum):
default = "default"
class Iran(Enum):
default = "default"
class Ukraine(Enum):
default = "default"
class Libya(Enum):
default = "default"
class Belgium(Enum):
default = "default"
class Slovakia(Enum):
default = "default"
class Greece(Enum):
default = "default"
class UK(Enum):
default = "default"
class Third_Reich(Enum):
default = "default"
class Hungary(Enum):
default = "default"
class Abkhazia(Enum):
default = "default"
class Morocco(Enum):
default = "default"
class United_Nations_Peacekeepers(Enum):
default = "default"
class Switzerland(Enum):
default = "default"
class SouthOssetia(Enum):
default = "default"
class Vietnam(Enum):
default = "default"
class China(Enum):
default = "default"
class Yemen(Enum):
default = "default"
class Kuwait(Enum):
default = "default"
class Serbia(Enum):
default = "default"
class Oman(Enum):
default = "default"
class India(Enum):
default = "default"
class Egypt(Enum):
default = "default"
class TheNetherlands(Enum):
default = "default"
class Poland(Enum):
default = "default"
class Syria(Enum):
default = "default"
class Finland(Enum):
default = "default"
class Kazakhstan(Enum):
default = "default"
class Denmark(Enum):
default = "default"
class Sweden(Enum):
default = "default"
class Croatia(Enum):
default = "default"
class CzechRepublic(Enum):
default = "default"
class GDR(Enum):
default = "default"
class Yugoslavia(Enum):
default = "default"
class Bulgaria(Enum):
default = "default"
class SouthKorea(Enum):
default = "default"
class Tunisia(Enum):
default = "default"
class Combined_Joint_Task_Forces_Red(Enum):
default = "default"
class Lebanon(Enum):
default = "default"
class Portugal(Enum):
default = "default"
class Cuba(Enum):
default = "default"
class Insurgents(Enum):
default = "default"
class SaudiArabia(Enum):
default = "default"
class France(Enum):
default = "default"
class USA(Enum):
default = "default"
class Honduras(Enum):
default = "default"
class Qatar(Enum):
default = "default"
class Russia(Enum):
default = "default"
class United_Arab_Emirates(Enum):
default = "default"
class Italian_Social_Republi(Enum):
default = "default"
class Austria(Enum):
default = "default"
class Bahrain(Enum):
default = "default"
class Italy(Enum):
default = "default"
class Chile(Enum):
default = "default"
class Turkey(Enum):
default = "default"
class Philippines(Enum):
default = "default"
class Algeria(Enum):
default = "default"
class Pakistan(Enum):
default = "default"
class Malaysia(Enum):
default = "default"
class Indonesia(Enum):
default = "default"
class Iraq(Enum):
default = "default"
class Germany(Enum):
default = "default"
class South_Africa(Enum):
default = "default"
class Jordan(Enum):
default = "default"
class Mexico(Enum):
default = "default"
class USAFAggressors(Enum):
default = "default"
class Brazil(Enum):
default = "default"
class Spain(Enum):
default = "default"
class Belarus(Enum):
default = "default"
class Canada(Enum):
default = "default"
class NorthKorea(Enum):
default = "default"
class Ethiopia(Enum):
default = "default"
class Japan(Enum):
default = "default"
class Thailand(Enum):
default = "default"
class Pylon1:
Herc_JATO = (1, HerculesWeapons.Herc_JATO)
class Pylon2:
LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (2, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
Smokewinder___red = (2, Weapons.Smokewinder___red)
Smokewinder___green = (2, Weapons.Smokewinder___green)
Smokewinder___blue = (2, Weapons.Smokewinder___blue)
Smokewinder___white = (2, Weapons.Smokewinder___white)
Smokewinder___yellow = (2, Weapons.Smokewinder___yellow)
Smokewinder___orange = (2, Weapons.Smokewinder___orange)
Herc_Ext_Fuel_Tank = (2, HerculesWeapons.Herc_Ext_Fuel_Tank)
class Pylon3:
LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (3, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
Smokewinder___red = (3, Weapons.Smokewinder___red)
Smokewinder___green = (3, Weapons.Smokewinder___green)
Smokewinder___blue = (3, Weapons.Smokewinder___blue)
Smokewinder___white = (3, Weapons.Smokewinder___white)
Smokewinder___yellow = (3, Weapons.Smokewinder___yellow)
Smokewinder___orange = (3, Weapons.Smokewinder___orange)
Herc_Ext_Fuel_Tank = (3, HerculesWeapons.Herc_Ext_Fuel_Tank)
class Pylon4:
LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (4, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
Smokewinder___red = (4, Weapons.Smokewinder___red)
Smokewinder___green = (4, Weapons.Smokewinder___green)
Smokewinder___blue = (4, Weapons.Smokewinder___blue)
Smokewinder___white = (4, Weapons.Smokewinder___white)
Smokewinder___yellow = (4, Weapons.Smokewinder___yellow)
Smokewinder___orange = (4, Weapons.Smokewinder___orange)
Herc_Ext_Fuel_Tank = (4, HerculesWeapons.Herc_Ext_Fuel_Tank)
class Pylon5:
LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (5, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
Smokewinder___red = (5, Weapons.Smokewinder___red)
Smokewinder___green = (5, Weapons.Smokewinder___green)
Smokewinder___blue = (5, Weapons.Smokewinder___blue)
Smokewinder___white = (5, Weapons.Smokewinder___white)
Smokewinder___yellow = (5, Weapons.Smokewinder___yellow)
Smokewinder___orange = (5, Weapons.Smokewinder___orange)
Herc_Ext_Fuel_Tank = (5, HerculesWeapons.Herc_Ext_Fuel_Tank)
class Pylon6:
M61_Vulcan_Rotary_Cannon__20mm_ = (6, HerculesWeapons.M61_Vulcan_Rotary_Cannon__20mm_)
class Pylon7:
GAU_23A_Chain_Gun__30mm_ = (7, HerculesWeapons.GAU_23A_Chain_Gun__30mm_)
class Pylon8:
_105mm_Howitzer = (8, HerculesWeapons._105mm_Howitzer)
class Pylon9:
Herc_BattleStation = (9, HerculesWeapons.Herc_BattleStation)
class Pylon10:
Herc_Ammo_AGM_65D_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
Herc_Ammo_AGM_65H_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
Herc_Ammo_AGM_65G_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
Herc_Ammo_AGM_65E_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
Herc_Ammo_AGM_88C_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
Herc_Ammo_AGM_65K_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
Herc_Ammo_Vikhr_missiles = (10, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
Herc_Ammo_AGM_84A_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
Herc_Ammo_AGM_84E_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
Herc_Ammo_KH25ML_missiles = (10, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
Herc_Ammo_KH25MPU_missiles = (10, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
Herc_Ammo_KH29T_missiles = (10, HerculesWeapons.Herc_Ammo_KH29T_missiles)
Herc_Ammo_KH29L_missiles = (10, HerculesWeapons.Herc_Ammo_KH29L_missiles)
Herc_Ammo_KH58U_missiles = (10, HerculesWeapons.Herc_Ammo_KH58U_missiles)
Herc_Ammo_S24B_missiles = (10, HerculesWeapons.Herc_Ammo_S24B_missiles)
Herc_Ammo_S25OFM_missiles = (10, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
Herc_Ammo_S25L_missiles = (10, HerculesWeapons.Herc_Ammo_S25L_missiles)
Herc_Ammo_GBU_10_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
Herc_Ammo_GBU_12_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
Herc_Ammo_GBU_16_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
Herc_Ammo_GBU_31_VB_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
Herc_Ammo_GBU_31_V3B_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
Herc_Ammo_GBU_38_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
Herc_Ammo_CBU_87_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
Herc_Ammo_CBU_97_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
Herc_Ammo_CBU_103_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
Herc_Ammo_CBU_105_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
Herc_Ammo_Mk_82_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
Herc_Ammo_Mk_82AIR_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
Herc_Ammo_Mk_82Snake_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
Herc_Ammo_Mk_83_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
Herc_Ammo_Mk_84_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
Herc_Ammo_FAB100_bombs = (10, HerculesWeapons.Herc_Ammo_FAB100_bombs)
Herc_Ammo_FAB250_bombs = (10, HerculesWeapons.Herc_Ammo_FAB250_bombs)
Herc_Ammo_FAB500_bombs = (10, HerculesWeapons.Herc_Ammo_FAB500_bombs)
Herc_Ammo_BETAB500_bombs = (10, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
Herc_Ammo_BETAB500SP_bombs = (10, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
Herc_Ammo_KAB500KR_bombs = (10, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
Herc_Ammo_RBK250PTAB25M_bombs = (10, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
Herc_Ammo_RBK500255PTAB105_bombs = (10, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
Herc_Ammo_RBK500PTAB1M_bombs = (10, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
Herc_Ammo_KMGU296AO25RT_bombs = (10, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
Herc_Ammo_KMGU296AO25KO_bombs = (10, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
Herc_Ammo_MK20_bombs = (10, HerculesWeapons.Herc_Ammo_MK20_bombs)
Herc_Ammo_SAB100_bombs = (10, HerculesWeapons.Herc_Ammo_SAB100_bombs)
Herc_Ammo_hydra_HE_rockets = (10, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
Herc_Ammo_hydra_WP_rockets = (10, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
Herc_Ammo_AIM9M_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
Herc_Ammo_AIM9P5_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
Herc_Ammo_AIM9X_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
Herc_Ammo_AIM7M_missiles = (10, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
Herc_Ammo_AIM120B_missiles = (10, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
Herc_Ammo_AIM120C_missiles = (10, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
Herc_Ammo_R60M_missiles = (10, HerculesWeapons.Herc_Ammo_R60M_missiles)
Herc_Ammo_MAGIC2_missiles = (10, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
Herc_Ammo_R27R_missiles = (10, HerculesWeapons.Herc_Ammo_R27R_missiles)
Herc_Ammo_R27ER_missiles = (10, HerculesWeapons.Herc_Ammo_R27ER_missiles)
Herc_Ammo_R27T_missiles = (10, HerculesWeapons.Herc_Ammo_R27T_missiles)
Herc_Ammo_R27ET_missiles = (10, HerculesWeapons.Herc_Ammo_R27ET_missiles)
#ERRR Herc_Ammo_R27_missiles
Herc_Ammo_S530D_missiles = (10, HerculesWeapons.Herc_Ammo_S530D_missiles)
Herc_Ammo_AIM54C_missiles = (10, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
Herc_APC_M1043_HMMWV_Armament = (10, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
Herc_ATGM_M1045_HMMWV_TOW = (10, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
Herc_APC_M1025_HMMWV = (10, HerculesWeapons.Herc_APC_M1025_HMMWV)
Herc_SAM_M1097_HMMWV = (10, HerculesWeapons.Herc_SAM_M1097_HMMWV)
Herc_APC_COBRA = (10, HerculesWeapons.Herc_APC_COBRA)
Herc_ARV_BRDM_2 = (10, HerculesWeapons.Herc_ARV_BRDM_2)
Herc_TIGR_233036 = (10, HerculesWeapons.Herc_TIGR_233036)
Herc_IFV_BMD1 = (10, HerculesWeapons.Herc_IFV_BMD1)
Herc_IFV_BTRD = (10, HerculesWeapons.Herc_IFV_BTRD)
Herc_ART_NONA = (10, HerculesWeapons.Herc_ART_NONA)
Herc_GEN_CRATE = (10, HerculesWeapons.Herc_GEN_CRATE)
class Pylon11:
Herc_Ammo_AGM_65D_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
Herc_Ammo_AGM_65H_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
Herc_Ammo_AGM_65G_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
Herc_Ammo_AGM_65E_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
Herc_Ammo_AGM_88C_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
Herc_Ammo_AGM_65K_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
Herc_Ammo_Vikhr_missiles = (11, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
Herc_Ammo_AGM_84A_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
Herc_Ammo_AGM_84E_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
Herc_Ammo_KH25ML_missiles = (11, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
Herc_Ammo_KH25MPU_missiles = (11, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
Herc_Ammo_KH29T_missiles = (11, HerculesWeapons.Herc_Ammo_KH29T_missiles)
Herc_Ammo_KH29L_missiles = (11, HerculesWeapons.Herc_Ammo_KH29L_missiles)
Herc_Ammo_KH58U_missiles = (11, HerculesWeapons.Herc_Ammo_KH58U_missiles)
Herc_Ammo_S24B_missiles = (11, HerculesWeapons.Herc_Ammo_S24B_missiles)
Herc_Ammo_S25OFM_missiles = (11, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
Herc_Ammo_S25L_missiles = (11, HerculesWeapons.Herc_Ammo_S25L_missiles)
Herc_Ammo_GBU_10_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
Herc_Ammo_GBU_12_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
Herc_Ammo_GBU_16_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
Herc_Ammo_GBU_31_VB_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
Herc_Ammo_GBU_31_V3B_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
Herc_Ammo_GBU_38_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
Herc_Ammo_CBU_87_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
Herc_Ammo_CBU_97_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
Herc_Ammo_CBU_103_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
Herc_Ammo_CBU_105_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
Herc_Ammo_Mk_82_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
Herc_Ammo_Mk_82AIR_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
Herc_Ammo_Mk_82Snake_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
Herc_Ammo_Mk_83_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
Herc_Ammo_Mk_84_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
Herc_Ammo_FAB100_bombs = (11, HerculesWeapons.Herc_Ammo_FAB100_bombs)
Herc_Ammo_FAB250_bombs = (11, HerculesWeapons.Herc_Ammo_FAB250_bombs)
Herc_Ammo_FAB500_bombs = (11, HerculesWeapons.Herc_Ammo_FAB500_bombs)
Herc_Ammo_BETAB500_bombs = (11, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
Herc_Ammo_BETAB500SP_bombs = (11, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
Herc_Ammo_KAB500KR_bombs = (11, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
Herc_Ammo_RBK250PTAB25M_bombs = (11, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
Herc_Ammo_RBK500255PTAB105_bombs = (11, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
Herc_Ammo_RBK500PTAB1M_bombs = (11, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
Herc_Ammo_KMGU296AO25RT_bombs = (11, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
Herc_Ammo_KMGU296AO25KO_bombs = (11, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
Herc_Ammo_MK20_bombs = (11, HerculesWeapons.Herc_Ammo_MK20_bombs)
Herc_Ammo_SAB100_bombs = (11, HerculesWeapons.Herc_Ammo_SAB100_bombs)
Herc_Ammo_hydra_HE_rockets = (11, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
Herc_Ammo_hydra_WP_rockets = (11, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
Herc_Ammo_AIM9M_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
Herc_Ammo_AIM9P5_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
Herc_Ammo_AIM9X_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
Herc_Ammo_AIM7M_missiles = (11, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
Herc_Ammo_AIM120B_missiles = (11, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
Herc_Ammo_AIM120C_missiles = (11, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
Herc_Ammo_R60M_missiles = (11, HerculesWeapons.Herc_Ammo_R60M_missiles)
Herc_Ammo_MAGIC2_missiles = (11, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
Herc_Ammo_R27R_missiles = (11, HerculesWeapons.Herc_Ammo_R27R_missiles)
Herc_Ammo_R27ER_missiles = (11, HerculesWeapons.Herc_Ammo_R27ER_missiles)
Herc_Ammo_R27T_missiles = (11, HerculesWeapons.Herc_Ammo_R27T_missiles)
Herc_Ammo_R27ET_missiles = (11, HerculesWeapons.Herc_Ammo_R27ET_missiles)
#ERRR Herc_Ammo_R27_missiles
Herc_Ammo_S530D_missiles = (11, HerculesWeapons.Herc_Ammo_S530D_missiles)
Herc_Ammo_AIM54C_missiles = (11, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
Herc_APC_M1043_HMMWV_Armament = (11, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
Herc_ATGM_M1045_HMMWV_TOW = (11, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
Herc_AAA_Vulcan_M163 = (11, HerculesWeapons.Herc_AAA_Vulcan_M163)
Herc_SPG_M1126_Stryker_ICV = (11, HerculesWeapons.Herc_SPG_M1126_Stryker_ICV)
Herc_SPG_M1128_Stryker_MGS = (11, HerculesWeapons.Herc_SPG_M1128_Stryker_MGS)
Herc_ATGM_M1134_Stryker = (11, HerculesWeapons.Herc_ATGM_M1134_Stryker)
Herc_APC_LAV_25 = (11, HerculesWeapons.Herc_APC_LAV_25)
Herc_APC_M1025_HMMWV = (11, HerculesWeapons.Herc_APC_M1025_HMMWV)
Herc_SAM_M1097_HMMWV = (11, HerculesWeapons.Herc_SAM_M1097_HMMWV)
Herc_APC_COBRA = (11, HerculesWeapons.Herc_APC_COBRA)
Herc_APC_M113 = (11, HerculesWeapons.Herc_APC_M113)
Herc_Tanker_HEMTT = (11, HerculesWeapons.Herc_Tanker_HEMTT)
Herc_HEMTT_TFFT = (11, HerculesWeapons.Herc_HEMTT_TFFT)
Herc_IFV_M2A2_Bradley = (11, HerculesWeapons.Herc_IFV_M2A2_Bradley)
Herc_IFV_MCV80_Warrior = (11, HerculesWeapons.Herc_IFV_MCV80_Warrior)
Herc_IFV_BMP_1 = (11, HerculesWeapons.Herc_IFV_BMP_1)
Herc_IFV_BMP_2 = (11, HerculesWeapons.Herc_IFV_BMP_2)
Herc_IFV_BMP_3 = (11, HerculesWeapons.Herc_IFV_BMP_3)
Herc_ARV_BRDM_2 = (11, HerculesWeapons.Herc_ARV_BRDM_2)
Herc_APC_BTR_80 = (11, HerculesWeapons.Herc_APC_BTR_80)
Herc_SAM_ROLAND_ADS = (11, HerculesWeapons.Herc_SAM_ROLAND_ADS)
Herc_SAM_ROLAND_LN = (11, HerculesWeapons.Herc_SAM_ROLAND_LN)
Herc_SAM_13 = (11, HerculesWeapons.Herc_SAM_13)
Herc_ZSU_23_4 = (11, HerculesWeapons.Herc_ZSU_23_4)
Herc_SAM_19 = (11, HerculesWeapons.Herc_SAM_19)
Herc_UAZ_469 = (11, HerculesWeapons.Herc_UAZ_469)
Herc_URAL_375 = (11, HerculesWeapons.Herc_URAL_375)
Herc_M_818 = (11, HerculesWeapons.Herc_M_818)
Herc_TIGR_233036 = (11, HerculesWeapons.Herc_TIGR_233036)
Herc_AAA_GEPARD = (11, HerculesWeapons.Herc_AAA_GEPARD)
Herc_SAM_CHAPARRAL = (11, HerculesWeapons.Herc_SAM_CHAPARRAL)
Herc_SAM_LINEBACKER = (11, HerculesWeapons.Herc_SAM_LINEBACKER)
Herc_IFV_MARDER = (11, HerculesWeapons.Herc_IFV_MARDER)
Herc_IFV_TPZ = (11, HerculesWeapons.Herc_IFV_TPZ)
Herc_IFV_BMD1 = (11, HerculesWeapons.Herc_IFV_BMD1)
Herc_IFV_BTRD = (11, HerculesWeapons.Herc_IFV_BTRD)
Herc_ART_NONA = (11, HerculesWeapons.Herc_ART_NONA)
Herc_ART_GVOZDIKA = (11, HerculesWeapons.Herc_ART_GVOZDIKA)
Herc_APC_MTLB = (11, HerculesWeapons.Herc_APC_MTLB)
Herc_GEN_CRATE = (11, HerculesWeapons.Herc_GEN_CRATE)
class Pylon12:
Herc_Soldier_Squad = (12, HerculesWeapons.Herc_Soldier_Squad)
Herc_Ammo_AGM_65D_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
Herc_Ammo_AGM_65H_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
Herc_Ammo_AGM_65G_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
Herc_Ammo_AGM_65E_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
Herc_Ammo_AGM_88C_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
Herc_Ammo_AGM_65K_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
Herc_Ammo_Vikhr_missiles = (12, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
Herc_Ammo_AGM_84A_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
Herc_Ammo_AGM_84E_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
Herc_Ammo_KH25ML_missiles = (12, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
Herc_Ammo_KH25MPU_missiles = (12, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
Herc_Ammo_KH29T_missiles = (12, HerculesWeapons.Herc_Ammo_KH29T_missiles)
Herc_Ammo_KH29L_missiles = (12, HerculesWeapons.Herc_Ammo_KH29L_missiles)
Herc_Ammo_KH58U_missiles = (12, HerculesWeapons.Herc_Ammo_KH58U_missiles)
Herc_Ammo_S24B_missiles = (12, HerculesWeapons.Herc_Ammo_S24B_missiles)
Herc_Ammo_S25OFM_missiles = (12, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
Herc_Ammo_S25L_missiles = (12, HerculesWeapons.Herc_Ammo_S25L_missiles)
Herc_Ammo_GBU_10_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
Herc_Ammo_GBU_12_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
Herc_Ammo_GBU_16_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
Herc_Ammo_GBU_31_VB_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
Herc_Ammo_GBU_31_V3B_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
Herc_Ammo_GBU_38_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
Herc_Ammo_CBU_87_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
Herc_Ammo_CBU_97_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
Herc_Ammo_CBU_103_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
Herc_Ammo_CBU_105_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
Herc_Ammo_Mk_82_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
Herc_Ammo_Mk_82AIR_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
Herc_Ammo_Mk_82Snake_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
Herc_Ammo_Mk_83_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
Herc_Ammo_Mk_84_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
Herc_Ammo_FAB100_bombs = (12, HerculesWeapons.Herc_Ammo_FAB100_bombs)
Herc_Ammo_FAB250_bombs = (12, HerculesWeapons.Herc_Ammo_FAB250_bombs)
Herc_Ammo_FAB500_bombs = (12, HerculesWeapons.Herc_Ammo_FAB500_bombs)
Herc_Ammo_BETAB500_bombs = (12, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
Herc_Ammo_BETAB500SP_bombs = (12, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
Herc_Ammo_KAB500KR_bombs = (12, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
Herc_Ammo_RBK250PTAB25M_bombs = (12, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
Herc_Ammo_RBK500255PTAB105_bombs = (12, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
Herc_Ammo_RBK500PTAB1M_bombs = (12, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
Herc_Ammo_KMGU296AO25RT_bombs = (12, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
Herc_Ammo_KMGU296AO25KO_bombs = (12, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
Herc_Ammo_MK20_bombs = (12, HerculesWeapons.Herc_Ammo_MK20_bombs)
Herc_Ammo_SAB100_bombs = (12, HerculesWeapons.Herc_Ammo_SAB100_bombs)
Herc_Ammo_hydra_HE_rockets = (12, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
Herc_Ammo_hydra_WP_rockets = (12, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
Herc_Ammo_AIM9M_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
Herc_Ammo_AIM9P5_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
Herc_Ammo_AIM9X_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
Herc_Ammo_AIM7M_missiles = (12, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
Herc_Ammo_AIM120B_missiles = (12, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
Herc_Ammo_AIM120C_missiles = (12, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
Herc_Ammo_R60M_missiles = (12, HerculesWeapons.Herc_Ammo_R60M_missiles)
Herc_Ammo_MAGIC2_missiles = (12, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
Herc_Ammo_R27R_missiles = (12, HerculesWeapons.Herc_Ammo_R27R_missiles)
Herc_Ammo_R27ER_missiles = (12, HerculesWeapons.Herc_Ammo_R27ER_missiles)
Herc_Ammo_R27T_missiles = (12, HerculesWeapons.Herc_Ammo_R27T_missiles)
Herc_Ammo_R27ET_missiles = (12, HerculesWeapons.Herc_Ammo_R27ET_missiles)
#ERRR Herc_Ammo_R27_missiles
Herc_Ammo_S530D_missiles = (12, HerculesWeapons.Herc_Ammo_S530D_missiles)
Herc_Ammo_AIM54C_missiles = (12, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
Herc_APC_M1043_HMMWV_Armament = (12, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
Herc_ATGM_M1045_HMMWV_TOW = (12, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
Herc_AAA_Vulcan_M163 = (12, HerculesWeapons.Herc_AAA_Vulcan_M163)
Herc_APC_LAV_25 = (12, HerculesWeapons.Herc_APC_LAV_25)
Herc_APC_M1025_HMMWV = (12, HerculesWeapons.Herc_APC_M1025_HMMWV)
Herc_SAM_M1097_HMMWV = (12, HerculesWeapons.Herc_SAM_M1097_HMMWV)
Herc_APC_COBRA = (12, HerculesWeapons.Herc_APC_COBRA)
Herc_APC_M113 = (12, HerculesWeapons.Herc_APC_M113)
Herc_IFV_BMP_1 = (12, HerculesWeapons.Herc_IFV_BMP_1)
Herc_ARV_BRDM_2 = (12, HerculesWeapons.Herc_ARV_BRDM_2)
Herc_APC_BTR_80 = (12, HerculesWeapons.Herc_APC_BTR_80)
Herc_SAM_13 = (12, HerculesWeapons.Herc_SAM_13)
Herc_UAZ_469 = (12, HerculesWeapons.Herc_UAZ_469)
Herc_URAL_375 = (12, HerculesWeapons.Herc_URAL_375)
Herc_M_818 = (12, HerculesWeapons.Herc_M_818)
Herc_TIGR_233036 = (12, HerculesWeapons.Herc_TIGR_233036)
Herc_SAM_CHAPARRAL = (12, HerculesWeapons.Herc_SAM_CHAPARRAL)
Herc_IFV_BMD1 = (12, HerculesWeapons.Herc_IFV_BMD1)
Herc_IFV_BTRD = (12, HerculesWeapons.Herc_IFV_BTRD)
Herc_ART_NONA = (12, HerculesWeapons.Herc_ART_NONA)
Herc_GEN_CRATE = (12, HerculesWeapons.Herc_GEN_CRATE)
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
tasks = [task.Transport, task.CAS, task.GroundAttack]
task_default = task.Transport

View File

@ -1,11 +1,13 @@
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.highdigitsams import highdigitsams from pydcs_extensions.highdigitsams import highdigitsams
from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S, Rafale_B
from pydcs_extensions.su57.su57 import Su_57 from pydcs_extensions.su57.su57 import Su_57
import pydcs_extensions.frenchpack.frenchpack as frenchpack import pydcs_extensions.frenchpack.frenchpack as frenchpack
MODDED_AIRPLANES = [A_4E_C, MB_339PAN, Rafale_A_S, Rafale_M, Su_57] MODDED_AIRPLANES = [A_4E_C, MB_339PAN, Rafale_A_S, Rafale_M, Rafale_B, Su_57, F_22A, Hercules]
MODDED_VEHICLES = [ MODDED_VEHICLES = [
frenchpack._FIELD_HIDE, frenchpack._FIELD_HIDE,
frenchpack._FIELD_HIDE_SMALL, frenchpack._FIELD_HIDE_SMALL,

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
from typing import Optional from typing import Optional
from gen.flights.flight import Flight from gen.flights.flight import Flight
from theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from .models import GameModel, PackageModel from .models import GameModel, PackageModel
from .windows.mission.QEditFlightDialog import QEditFlightDialog from .windows.mission.QEditFlightDialog import QEditFlightDialog
from .windows.mission.QPackageDialog import ( from .windows.mission.QPackageDialog import (

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