mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into ITAHawkmoon-update-db
This commit is contained in:
commit
95db2aa14f
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -36,11 +36,6 @@ jobs:
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -43,11 +43,6 @@ jobs:
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy theater
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy theater
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
|
||||
18
changelog.md
18
changelog.md
@ -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
|
||||
|
||||
# Features/Improvements
|
||||
|
||||
@ -3,9 +3,9 @@ import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
|
||||
|
||||
WW2_FREE = ['fuel', 'factory', 'ware']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
|
||||
WW2_FREE = ['fuel', 'factory', 'ware', 'fob']
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'fob']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'fob']
|
||||
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
|
||||
@ -36,6 +36,8 @@ class Doctrine:
|
||||
|
||||
cas_duration: timedelta
|
||||
|
||||
sweep_distance: int
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@ -62,6 +64,7 @@ MODERN_DOCTRINE = Doctrine(
|
||||
cap_min_distance_from_cp=nm_to_meter(10),
|
||||
cap_max_distance_from_cp=nm_to_meter(40),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(60),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@ -89,6 +92,7 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
cap_min_distance_from_cp=nm_to_meter(8),
|
||||
cap_max_distance_from_cp=nm_to_meter(25),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(40),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@ -116,4 +120,5 @@ WWII_DOCTRINE = Doctrine(
|
||||
cap_min_distance_from_cp=nm_to_meter(0),
|
||||
cap_max_distance_from_cp=nm_to_meter(5),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(10),
|
||||
)
|
||||
|
||||
92
game/db.py
92
game/db.py
@ -106,7 +106,8 @@ from dcs.planes import (
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
plane_map
|
||||
plane_map,
|
||||
I_16
|
||||
)
|
||||
from dcs.ships import (
|
||||
Armed_speedboat,
|
||||
@ -115,6 +116,7 @@ from dcs.ships import (
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
CVN_74_John_C__Stennis,
|
||||
CVN_75_Harry_S__Truman,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
@ -159,15 +161,19 @@ import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
# PATCH pydcs data with MODS
|
||||
from game.factions.faction_loader import FactionLoader
|
||||
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.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
|
||||
|
||||
plane_map["A-4E-C"] = A_4E_C
|
||||
plane_map["MB-339PAN"] = MB_339PAN
|
||||
plane_map["Rafale_M"] = Rafale_M
|
||||
plane_map["Rafale_A_S"] = Rafale_A_S
|
||||
plane_map["Rafale_B"] = Rafale_B
|
||||
plane_map["Su-57"] = Su_57
|
||||
plane_map["Hercules"] = Hercules
|
||||
|
||||
vehicle_map["FieldHL"] = frenchpack._FIELD_HIDE
|
||||
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`
|
||||
"""
|
||||
|
||||
# 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.
|
||||
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,
|
||||
SpitfireLFMkIXCW: 14,
|
||||
I_16: 10,
|
||||
Bf_109K_4: 14,
|
||||
FW_190D9: 16,
|
||||
FW_190A8: 14,
|
||||
@ -274,6 +286,7 @@ PRICES = {
|
||||
F_16A: 14,
|
||||
F_14A_135_GR: 20,
|
||||
F_14B: 24,
|
||||
F_22A: 40,
|
||||
Tornado_IDS: 20,
|
||||
Tornado_GR4: 20,
|
||||
|
||||
@ -330,6 +343,7 @@ PRICES = {
|
||||
KJ_2000: 50,
|
||||
E_3A: 50,
|
||||
C_130: 25,
|
||||
Hercules: 25,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA: 18,
|
||||
@ -347,6 +361,7 @@ PRICES = {
|
||||
# Modded
|
||||
Rafale_M: 26,
|
||||
Rafale_A_S: 26,
|
||||
Rafale_B: 26,
|
||||
|
||||
# armor
|
||||
Armor.APC_MTLB: 4,
|
||||
@ -579,6 +594,7 @@ UNIT_BY_TASK = {
|
||||
MiG_31,
|
||||
FA_18C_hornet,
|
||||
F_15C,
|
||||
F_22A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_16A,
|
||||
@ -593,6 +609,7 @@ UNIT_BY_TASK = {
|
||||
JF_17,
|
||||
F_4E,
|
||||
C_101CC,
|
||||
I_16,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
@ -635,6 +652,7 @@ UNIT_BY_TASK = {
|
||||
P_47D_40,
|
||||
RQ_1A_Predator,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
@ -651,14 +669,14 @@ UNIT_BY_TASK = {
|
||||
Tu_95MS,
|
||||
UH_1H,
|
||||
WingLoong_I,
|
||||
Hercules
|
||||
],
|
||||
Transport: [
|
||||
IL_76MD,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
C_130,
|
||||
C_130
|
||||
],
|
||||
Refueling: [
|
||||
IL_78M,
|
||||
@ -1010,6 +1028,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
F_14B: COMMON_OVERRIDE,
|
||||
F_15C: COMMON_OVERRIDE,
|
||||
F_111F: COMMON_OVERRIDE,
|
||||
F_22A: COMMON_OVERRIDE,
|
||||
F_16C_50: COMMON_OVERRIDE,
|
||||
JF_17: 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_190A8: COMMON_OVERRIDE,
|
||||
Bf_109K_4: COMMON_OVERRIDE,
|
||||
I_16: COMMON_OVERRIDE,
|
||||
SpitfireLFMkIXCW: COMMON_OVERRIDE,
|
||||
SpitfireLFMkIX: COMMON_OVERRIDE,
|
||||
A_20G: COMMON_OVERRIDE,
|
||||
@ -1061,6 +1081,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
MB_339PAN: COMMON_OVERRIDE,
|
||||
Rafale_M: COMMON_OVERRIDE,
|
||||
Rafale_A_S: COMMON_OVERRIDE,
|
||||
Rafale_B: COMMON_OVERRIDE,
|
||||
OH_58D: COMMON_OVERRIDE,
|
||||
F_16A: 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_64D: COMMON_OVERRIDE,
|
||||
AH_64A: COMMON_OVERRIDE,
|
||||
Hercules: COMMON_OVERRIDE,
|
||||
|
||||
Su_25TM: {
|
||||
SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410",
|
||||
@ -1130,7 +1152,7 @@ TIME_PERIODS = {
|
||||
}
|
||||
|
||||
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,
|
||||
"derrick": 8
|
||||
}
|
||||
@ -1201,6 +1223,8 @@ def upgrade_to_supercarrier(unit, name: str):
|
||||
return CVN_72_Abraham_Lincoln
|
||||
elif name == "CVN-73 George Washington":
|
||||
return CVN_73_George_Washington
|
||||
elif name == "CVN-75 Harry S. Truman":
|
||||
return CVN_75_Harry_S__Truman
|
||||
else:
|
||||
return CVN_71_Theodore_Roosevelt
|
||||
elif unit == CV_1143_5_Admiral_Kuznetsov:
|
||||
@ -1221,29 +1245,45 @@ def unit_task(unit: UnitType) -> Optional[Task]:
|
||||
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]
|
||||
|
||||
|
||||
def find_infantry(country_name: str) -> List[UnitType]:
|
||||
inf = [
|
||||
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.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
|
||||
]
|
||||
MANPADS: List[VehicleType] = [
|
||||
AirDefence.SAM_SA_18_Igla_MANPADS,
|
||||
AirDefence.SAM_SA_18_Igla_S_MANPADS,
|
||||
AirDefence.Stinger_MANPADS
|
||||
]
|
||||
|
||||
INFANTRY: List[VehicleType] = [
|
||||
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Soldier_RPG,
|
||||
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Georgian_soldier_with_M4,
|
||||
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.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]
|
||||
|
||||
|
||||
@ -1255,7 +1295,7 @@ def unit_type_name_2(unit_type) -> str:
|
||||
return unit_type.name and unit_type.name or unit_type.id
|
||||
|
||||
|
||||
def unit_type_from_name(name: str) -> Optional[UnitType]:
|
||||
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
if name in vehicle_map:
|
||||
return vehicle_map[name]
|
||||
elif name in plane_map:
|
||||
|
||||
@ -1,176 +1,233 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
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.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"
|
||||
|
||||
class DebriefingDeadUnitInfo:
|
||||
country_id = -1
|
||||
player_unit = False
|
||||
type = None
|
||||
|
||||
def __init__(self, country_id, player_unit , type):
|
||||
self.country_id = country_id
|
||||
self.player_unit = player_unit
|
||||
self.type = type
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[Flight]
|
||||
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:
|
||||
def __init__(self, state_data, game):
|
||||
self.state_data = state_data
|
||||
self.killed_aircrafts = state_data["killed_aircrafts"]
|
||||
self.killed_ground_units = state_data["killed_ground_units"]
|
||||
self.weapons_fired = state_data["weapons_fired"]
|
||||
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("--------------------------------")
|
||||
def __init__(self, state_data: Dict[str, Any], game: Game,
|
||||
unit_map: UnitMap) -> None:
|
||||
self.state_data = StateData.from_json(state_data)
|
||||
self.unit_map = unit_map
|
||||
|
||||
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.enemy_country_id = db.country_id_from_name(game.enemy_country)
|
||||
|
||||
self.dead_aircraft = []
|
||||
self.dead_units = []
|
||||
self.dead_aaa_groups = []
|
||||
self.dead_buildings = []
|
||||
self.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
|
||||
for aircraft in self.killed_aircrafts:
|
||||
try:
|
||||
country = int(aircraft.split("|")[1])
|
||||
type = db.unit_type_from_name(aircraft.split("|")[4])
|
||||
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)
|
||||
@property
|
||||
def front_line_losses(self) -> Iterator[FrontLineUnit]:
|
||||
yield from self.ground_losses.player_front_line
|
||||
yield from self.ground_losses.enemy_front_line
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
try:
|
||||
country = int(unit.split("|")[1])
|
||||
type = db.unit_type_from_name(unit.split("|")[4])
|
||||
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)
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
for unit in self.killed_ground_units:
|
||||
for cp in game.theater.controlpoints:
|
||||
@property
|
||||
def building_losses(self) -> Iterator[Building]:
|
||||
yield from self.ground_losses.player_buildings
|
||||
yield from self.ground_losses.enemy_buildings
|
||||
|
||||
logging.info(cp.name)
|
||||
logging.info(cp.captured)
|
||||
@property
|
||||
def damaged_runways(self) -> Iterator[Airfield]:
|
||||
yield from self.ground_losses.player_airfields
|
||||
yield from self.ground_losses.enemy_airfields
|
||||
|
||||
if cp.captured:
|
||||
country = self.player_country_id
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
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:
|
||||
country = self.enemy_country_id
|
||||
player_unit = (country == self.player_country_id)
|
||||
losses.enemy_front_line.append(front_line_unit)
|
||||
continue
|
||||
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
logging.info(unit)
|
||||
logging.info(ground_object.group_name)
|
||||
if ground_object.is_same_group(unit):
|
||||
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
|
||||
self.dead_buildings.append(unit)
|
||||
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
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)
|
||||
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
|
||||
if ground_object_unit is not None:
|
||||
if ground_object_unit.ground_object.control_point.captured:
|
||||
losses.player_ground_objects.append(ground_object_unit)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object_unit)
|
||||
continue
|
||||
|
||||
self.player_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_units = [a for a in self.dead_units if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_units = [a for a in self.dead_units if a.country_id == self.enemy_country_id]
|
||||
self.player_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.player_country_id]
|
||||
self.enemy_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.enemy_country_id]
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
else:
|
||||
losses.enemy_buildings.append(building)
|
||||
continue
|
||||
|
||||
logging.info(self.player_dead_aircraft)
|
||||
logging.info(self.enemy_dead_aircraft)
|
||||
logging.info(self.player_dead_units)
|
||||
logging.info(self.enemy_dead_units)
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
if airfield is not None:
|
||||
if airfield.captured:
|
||||
losses.player_airfields.append(airfield)
|
||||
else:
|
||||
losses.enemy_airfields.append(airfield)
|
||||
continue
|
||||
|
||||
self.player_dead_aircraft_dict = {}
|
||||
for a in self.player_dead_aircraft:
|
||||
if a.type in self.player_dead_aircraft_dict.keys():
|
||||
self.player_dead_aircraft_dict[a.type] = self.player_dead_aircraft_dict[a.type] + 1
|
||||
else:
|
||||
self.player_dead_aircraft_dict[a.type] = 1
|
||||
# Only logging as debug because we don't currently track infantry
|
||||
# deaths, so we expect to see quite a few unclaimed dead ground
|
||||
# units. We should start tracking those and covert this to a
|
||||
# warning.
|
||||
logging.debug(f"Death of untracked ground unit {unit_name} will "
|
||||
"have no effect. This may be normal behavior.")
|
||||
|
||||
self.enemy_dead_aircraft_dict = {}
|
||||
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)
|
||||
return losses
|
||||
|
||||
@property
|
||||
def base_capture_events(self):
|
||||
"""Keeps only the last instance of a base capture event for each base ID"""
|
||||
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
reversed_captures = list(reversed(self.state_data.base_capture_events))
|
||||
last_base_cap_indexes = []
|
||||
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
|
||||
if base in [x[1] for x in last_base_cap_indexes]:
|
||||
continue
|
||||
else:
|
||||
if base not in [x[1] for x in last_base_cap_indexes]:
|
||||
last_base_cap_indexes.append((idx, base))
|
||||
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
|
||||
|
||||
@ -179,11 +236,13 @@ class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
regularly for the stopped() condition."""
|
||||
|
||||
def __init__(self, callback: typing.Callable, game):
|
||||
super(PollDebriefingFileThread, self).__init__()
|
||||
def __init__(self, callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map: UnitMap) -> None:
|
||||
super().__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
def stop(self):
|
||||
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:
|
||||
with open("state.json", "r") as 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)
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(callback: typing.Callable, game)->PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game)
|
||||
def wait_for_debriefing(callback: Callable[[Debriefing], None],
|
||||
game: Game, unit_map) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
|
||||
14
game/event/airwar.py
Normal file
14
game/event/airwar.py
Normal 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"
|
||||
@ -2,22 +2,25 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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.task import Task
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import db, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game import persistency
|
||||
from game.debriefing import AirLosses, Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
@ -30,21 +33,16 @@ STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
class Event:
|
||||
silent = False
|
||||
informational = False
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
game = None # type: Game
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
self.departure_cp: Optional[ControlPoint] = None
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
self.location = location
|
||||
@ -55,131 +53,130 @@ class Event:
|
||||
def is_player_attacking(self) -> bool:
|
||||
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
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
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:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return self.operation.is_successfull(debriefing)
|
||||
def generate(self) -> UnitMap:
|
||||
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):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.ca_slots = self.ca_slots
|
||||
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=False)
|
||||
self.operation.generate()
|
||||
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
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:
|
||||
@staticmethod
|
||||
def _transfer_aircraft(ato: AirTaskingOrder, losses: AirLosses,
|
||||
for_player: bool) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
# No need to transfer to the same location.
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
|
||||
# -- Static ground objects
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if (
|
||||
(ground_object.group_name == destroyed_ground_unit_name)
|
||||
or
|
||||
(ground_object.is_same_group(destroyed_ground_unit_name))
|
||||
):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
# Don't transfer to bases that were captured. Note that if the
|
||||
# airfield was back-filling transfers it may overflow. We could
|
||||
# attempt to be smarter in the future by performing transfers in
|
||||
# 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
|
||||
|
||||
info = Information("Building destroyed",
|
||||
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
transfer_count = losses.surviving_flight_members(flight)
|
||||
if transfer_count < 0:
|
||||
logging.error(f"{flight} had {flight.count} aircraft but "
|
||||
f"{transfer_count} losses were recorded.")
|
||||
continue
|
||||
|
||||
aircraft = flight.unit_type
|
||||
available = flight.departure.base.total_units_of_type(aircraft)
|
||||
if available < transfer_count:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {flight.departure} but "
|
||||
f"that airbase has only {available} available.")
|
||||
continue
|
||||
|
||||
# -- AA Site groups
|
||||
destroyed_units = 0
|
||||
info = Information("Units destroyed at " + ground_object.obj_name,
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for u in g.units:
|
||||
if u.name == destroyed_ground_unit_name:
|
||||
g.units.remove(u)
|
||||
g.units_losts.append(u)
|
||||
destroyed_units = destroyed_units + 1
|
||||
info.text = u.type
|
||||
ucount = sum([len(g.units) for g in ground_object.groups])
|
||||
if ucount == 0:
|
||||
ground_object.is_dead = True
|
||||
if destroyed_units > 0:
|
||||
self.game.informations.append(info)
|
||||
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
|
||||
|
||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
||||
self._transfer_aircraft(self.game.blue_ato, debriefing.air_losses,
|
||||
for_player=True)
|
||||
self._transfer_aircraft(self.game.red_ato, debriefing.air_losses,
|
||||
for_player=False)
|
||||
|
||||
@staticmethod
|
||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
aircraft = loss.unit_type
|
||||
cp = loss.departure
|
||||
available = cp.base.total_units_of_type(aircraft)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {cp} but that airbase has "
|
||||
"none available.")
|
||||
continue
|
||||
|
||||
logging.info(f"{aircraft} destroyed from {cp}")
|
||||
cp.base.aircraft[aircraft] -= 1
|
||||
|
||||
@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
|
||||
@ -215,14 +212,14 @@ class Event:
|
||||
for cp in captured_cps:
|
||||
logging.info("Will run redeploy for " + cp.name)
|
||||
self.redeploy_units(cp)
|
||||
except Exception:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.complete_aircraft_transfers(debriefing)
|
||||
|
||||
# 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)
|
||||
|
||||
# -----------------------------------
|
||||
@ -234,8 +231,8 @@ class Event:
|
||||
|
||||
delta = 0.0
|
||||
player_won = True
|
||||
ally_casualties = killed_unit_count_by_cp[cp.id]
|
||||
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
|
||||
ally_casualties = debriefing.casualty_count(cp)
|
||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||
ally_units_alive = cp.base.total_armor
|
||||
enemy_units_alive = enemy_cp.base.total_armor
|
||||
|
||||
@ -352,11 +349,13 @@ class Event:
|
||||
logging.info(info.text)
|
||||
|
||||
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
|
||||
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,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
@ -364,19 +363,22 @@ class UnitsDeliveryEvent(Event):
|
||||
attacker_name=attacker_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)
|
||||
|
||||
def deliver(self, units: Dict[UnitType, int]):
|
||||
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
|
||||
def skip(self) -> None:
|
||||
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)
|
||||
self.game.informations.append(info)
|
||||
if self.to_cp.captured:
|
||||
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)
|
||||
|
||||
@ -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 ..debriefing import Debriefing
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
if self.is_player_attacking:
|
||||
return [CAS, CAP]
|
||||
else:
|
||||
return [CAP]
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return True
|
||||
|
||||
"""
|
||||
An event centered on a FrontLine Conflict.
|
||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||
future unique Event handling
|
||||
"""
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
|
||||
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
|
||||
|
||||
@ -31,31 +31,28 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[UnitType] = field(default_factory=list)
|
||||
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[UnitType] = field(default_factory=list)
|
||||
awacs: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[UnitType] = field(default_factory=list)
|
||||
tankers: List[Type[FlyingType]] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[VehicleType] = field(default_factory=list)
|
||||
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# 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: List[VehicleType] = field(default_factory=list)
|
||||
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[VehicleType] = field(default_factory=list)
|
||||
|
||||
# List of units that can be deployed as SHORAD
|
||||
shorads: List[str] = field(default_factory=list)
|
||||
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
|
||||
|
||||
# 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.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
@ -67,10 +64,10 @@ class Faction:
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# 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
|
||||
helicopter_carrier: List[UnitType] = field(default_factory=list)
|
||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
@ -82,10 +79,10 @@ class Faction:
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[str] = field(default_factory=list)
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# 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
|
||||
navy_group_count: int = field(default=1)
|
||||
@ -97,7 +94,7 @@ class Faction:
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# 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 = field(default=MODERN_DOCTRINE)
|
||||
@ -106,7 +103,17 @@ class Faction:
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# 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
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
@ -137,9 +144,14 @@ class Faction:
|
||||
faction.logistics_units = load_all_vehicles(
|
||||
json.get("logistics_units", []))
|
||||
|
||||
faction.sams = json.get("sams", [])
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
faction.shorads = json.get("shorads", [])
|
||||
|
||||
faction.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.requirements = json.get("requirements", {})
|
||||
|
||||
@ -194,16 +206,19 @@ class Faction:
|
||||
if k is not None:
|
||||
faction.liveries_overrides[k] = [s.lower() for s in v]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def units(self) -> List[UnitType]:
|
||||
def units(self) -> List[Type[UnitType]]:
|
||||
return (self.infantry_units + self.aircrafts + self.awacs +
|
||||
self.artillery_units + self.frontline_units +
|
||||
self.tankers + self.logistics_units)
|
||||
|
||||
|
||||
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
|
||||
:param unit: Unit name as string
|
||||
@ -226,13 +241,13 @@ def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
|
||||
return None
|
||||
|
||||
|
||||
def load_aircraft(name: str) -> Optional[FlyingType]:
|
||||
def load_aircraft(name: str) -> Optional[Type[FlyingType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_aircraft(data) -> List[FlyingType]:
|
||||
def load_all_aircraft(data) -> List[Type[FlyingType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_aircraft(name)
|
||||
@ -241,13 +256,13 @@ def load_all_aircraft(data) -> List[FlyingType]:
|
||||
return items
|
||||
|
||||
|
||||
def load_vehicle(name: str) -> Optional[VehicleType]:
|
||||
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
|
||||
return cast(Optional[FlyingType], unit_loader(
|
||||
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
|
||||
))
|
||||
|
||||
|
||||
def load_all_vehicles(data) -> List[VehicleType]:
|
||||
def load_all_vehicles(data) -> List[Type[VehicleType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_vehicle(name)
|
||||
@ -256,11 +271,11 @@ def load_all_vehicles(data) -> List[VehicleType]:
|
||||
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]))
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[ShipType]:
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
|
||||
239
game/game.py
239
game/game.py
@ -1,14 +1,13 @@
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike, Task
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import db
|
||||
@ -21,15 +20,16 @@ from gen.conflictgen import Conflict
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from . import persistency
|
||||
from .debriefing import Debriefing
|
||||
from .event.event import Event, UnitsDeliveryEvent
|
||||
from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .infos.information import Information
|
||||
from .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
@ -62,17 +62,19 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
|
||||
# cost of AWACS for single operation
|
||||
AWACS_BUDGET_COST = 4
|
||||
|
||||
# Initial budget value
|
||||
PLAYER_BUDGET_INITIAL = 650
|
||||
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
|
||||
class Game:
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings):
|
||||
settings: Settings, player_budget: int,
|
||||
enemy_budget: int) -> None:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
@ -87,10 +89,12 @@ class Game:
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
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.savepath = ""
|
||||
self.budget = PLAYER_BUDGET_INITIAL
|
||||
self.budget = player_budget
|
||||
self.enemy_budget = enemy_budget
|
||||
self.current_unit_id = 0
|
||||
self.current_group_id = 0
|
||||
|
||||
@ -103,9 +107,24 @@ class Game:
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
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:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
@ -148,23 +167,29 @@ class Game:
|
||||
front_line.control_point_b)
|
||||
|
||||
@property
|
||||
def budget_reward_amount(self):
|
||||
reward = 0
|
||||
if len(self.theater.player_points()) > 0:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
reward = reward + REWARDS[g.category]
|
||||
return reward
|
||||
else:
|
||||
return reward
|
||||
def budget_reward_amount(self) -> int:
|
||||
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
|
||||
for cp in self.theater.player_points():
|
||||
for g in cp.ground_objects:
|
||||
if g.category in REWARDS.keys() and not g.is_dead:
|
||||
reward += REWARDS[g.category]
|
||||
return int(reward * self.settings.player_income_multiplier)
|
||||
|
||||
def _budget_player(self):
|
||||
def process_player_income(self):
|
||||
self.budget += self.budget_reward_amount
|
||||
|
||||
def awacs_expense_commit(self):
|
||||
self.budget -= AWACS_BUDGET_COST
|
||||
def process_enemy_income(self):
|
||||
# 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:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
@ -175,20 +200,16 @@ class Game:
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def units_delivery_remove(self, event: Event):
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
|
||||
def initiate_event(self, event: Event):
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
event.generate()
|
||||
return event.generate()
|
||||
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
if event.is_successfull(debriefing):
|
||||
self.budget += event.bonus()
|
||||
self.budget += int(event.bonus() *
|
||||
self.settings.player_income_multiplier)
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@ -199,18 +220,12 @@ class Game:
|
||||
if isinstance(event, Event):
|
||||
return event and event.attacker_name and event.attacker_name == self.player_name
|
||||
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:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
|
||||
# Save game compatibility.
|
||||
|
||||
# TODO: Remove in 2.3.
|
||||
if not hasattr(self, "conditions"):
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
@ -224,8 +239,12 @@ class Game:
|
||||
else:
|
||||
event.skip()
|
||||
|
||||
self._enemy_reinforcement()
|
||||
self._budget_player()
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn()
|
||||
|
||||
self.process_enemy_income()
|
||||
|
||||
self.process_player_income()
|
||||
|
||||
if not no_action and self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
@ -242,6 +261,14 @@ class Game:
|
||||
# Autosave progress
|
||||
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:
|
||||
self.events = []
|
||||
self._generate_events()
|
||||
@ -251,92 +278,56 @@ class Game:
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(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
|
||||
self.__culling_points = self.compute_conflicts_position()
|
||||
self.compute_conflicts_position()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
CoalitionMissionPlanner(self, is_player=True).plan_missions()
|
||||
CoalitionMissionPlanner(self, is_player=False).plan_missions()
|
||||
|
||||
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:
|
||||
if cp.has_frontline:
|
||||
gplanner = GroundPlanner(cp, self)
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def _enemy_reinforcement(self):
|
||||
"""
|
||||
Compute and commision reinforcement for enemy bases
|
||||
"""
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
MAX_ARMOR = 30 * self.settings.multiplier
|
||||
MAX_AIRCRAFT = 25 * self.settings.multiplier
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
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
|
||||
for enemy_point in self.theater.enemy_points():
|
||||
for g in enemy_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production = production + REWARDS[g.category]
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
production = production * 0.75
|
||||
budget_for_armored_units = production / 2
|
||||
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)
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
@ -369,13 +360,19 @@ class Game:
|
||||
|
||||
# By default, use the existing frontline conflict position
|
||||
for front_line in self.theater.conflicts():
|
||||
position = Conflict.frontline_position(self.theater,
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
position = Conflict.frontline_position(front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
self.theater)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.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 len(points) == 0:
|
||||
cpoint = None
|
||||
@ -399,7 +396,7 @@ class Game:
|
||||
if len(points) == 0:
|
||||
points.append(Point(0, 0))
|
||||
|
||||
return points
|
||||
self.__culling_points = points
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
@ -447,4 +444,10 @@ class Game:
|
||||
return "blue"
|
||||
|
||||
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.")
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import datetime
|
||||
|
||||
class Information():
|
||||
|
||||
@ -5,7 +6,12 @@ class Information():
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.turn = turn
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
def __str__(self):
|
||||
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text
|
||||
return s
|
||||
return '[{}][{}] {} {}'.format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
|
||||
self.turn,
|
||||
self.title,
|
||||
self.text
|
||||
)
|
||||
@ -1,11 +1,15 @@
|
||||
"""Inventory management APIs."""
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Iterator, Set, Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
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 theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class ControlPointAircraftInventory:
|
||||
@ -13,9 +17,9 @@ class ControlPointAircraftInventory:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
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.
|
||||
|
||||
Args:
|
||||
@ -24,7 +28,7 @@ class ControlPointAircraftInventory:
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
@ -43,7 +47,7 @@ class ControlPointAircraftInventory:
|
||||
)
|
||||
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.
|
||||
|
||||
Args:
|
||||
@ -55,14 +59,14 @@ class ControlPointAircraftInventory:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def types_available(self) -> Iterator[UnitType]:
|
||||
def types_available(self) -> Iterator[Type[FlyingType]]:
|
||||
"""Iterates over all available aircraft types."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
yield aircraft
|
||||
|
||||
@property
|
||||
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
|
||||
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
|
||||
"""Iterates over all available aircraft types, including amounts."""
|
||||
for aircraft, count in self.inventory.items():
|
||||
if count > 0:
|
||||
@ -102,12 +106,14 @@ class GlobalAircraftInventory:
|
||||
return self.inventories[control_point]
|
||||
|
||||
@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."""
|
||||
seen: Set[UnitType] = set()
|
||||
seen: Set[Type[FlyingType]] = set()
|
||||
for control_point, inventory in self.inventories.items():
|
||||
if control_point.captured:
|
||||
for aircraft in inventory.types_available:
|
||||
if not control_point.can_operate(aircraft):
|
||||
continue
|
||||
if aircraft not in seen:
|
||||
seen.add(aircraft)
|
||||
yield aircraft
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from theater import ControlPoint
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class FrontlineData:
|
||||
|
||||
@ -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()
|
||||
@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
import logging
|
||||
import os
|
||||
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.action import DoScript, DoScriptFile
|
||||
@ -9,11 +12,8 @@ from dcs.coalition import Coalition
|
||||
from dcs.countries import country_dict
|
||||
from dcs.lua.parse import loads
|
||||
from dcs.mapping import Point
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import TriggerStart
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.plugins import LuaPluginManager
|
||||
from gen import Conflict, FlightType, VisualGenerator
|
||||
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
|
||||
@ -29,19 +29,19 @@ from gen.kneeboard import KneeboardGenerator
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from theater import ControlPoint
|
||||
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
from ..theater import Airfield
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class Operation:
|
||||
attackers_starting_position = None # type: db.StartingPosition
|
||||
defenders_starting_position = None # type: db.StartingPosition
|
||||
|
||||
"""Static class for managing the final Mission generation"""
|
||||
current_mission = None # type: Mission
|
||||
regular_mission = None # type: Mission
|
||||
quick_mission = None # type: Mission
|
||||
conflict = None # type: Conflict
|
||||
airgen = None # type: AircraftConflictGenerator
|
||||
triggersgen = None # type: TriggersGenerator
|
||||
airsupportgen = None # type: AirSupportConflictGenerator
|
||||
@ -51,104 +51,96 @@ class Operation:
|
||||
forcedoptionsgen = None # type: ForcedOptionsGenerator
|
||||
radio_registry: Optional[RadioRegistry] = None
|
||||
tacan_registry: Optional[TacanRegistry] = None
|
||||
|
||||
game = None # type: Game
|
||||
environment_settings = None
|
||||
trigger_radius = TRIGGER_RADIUS_MEDIUM
|
||||
is_quick = None
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
player_awacs_enabled = True
|
||||
# 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,
|
||||
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):
|
||||
@classmethod
|
||||
def prepare(cls, game: Game):
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
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
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
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
|
||||
)
|
||||
|
||||
print(self.game.player_country)
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)])
|
||||
print(country_dict[db.country_id_from_name(self.game.player_country)]())
|
||||
@classmethod
|
||||
def _set_mission(cls, mission: Mission) -> None:
|
||||
cls.current_mission = mission
|
||||
|
||||
# Setup coalition :
|
||||
self.current_mission.coalition["blue"] = Coalition("blue")
|
||||
self.current_mission.coalition["red"] = Coalition("red")
|
||||
@classmethod
|
||||
def _setup_mission_coalitions(cls):
|
||||
cls.current_mission.coalition["blue"] = Coalition("blue")
|
||||
cls.current_mission.coalition["red"] = Coalition("red")
|
||||
|
||||
p_country = self.game.player_country
|
||||
e_country = self.game.enemy_country
|
||||
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]())
|
||||
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]())
|
||||
p_country = cls.game.player_country
|
||||
e_country = cls.game.enemy_country
|
||||
cls.current_mission.coalition["blue"].add_country(
|
||||
country_dict[db.country_id_from_name(p_country)]())
|
||||
cls.current_mission.coalition["red"].add_country(
|
||||
country_dict[db.country_id_from_name(e_country)]())
|
||||
|
||||
print([c for c in self.current_mission.coalition["blue"].countries.keys()])
|
||||
print([c for c in self.current_mission.coalition["red"].countries.keys()])
|
||||
|
||||
if is_quick:
|
||||
self.quick_mission = self.current_mission
|
||||
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:
|
||||
@classmethod
|
||||
def inject_lua_trigger(cls, contents: str, comment: str) -> None:
|
||||
trigger = TriggerStart(comment=comment)
|
||||
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:
|
||||
self.plugin_scripts.append(mnemonic)
|
||||
@classmethod
|
||||
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:
|
||||
if script_mnemonic in self.plugin_scripts:
|
||||
if script_mnemonic in cls.plugin_scripts:
|
||||
logging.debug(
|
||||
f"Skipping already loaded {script} for {plugin_mnemonic}"
|
||||
)
|
||||
else:
|
||||
self.plugin_scripts.append(script_mnemonic)
|
||||
cls.plugin_scripts.append(script_mnemonic)
|
||||
|
||||
plugin_path = Path("./resources/plugins", plugin_mnemonic)
|
||||
|
||||
@ -161,23 +153,25 @@ class Operation:
|
||||
|
||||
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
|
||||
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))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
cls.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@classmethod
|
||||
def notify_info_generators(
|
||||
self,
|
||||
cls,
|
||||
groundobjectgen: GroundObjectsGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
):
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
|
||||
"""
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
KneeboardGenerator(self.current_mission, self.game),
|
||||
BriefingGenerator(self.current_mission, self.game)
|
||||
]
|
||||
KneeboardGenerator(cls.current_mission, cls.game),
|
||||
BriefingGenerator(cls.current_mission, cls.game)
|
||||
]
|
||||
for gen in gens:
|
||||
for dynamic_runway in groundobjectgen.runways.values():
|
||||
gen.add_dynamic_runway(dynamic_runway)
|
||||
@ -185,7 +179,7 @@ class Operation:
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
gen.add_tanker(tanker)
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
if cls.player_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
gen.add_awacs(awacs)
|
||||
|
||||
@ -196,301 +190,30 @@ class Operation:
|
||||
gen.add_flight(flight)
|
||||
gen.generate()
|
||||
|
||||
def generate(self):
|
||||
radio_registry = RadioRegistry()
|
||||
tacan_registry = TacanRegistry()
|
||||
@classmethod
|
||||
def create_unit_map(cls) -> None:
|
||||
cls.unit_map = UnitMap()
|
||||
for control_point in cls.game.theater.controlpoints:
|
||||
if isinstance(control_point, Airfield):
|
||||
cls.unit_map.add_airfield(control_point)
|
||||
|
||||
# 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:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
tacan_registry.reserve(beacon.tacan_channel)
|
||||
@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)
|
||||
|
||||
for airfield, data in AIRFIELD_DATA.items():
|
||||
if data.theater == self.game.theater.terrain.name:
|
||||
unique_map_frequencies.add(data.atc.hf)
|
||||
unique_map_frequencies.add(data.atc.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
for frequency in unique_map_frequencies:
|
||||
radio_registry.reserve(frequency)
|
||||
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(self.current_mission,
|
||||
self.game.conditions).generate()
|
||||
|
||||
# Generate ground object first
|
||||
|
||||
groundobjectgen = GroundObjectsGenerator(
|
||||
self.current_mission,
|
||||
self.conflict,
|
||||
self.game,
|
||||
radio_registry,
|
||||
tacan_registry
|
||||
)
|
||||
groundobjectgen.generate()
|
||||
|
||||
# Generate destroyed units
|
||||
for d in self.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
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:
|
||||
self.current_mission.static_group(
|
||||
country=self.current_mission.country(self.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
airsupportgen = AirSupportConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game, radio_registry,
|
||||
tacan_registry)
|
||||
airsupportgen.generate(self.is_awacs_enabled)
|
||||
|
||||
# Generate Activity on the map
|
||||
airgen = AircraftConflictGenerator(
|
||||
self.current_mission, self.conflict, self.game.settings, self.game,
|
||||
radio_registry)
|
||||
|
||||
airgen.generate_flights(
|
||||
self.current_mission.country(self.game.player_country),
|
||||
self.game.blue_ato,
|
||||
groundobjectgen.runways
|
||||
)
|
||||
airgen.generate_flights(
|
||||
self.current_mission.country(self.game.enemy_country),
|
||||
self.game.red_ato,
|
||||
groundobjectgen.runways
|
||||
)
|
||||
|
||||
# Generate ground units on frontline everywhere
|
||||
jtacs: List[JtacInfo] = []
|
||||
for 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
|
||||
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
triggersgen.generate()
|
||||
|
||||
# Options
|
||||
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission,
|
||||
self.conflict, self.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(self.current_mission, self.conflict,
|
||||
self.game)
|
||||
if self.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
luaData = {}
|
||||
luaData["AircraftCarriers"] = {}
|
||||
luaData["Tankers"] = {}
|
||||
luaData["AWACs"] = {}
|
||||
luaData["JTACs"] = {}
|
||||
luaData["TargetPoints"] = {}
|
||||
|
||||
self.assign_channels_to_flights(airgen.flights,
|
||||
airsupportgen.air_support)
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
}
|
||||
|
||||
if self.is_awacs_enabled:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
|
||||
flightType = flight.flight_type.name
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if hasattr(flightTarget, 'obj_name'):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
"""
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
-- list the tankers generated by Liberation
|
||||
dcsLiberation.Tankers = {
|
||||
"""
|
||||
for key in luaData["Tankers"]:
|
||||
data = luaData["Tankers"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
variant = data["variant"]
|
||||
tacan = data["tacan"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the AWACSes
|
||||
lua += """
|
||||
|
||||
-- list the AWACs generated by Liberation
|
||||
dcsLiberation.AWACs = {
|
||||
"""
|
||||
for key in luaData["AWACs"]:
|
||||
data = luaData["AWACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the JTACs
|
||||
lua += """
|
||||
|
||||
-- list the JTACs generated by Liberation
|
||||
dcsLiberation.JTACs = {
|
||||
"""
|
||||
for key in luaData["JTACs"]:
|
||||
data = luaData["JTACs"][key]
|
||||
dcsGroupName= data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
#lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
lua += """
|
||||
|
||||
-- list the target points generated by Liberation
|
||||
dcsLiberation.TargetPoints = {
|
||||
"""
|
||||
for key in luaData["TargetPoints"]:
|
||||
data = luaData["TargetPoints"][key]
|
||||
name = data["name"]
|
||||
pointType = data["type"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
#lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the airbases generated by Liberation
|
||||
-- dcsLiberation.Airbases = {}
|
||||
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
|
||||
"""
|
||||
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
self.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
# Inject 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],
|
||||
@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
|
||||
self.assign_channels_to_flight(flight, air_support)
|
||||
cls.assign_channels_to_flight(flight, air_support)
|
||||
|
||||
def assign_channels_to_flight(self, flight: FlightData,
|
||||
@staticmethod
|
||||
def assign_channels_to_flight(flight: FlightData,
|
||||
air_support: AirSupport) -> None:
|
||||
"""Assigns preset radio channels for a client flight."""
|
||||
airframe = flight.aircraft_type
|
||||
@ -505,3 +228,340 @@ dcsLiberation.TargetPoints = {
|
||||
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)
|
||||
|
||||
for beacon in beacons:
|
||||
unique_map_frequencies.add(beacon.frequency)
|
||||
if beacon.is_tacan:
|
||||
if beacon.channel is None:
|
||||
logging.error(
|
||||
f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
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.vhf_fm)
|
||||
unique_map_frequencies.add(data.atc.vhf_am)
|
||||
unique_map_frequencies.add(data.atc.uhf)
|
||||
# No need to reserve ILS or TACAN because those are in the
|
||||
# beacon list.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
cls.groundobjectgen = GroundObjectsGenerator(
|
||||
cls.current_mission,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map
|
||||
)
|
||||
cls.groundobjectgen.generate()
|
||||
|
||||
@classmethod
|
||||
def _generate_destroyed_units(cls) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
for d in cls.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
pos = Point(d["x"], d["z"])
|
||||
if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units:
|
||||
cls.current_mission.static_group(
|
||||
country=cls.current_mission.country(
|
||||
cls.game.player_country),
|
||||
name="",
|
||||
_type=utype,
|
||||
hidden=True,
|
||||
position=pos,
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
cls.create_unit_map()
|
||||
cls.create_radio_registries()
|
||||
# Set mission time and weather conditions.
|
||||
EnvironmentGenerator(cls.current_mission,
|
||||
cls.game.conditions).generate()
|
||||
cls._generate_ground_units()
|
||||
cls._generate_destroyed_units()
|
||||
cls._generate_air_units()
|
||||
cls.assign_channels_to_flights(cls.airgen.flights,
|
||||
cls.airsupportgen.air_support)
|
||||
cls._generate_ground_conflicts()
|
||||
|
||||
# Triggers
|
||||
triggersgen = TriggersGenerator(cls.current_mission, cls.game)
|
||||
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
|
||||
forcedoptionsgen = ForcedOptionsGenerator(
|
||||
cls.current_mission, cls.game)
|
||||
forcedoptionsgen.generate()
|
||||
|
||||
# Generate Visuals Smoke Effects
|
||||
visualgen = VisualGenerator(cls.current_mission, cls.game)
|
||||
if cls.game.settings.perf_smoke_gen:
|
||||
visualgen.generate()
|
||||
|
||||
cls.generate_lua(cls.airgen, cls.airsupportgen, cls.jtacs)
|
||||
|
||||
# Inject Plugins Lua Scripts and data
|
||||
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:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
"dcsGroupName": tanker.dcsGroupName,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
"radio": tanker.freq.mhz,
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
|
||||
}
|
||||
|
||||
if airsupportgen.air_support.awacs:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.dcsGroupName,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
"dcsGroupName": jtac.dcsGroupName,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code
|
||||
}
|
||||
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.STRIKE]:
|
||||
flightType = str(flight.flight_type)
|
||||
flightTarget = flight.package.target
|
||||
if flightTarget:
|
||||
flightTargetName = None
|
||||
flightTargetType = None
|
||||
if isinstance(flightTarget, TheaterGroundObject):
|
||||
flightTargetName = flightTarget.obj_name
|
||||
flightTargetType = flightType + \
|
||||
f" TGT ({flightTarget.category})"
|
||||
elif hasattr(flightTarget, 'name'):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": {"x": flightTarget.position.x,
|
||||
"y": flightTarget.position.y}
|
||||
}
|
||||
|
||||
# set a LUA table with data from Liberation that we want to set
|
||||
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
|
||||
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
|
||||
state_location = "[[" + os.path.abspath(".") + "]]"
|
||||
lua = """
|
||||
-- setting configuration table
|
||||
env.info("DCSLiberation|: setting configuration table")
|
||||
|
||||
-- all data in this table is overridable.
|
||||
dcsLiberation = {}
|
||||
|
||||
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
|
||||
dcsLiberation.installPath=""" + state_location + """
|
||||
|
||||
"""
|
||||
# Process the tankers
|
||||
lua += """
|
||||
|
||||
-- list the tankers generated by Liberation
|
||||
dcsLiberation.Tankers = {
|
||||
"""
|
||||
for key in luaData["Tankers"]:
|
||||
data = luaData["Tankers"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
variant = data["variant"]
|
||||
tacan = data["tacan"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the AWACSes
|
||||
lua += """
|
||||
|
||||
-- list the AWACs generated by Liberation
|
||||
dcsLiberation.AWACs = {
|
||||
"""
|
||||
for key in luaData["AWACs"]:
|
||||
data = luaData["AWACs"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
radio = data["radio"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the JTACs
|
||||
lua += """
|
||||
|
||||
-- list the JTACs generated by Liberation
|
||||
dcsLiberation.JTACs = {
|
||||
"""
|
||||
for key in luaData["JTACs"]:
|
||||
data = luaData["JTACs"][key]
|
||||
dcsGroupName = data["dcsGroupName"]
|
||||
callsign = data["callsign"]
|
||||
zone = data["zone"]
|
||||
laserCode = data["laserCode"]
|
||||
dcsUnit = data["dcsUnit"]
|
||||
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
|
||||
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
# Process the Target Points
|
||||
lua += """
|
||||
|
||||
-- list the target points generated by Liberation
|
||||
dcsLiberation.TargetPoints = {
|
||||
"""
|
||||
for key in luaData["TargetPoints"]:
|
||||
data = luaData["TargetPoints"][key]
|
||||
name = data["name"]
|
||||
pointType = data["type"]
|
||||
positionX = data["position"]["x"]
|
||||
positionY = data["position"]["y"]
|
||||
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
|
||||
# lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
|
||||
lua += "}"
|
||||
|
||||
lua += """
|
||||
|
||||
-- list the airbases generated by Liberation
|
||||
-- dcsLiberation.Airbases = {}
|
||||
|
||||
-- list the aircraft carriers generated by Liberation
|
||||
-- dcsLiberation.Carriers = {}
|
||||
|
||||
-- later, we'll add more data to the table
|
||||
|
||||
"""
|
||||
|
||||
trigger = TriggerStart(comment="Set DCS Liberation data")
|
||||
trigger.add_action(DoScript(String(lua)))
|
||||
Operation.current_mission.triggerrules.triggers.append(trigger)
|
||||
|
||||
@ -7,45 +7,31 @@ from typing import Optional
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
|
||||
def setup(user_folder: str):
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
global _dcs_saved_game_folder
|
||||
assert _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:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
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:
|
||||
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):
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
|
||||
@ -5,7 +5,7 @@ import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from typing import List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
@ -22,7 +22,7 @@ class LuaPluginWorkOrder:
|
||||
self.mnemonic = mnemonic
|
||||
self.disable = disable
|
||||
|
||||
def work(self, operation: Operation) -> None:
|
||||
def work(self, operation: Type[Operation]) -> None:
|
||||
if self.disable:
|
||||
operation.bypass_plugin_script(self.mnemonic)
|
||||
else:
|
||||
@ -144,11 +144,11 @@ class LuaPlugin(PluginSettings):
|
||||
for option in self.definition.options:
|
||||
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:
|
||||
work_order.work(operation)
|
||||
|
||||
def inject_configuration(self, operation: Operation) -> None:
|
||||
def inject_configuration(self, operation: Type[Operation]) -> None:
|
||||
# inject the plugin options
|
||||
if self.options:
|
||||
option_decls = []
|
||||
|
||||
194
game/procurement.py
Normal file
194
game/procurement.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
def __init__(self):
|
||||
# Generator settings
|
||||
self.inverted = False
|
||||
self.do_not_generate_carrier = False # TODO : implement
|
||||
self.do_not_generate_lha = False # TODO : implement
|
||||
self.do_not_generate_player_navy = True # TODO : implement
|
||||
self.do_not_generate_enemy_navy = True # TODO : implement
|
||||
# Difficulty settings
|
||||
player_skill: str = "Good"
|
||||
enemy_skill: str = "Average"
|
||||
enemy_vehicle_skill: str = "Average"
|
||||
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
|
||||
labels: str = "Full"
|
||||
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
|
||||
self.player_skill = "Good"
|
||||
self.enemy_skill = "Average"
|
||||
self.enemy_vehicle_skill = "Average"
|
||||
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
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
|
||||
# Performance oriented
|
||||
self.perf_red_alert_state = True
|
||||
self.perf_smoke_gen = True
|
||||
self.perf_artillery = True
|
||||
self.perf_moving_units = True
|
||||
self.perf_infantry = True
|
||||
self.perf_ai_parking_start = True
|
||||
self.perf_destroyed_units = True
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
perf_smoke_gen: bool = True
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_ai_parking_start: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
|
||||
# Performance culling
|
||||
self.perf_culling = False
|
||||
self.perf_culling_distance = 100
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
perf_culling_distance: int = 100
|
||||
perf_do_not_cull_carrier = True
|
||||
|
||||
# LUA Plugins system
|
||||
self.plugins: Dict[str, bool] = {}
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
# Cheating
|
||||
self.show_red_ato = False
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
|
||||
self.never_delay_player_flights = False
|
||||
never_delay_player_flights: bool = False
|
||||
|
||||
@staticmethod
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import SamGroundObject
|
||||
@ -4,9 +4,8 @@ import math
|
||||
import typing
|
||||
from typing import Dict, Type
|
||||
|
||||
from dcs.planes import PlaneType
|
||||
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 game import db
|
||||
@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
|
||||
|
||||
|
||||
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):
|
||||
self.aircraft = {}
|
||||
self.armor = {}
|
||||
self.aa = {}
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
|
||||
@property
|
||||
def total_planes(self) -> int:
|
||||
def total_aircraft(self) -> int:
|
||||
return sum(self.aircraft.values())
|
||||
|
||||
@property
|
||||
@ -83,7 +78,7 @@ class Base:
|
||||
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
|
||||
return result
|
||||
|
||||
def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[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)
|
||||
|
||||
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
|
||||
@ -155,7 +150,7 @@ class Base:
|
||||
if task:
|
||||
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
|
||||
else:
|
||||
count = self.total_planes
|
||||
count = self.total_aircraft
|
||||
|
||||
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
|
||||
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
|
||||
@ -167,18 +162,18 @@ class Base:
|
||||
# previous logic removed because we always want the full air defense capabilities.
|
||||
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))
|
||||
|
||||
def scramble_last_defense(self):
|
||||
# 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)
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
def assemble_attack(self) -> typing.Dict[Armor, int]:
|
||||
905
game/theater/conflicttheater.py
Normal file
905
game/theater/conflicttheater.py
Normal 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
|
||||
721
game/theater/controlpoint.py
Normal file
721
game/theater/controlpoint.py
Normal 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
30
game/theater/landmap.py
Normal 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)
|
||||
|
||||
43
game/theater/missiontarget.py
Normal file
43
game/theater/missiontarget.py
Normal 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,
|
||||
]
|
||||
741
game/theater/start_generator.py
Normal file
741
game/theater/start_generator.py
Normal 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()
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
@ -9,6 +9,8 @@ from dcs.unitgroup import Group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
@ -117,11 +119,36 @@ class TheaterGroundObject(MissionTarget):
|
||||
def faction_color(self) -> str:
|
||||
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):
|
||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||
position: Point, heading: int, control_point: ControlPoint,
|
||||
dcs_identifier: str) -> None:
|
||||
dcs_identifier: str, airbase_group=False) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
@ -130,7 +157,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=False,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False
|
||||
)
|
||||
self.object_id = object_id
|
||||
@ -145,7 +172,19 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
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
|
||||
|
||||
|
||||
@ -216,8 +255,8 @@ class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
|
||||
|
||||
# TODO: Differentiate types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the
|
||||
# armor garrisons at airbases. These should each be split into their own types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# be split into their own types.
|
||||
class SamGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(self, name: str, group_id: int, position: Point,
|
||||
control_point: ControlPoint, for_airbase: bool) -> None:
|
||||
@ -245,6 +284,32 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
else:
|
||||
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):
|
||||
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.
|
||||
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,
|
||||
control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
133
game/unitmap.py
Normal file
133
game/unitmap.py
Normal 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)
|
||||
@ -1,14 +1,75 @@
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.2.0"]
|
||||
components = ["2.3.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
|
||||
@ -5,12 +5,14 @@ import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from theater import ConflictTheater
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class TimeOfDay(Enum):
|
||||
|
||||
423
gen/aircraft.py
423
gen/aircraft.py
@ -5,7 +5,7 @@ import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
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.action import AITaskPush, ActivateGroup
|
||||
@ -13,17 +13,22 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
|
||||
from dcs.country import Country
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
from dcs.helicopters import UH_1H, helicopter_map
|
||||
from dcs.mapping import Point
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
B_17G,
|
||||
B_52H,
|
||||
Bf_109K_4,
|
||||
C_101EB,
|
||||
C_101CC,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_14B,
|
||||
I_16,
|
||||
JF_17,
|
||||
Ju_88A4,
|
||||
PlaneType,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
@ -31,34 +36,37 @@ from dcs.planes import (
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_33, A_20G, Tu_22M3, B_52H,
|
||||
Su_33,
|
||||
Tu_22M3,
|
||||
)
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AntishipStrike,
|
||||
AttackGroup,
|
||||
Bombing,
|
||||
BombingRunway,
|
||||
CAP,
|
||||
CAS,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
EngageTargets,
|
||||
EngageTargetsInZone,
|
||||
FighterSweep,
|
||||
GroundAttack,
|
||||
OptROE,
|
||||
OptRTBOnBingoFuel,
|
||||
OptRTBOnOutOfAmmo,
|
||||
OptReactOnThreat,
|
||||
OptRestrictAfterburner,
|
||||
OptRestrictJettison,
|
||||
OrbitAction,
|
||||
PinpointStrike,
|
||||
RunwayAttack,
|
||||
SEAD,
|
||||
StartCommand,
|
||||
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.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
@ -66,11 +74,22 @@ from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.factions.faction import Faction
|
||||
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.ato import AirTaskingOrder, Package
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
from gen.conflictgen import FRONTLINE_LENGTH
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
@ -79,17 +98,14 @@ from gen.flights.flight import (
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from gen.conflictgen import FRONTLINE_LENGTH
|
||||
from dcs.mapping import Point
|
||||
from theater import TheaterGroundObject
|
||||
from theater.controlpoint import ControlPoint, ControlPointType
|
||||
from .conflictgen import Conflict
|
||||
from .flights.flightplan import (
|
||||
CasFlightPlan,
|
||||
FormationFlightPlan,
|
||||
LoiterFlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
SweepFlightPlan,
|
||||
)
|
||||
from .flights.traveltime import TotEstimator
|
||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||
from .naming import namegen
|
||||
from .runways import RunwayAssigner
|
||||
|
||||
@ -281,12 +297,19 @@ class FlightData:
|
||||
#: Map of radio frequencies to their assigned radio and channel, if any.
|
||||
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,
|
||||
units: List[FlyingUnit], size: int, friendly: bool,
|
||||
departure_delay: timedelta, departure: RunwayData,
|
||||
arrival: RunwayData, divert: Optional[RunwayData],
|
||||
waypoints: List[FlightWaypoint],
|
||||
intra_flight_channel: RadioFrequency) -> None:
|
||||
intra_flight_channel: RadioFrequency,
|
||||
bingo_fuel: Optional[int],
|
||||
joker_fuel: Optional[int]) -> None:
|
||||
self.package = package
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
@ -299,6 +322,8 @@ class FlightData:
|
||||
self.waypoints = waypoints
|
||||
self.intra_flight_channel = intra_flight_channel
|
||||
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])
|
||||
|
||||
@property
|
||||
@ -640,13 +665,13 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
|
||||
|
||||
|
||||
class AircraftConflictGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
|
||||
game: Game, radio_registry: RadioRegistry):
|
||||
def __init__(self, mission: Mission, settings: Settings, game: Game,
|
||||
radio_registry: RadioRegistry, unit_map: UnitMap) -> None:
|
||||
self.m = mission
|
||||
self.game = game
|
||||
self.settings = settings
|
||||
self.conflict = conflict
|
||||
self.radio_registry = radio_registry
|
||||
self.unit_map = unit_map
|
||||
self.flights: List[FlightData] = []
|
||||
|
||||
@cached_property
|
||||
@ -739,25 +764,15 @@ class AircraftConflictGenerator:
|
||||
if unit_type is F_14B:
|
||||
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
|
||||
|
||||
|
||||
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
|
||||
|
||||
channel = self.get_intra_flight_channel(unit_type)
|
||||
group.set_frequency(channel.mhz)
|
||||
|
||||
# TODO: Support for different departure/arrival airfields.
|
||||
cp = flight.from_cp
|
||||
fallback_runway = RunwayData(cp.full_name, runway_heading=0,
|
||||
runway_name="")
|
||||
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
|
||||
divert = None
|
||||
if flight.divert is not None:
|
||||
divert = flight.divert.active_runway(self.game.conditions,
|
||||
dynamic_runways)
|
||||
|
||||
self.flights.append(FlightData(
|
||||
package=package,
|
||||
@ -767,26 +782,25 @@ class AircraftConflictGenerator:
|
||||
friendly=flight.from_cp.captured,
|
||||
# Set later.
|
||||
departure_delay=timedelta(),
|
||||
departure=departure_runway,
|
||||
arrival=departure_runway,
|
||||
# TODO: Support for divert airfields.
|
||||
divert=None,
|
||||
departure=flight.departure.active_runway(self.game.conditions,
|
||||
dynamic_runways),
|
||||
arrival=flight.arrival.active_runway(self.game.conditions,
|
||||
dynamic_runways),
|
||||
divert=divert,
|
||||
# Waypoints are added later, after they've had their TOTs set.
|
||||
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
|
||||
if unit_type is Su_33:
|
||||
if flight.flight_type is not CAP:
|
||||
for unit in group.units:
|
||||
unit.fuel = Su_33.fuel_max / 2.2
|
||||
else:
|
||||
for unit in group.units:
|
||||
unit.fuel = Su_33.fuel_max * 0.8
|
||||
# Special case so Su 33 and C101 can take off
|
||||
if unit_type in [Su_33, C_101EB, C_101CC]:
|
||||
self.set_reduced_fuel(flight, group, unit_type)
|
||||
|
||||
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:
|
||||
assert count > 0
|
||||
|
||||
@ -801,35 +815,42 @@ class AircraftConflictGenerator:
|
||||
group_size=count,
|
||||
parking_slots=None)
|
||||
|
||||
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
|
||||
assert count > 0
|
||||
def _generate_inflight(self, name: str, side: Country, flight: Flight,
|
||||
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
|
||||
speed = WARM_START_HELI_AIRSPEED
|
||||
else:
|
||||
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))
|
||||
|
||||
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(
|
||||
country=side,
|
||||
name=name,
|
||||
aircraft_type=unit_type,
|
||||
aircraft_type=flight.unit_type,
|
||||
airport=None,
|
||||
position=pos,
|
||||
altitude=alt,
|
||||
speed=speed,
|
||||
maintask=None,
|
||||
group_size=count)
|
||||
group_size=flight.count)
|
||||
|
||||
group.points[0].alt_type = "RADIO"
|
||||
group.points[0].alt_type = alt_type
|
||||
return group
|
||||
|
||||
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:
|
||||
assert count > 0
|
||||
|
||||
@ -875,7 +896,6 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def _setup_custom_payload(self, flight, group:FlyingGroup):
|
||||
if flight.use_custom_loadout:
|
||||
|
||||
@ -895,13 +915,11 @@ class AircraftConflictGenerator:
|
||||
|
||||
def clear_parking_slots(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.airport is not None:
|
||||
for parking_slot in cp.airport.parking_slots:
|
||||
parking_slot.unit_id = None
|
||||
for parking_slot in cp.parking_slots:
|
||||
parking_slot.unit_id = None
|
||||
|
||||
def generate_flights(self, country, ato: AirTaskingOrder,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
self.clear_parking_slots()
|
||||
|
||||
for package in ato.packages:
|
||||
if not package.flights:
|
||||
@ -914,9 +932,59 @@ class AircraftConflictGenerator:
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
self.setup_flight_group(group, package, flight, dynamic_runways)
|
||||
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,
|
||||
delay: timedelta) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
@ -971,10 +1039,9 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
at=cp.position)
|
||||
elif cp.is_fleet:
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
elif isinstance(cp, NavalControlPoint):
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
@ -984,8 +1051,12 @@ class AircraftConflictGenerator:
|
||||
start_type=flight.start_type,
|
||||
at=self.m.find_group(group_name))
|
||||
else:
|
||||
if not isinstance(cp, Airfield):
|
||||
raise RuntimeError(
|
||||
f"Attempted to spawn at airfield for non-airfield {cp}")
|
||||
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,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
@ -999,13 +1070,26 @@ class AircraftConflictGenerator:
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
at=cp.position)
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
group.points[0].alt = 1500
|
||||
|
||||
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
|
||||
def configure_behavior(
|
||||
group: FlyingGroup,
|
||||
@ -1046,8 +1130,18 @@ class AircraftConflictGenerator:
|
||||
|
||||
self.configure_behavior(group, rtb_winchester=ammo_type)
|
||||
|
||||
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||
targets=[Targets.All.Air]))
|
||||
def configure_sweep(self, group: FlyingGroup, package: Package,
|
||||
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,
|
||||
flight: Flight,
|
||||
@ -1108,6 +1202,28 @@ class AircraftConflictGenerator:
|
||||
roe=OptROE.Values.OpenFire,
|
||||
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,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData]) -> None:
|
||||
@ -1121,7 +1237,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_unknown_task(self, group: FlyingGroup,
|
||||
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)
|
||||
|
||||
def setup_flight_group(self, group: FlyingGroup, package: Package,
|
||||
@ -1131,18 +1247,25 @@ class AircraftConflictGenerator:
|
||||
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
|
||||
FlightType.INTERCEPTION]:
|
||||
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]:
|
||||
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)
|
||||
elif flight_type in [FlightType.SEAD, ]:
|
||||
elif flight_type == FlightType.SEAD:
|
||||
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)
|
||||
elif flight_type in [FlightType.ANTISHIP]:
|
||||
elif flight_type == FlightType.ANTISHIP:
|
||||
self.configure_anti_ship(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.ESCORT:
|
||||
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:
|
||||
self.configure_unknown_task(group, flight)
|
||||
|
||||
@ -1164,10 +1287,10 @@ class AircraftConflictGenerator:
|
||||
if point.only_for_player and not flight.client_count:
|
||||
continue
|
||||
filtered_points.append(point)
|
||||
# Only add 1 target waypoint for Viggens. This only affects player flights,
|
||||
# Only add 1 target waypoint for Viggens. This only affects player flights,
|
||||
# the Viggen can't have more than 9 waypoints which leaves us with two target point
|
||||
# under the current flight plans.
|
||||
# TODO: Make this smarter, it currently selects a random unit in the group for target,
|
||||
# TODO: Make this smarter, it currently selects a random unit in the group for target,
|
||||
# this could be updated to make it pick the "best" two targets in the group.
|
||||
if flight.unit_type is AJS37 and flight.client_count:
|
||||
viggen_target_points = [
|
||||
@ -1180,7 +1303,7 @@ class AircraftConflictGenerator:
|
||||
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
for idx, point in enumerate(filtered_points):
|
||||
PydcsWaypointBuilder.for_waypoint(
|
||||
point, group, package, flight, self.m
|
||||
@ -1258,10 +1381,13 @@ class PydcsWaypointBuilder:
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
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.name = String(self.waypoint.name)
|
||||
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
if tot is not None:
|
||||
self.set_waypoint_tot(waypoint, tot)
|
||||
@ -1279,13 +1405,18 @@ class PydcsWaypointBuilder:
|
||||
package: Package, flight: Flight,
|
||||
mission: Mission) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
|
||||
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
|
||||
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
|
||||
FlightWaypointType.INGRESS_OCA_AIRCRAFT: OcaAircraftIngressBuilder,
|
||||
FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder,
|
||||
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
|
||||
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
|
||||
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
|
||||
FlightWaypointType.JOIN: JoinPointBuilder,
|
||||
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
|
||||
FlightWaypointType.LOITER: HoldPointBuilder,
|
||||
FlightWaypointType.PATROL: RaceTrackEndBuilder,
|
||||
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
@ -1296,7 +1427,7 @@ class PydcsWaypointBuilder:
|
||||
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
|
||||
"""
|
||||
if (
|
||||
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
|
||||
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
|
||||
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
|
||||
):
|
||||
return True
|
||||
@ -1323,7 +1454,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
altitude=waypoint.alt,
|
||||
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__
|
||||
logging.error(
|
||||
f"Cannot configure hold for for {self.flight} because "
|
||||
@ -1338,6 +1469,32 @@ class HoldPointBuilder(PydcsWaypointBuilder):
|
||||
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):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
@ -1371,14 +1528,16 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
|
||||
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
|
||||
task = AttackGroup(tgroup.id)
|
||||
# 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.Guided)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["weaponType"] = 268402702 # Guided Weapons
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
else:
|
||||
@ -1387,14 +1546,59 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
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):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
|
||||
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
|
||||
# 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:
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=tgroup.position,
|
||||
radius=nm_to_meter(30),
|
||||
@ -1467,6 +1671,24 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
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):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
@ -1541,4 +1763,29 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
racetrack.stop_after_time(
|
||||
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
|
||||
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
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type
|
||||
|
||||
@ -67,7 +68,7 @@ class AirSupportConflictGenerator:
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
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
|
||||
|
||||
fallback_tanker_number = 0
|
||||
@ -120,26 +121,28 @@ class AirSupportConflictGenerator:
|
||||
|
||||
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
|
||||
|
||||
if is_awacs_enabled:
|
||||
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)
|
||||
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
if len(possible_awacs) > 0:
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
|
||||
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)
|
||||
|
||||
self.air_support.awacs.append(AwacsInfo(
|
||||
str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
|
||||
except:
|
||||
print("No AWACS for faction")
|
||||
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")
|
||||
617
gen/armor.py
617
gen/armor.py
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import AITaskPush
|
||||
@ -10,31 +12,28 @@ from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import MQ_9_Reaper
|
||||
from dcs.point import PointAction
|
||||
from dcs.task import (
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
Hold,
|
||||
OrbitAction,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
|
||||
GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
|
||||
SetInvisibleCommand)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from .naming import namegen
|
||||
from gen.ground_forces.ai_ground_planner import (
|
||||
CombatGroupRole,
|
||||
DISTANCE_FROM_FRONTLINE,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import heading_sum, opposite_heading
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
|
||||
CombatGroup, CombatGroupRole)
|
||||
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
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_SIZE_FACTOR = 0.1
|
||||
@ -65,79 +64,87 @@ class JtacInfo:
|
||||
|
||||
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.conflict = conflict
|
||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||
self.player_planned_combat_groups = player_planned_combat_groups
|
||||
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.unit_map = unit_map
|
||||
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(
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||
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):
|
||||
|
||||
player_groups = []
|
||||
enemy_groups = []
|
||||
|
||||
combat_width = self.conflict.distance/2
|
||||
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)
|
||||
position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
|
||||
frontline_vector = Conflict.frontline_vector(
|
||||
self.conflict.from_cp,
|
||||
self.conflict.to_cp,
|
||||
self.game.theater
|
||||
)
|
||||
|
||||
# Create player groups at random position
|
||||
for group in self.player_planned_combat_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, 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)
|
||||
player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
|
||||
|
||||
# Create enemy groups at random position
|
||||
for group in self.enemy_planned_combat_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, 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)
|
||||
enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
|
||||
|
||||
# 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.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
|
||||
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.enemy_stance,
|
||||
enemy_groups,
|
||||
player_groups,
|
||||
self.conflict.heading - 90,
|
||||
self.conflict.to_cp,
|
||||
self.conflict.from_cp
|
||||
)
|
||||
|
||||
# Add JTAC
|
||||
if self.game.player_faction.has_jtac:
|
||||
@ -162,11 +169,13 @@ class GroundConflictGenerator:
|
||||
callsign = callsign_for_support_unit(jtac)
|
||||
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):
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
return
|
||||
def gen_infantry_group_for_group(
|
||||
self,
|
||||
group: VehicleGroup,
|
||||
is_player: bool,
|
||||
side: Country,
|
||||
forward_heading: int
|
||||
) -> None:
|
||||
|
||||
infantry_position = group.points[0].position.random_point_within(250, 50)
|
||||
|
||||
@ -180,7 +189,24 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
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:
|
||||
return
|
||||
|
||||
@ -204,125 +230,191 @@ class GroundConflictGenerator:
|
||||
heading=forward_heading,
|
||||
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:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
|
||||
if hasattr(group.units[0], 'eplrs'):
|
||||
if group.units[0].eplrs:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
# Fire on any ennemy in range
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
|
||||
if target is not None:
|
||||
|
||||
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)
|
||||
self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
|
||||
|
||||
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
|
||||
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.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)
|
||||
self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
|
||||
|
||||
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
|
||||
|
||||
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)
|
||||
self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
|
||||
|
||||
if stance == CombatStance.RETREAT:
|
||||
# In retreat mode, the units will fall back
|
||||
@ -332,11 +424,10 @@ class GroundConflictGenerator:
|
||||
else:
|
||||
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
|
||||
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
|
||||
dcs_group.add_waypoint(retreat_point, PointAction.OnRoad)
|
||||
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
|
||||
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
|
||||
|
||||
|
||||
def add_morale_trigger(self, dcs_group, forward_heading):
|
||||
def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None:
|
||||
"""
|
||||
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
|
||||
|
||||
# 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 = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points)))
|
||||
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
||||
fallback.enabled = False
|
||||
dcs_group.add_trigger_action(Hold())
|
||||
dcs_group.add_trigger_action(fallback)
|
||||
@ -372,8 +466,12 @@ class GroundConflictGenerator:
|
||||
|
||||
self.mission.triggerrules.triggers.append(fallback)
|
||||
|
||||
|
||||
def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE):
|
||||
@staticmethod
|
||||
def find_retreat_point(
|
||||
dcs_group: VehicleGroup,
|
||||
frontline_heading: int,
|
||||
distance: int = RETREAT_DISTANCE
|
||||
) -> Point:
|
||||
"""
|
||||
Find a point to retreat to
|
||||
: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)
|
||||
|
||||
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
|
||||
: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)
|
||||
|
||||
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 enemy_groups Potential enemy groups
|
||||
@param n number of nearby groups to take
|
||||
"""
|
||||
targets = []
|
||||
sorted_list = sorted(enemy_groups, key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position))
|
||||
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)
|
||||
)
|
||||
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:
|
||||
break
|
||||
else:
|
||||
targets.append(sorted_list[i][0])
|
||||
return targets
|
||||
|
||||
|
||||
def find_nearest_enemy_group(self, player_group, enemy_groups):
|
||||
@staticmethod
|
||||
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
|
||||
@param group Group for which we should find the nearest ennemy
|
||||
@ -417,29 +532,33 @@ class GroundConflictGenerator:
|
||||
"""
|
||||
min_distance = 99999999
|
||||
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)
|
||||
if dist < min_distance:
|
||||
min_distance = dist
|
||||
target = dcs_group
|
||||
return target
|
||||
|
||||
|
||||
def get_artillery_target_in_range(self, dcs_group, group, enemy_groups):
|
||||
@staticmethod
|
||||
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
|
||||
"""
|
||||
# TODO: Update to return a list of groups instead of a single point
|
||||
rng = group.units[0].threat_range
|
||||
if len(enemy_groups) == 0:
|
||||
if not enemy_groups:
|
||||
return None
|
||||
for o in range(10):
|
||||
for _ in range(10):
|
||||
potential_target = random.choice(enemy_groups)[0]
|
||||
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
|
||||
if distance_to_target < rng:
|
||||
return potential_target.points[0].position
|
||||
return None
|
||||
|
||||
|
||||
def get_artilery_group_distance_from_frontline(self, group):
|
||||
"""
|
||||
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
|
||||
return rg
|
||||
|
||||
|
||||
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
|
||||
def get_valid_position_for_group(
|
||||
self,
|
||||
conflict_position: Point,
|
||||
combat_width: int,
|
||||
distance_from_frontline: int,
|
||||
heading: int,
|
||||
spawn_heading: int
|
||||
):
|
||||
i = 0
|
||||
while i < 25: # 25 attempt for valid position
|
||||
heading_diff = -90 if isplayer else 90
|
||||
shifted = conflict_position[0].point_from_heading(self.conflict.heading,
|
||||
random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
|
||||
final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
|
||||
while i < 1000:
|
||||
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
|
||||
final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline)
|
||||
|
||||
if self.conflict.theater.is_on_land(final_position):
|
||||
return final_position
|
||||
else:
|
||||
i = i + 1
|
||||
continue
|
||||
i += 1
|
||||
continue
|
||||
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:
|
||||
cp = self.conflict.from_cp
|
||||
@ -478,13 +659,15 @@ class GroundConflictGenerator:
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
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,
|
||||
heading=heading,
|
||||
move_formation=move_formation)
|
||||
|
||||
self.unit_map.add_front_line_units(group, cp)
|
||||
|
||||
for c in range(count):
|
||||
vehicle: Vehicle = group.units[c]
|
||||
vehicle.player_can_drive = True
|
||||
|
||||
return group
|
||||
return group
|
||||
|
||||
18
gen/ato.py
18
gen/ato.py
@ -16,7 +16,7 @@ from typing import Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
from theater.missiontarget import MissionTarget
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
|
||||
@ -147,19 +147,14 @@ class Package:
|
||||
FlightType.CAS,
|
||||
FlightType.STRIKE,
|
||||
FlightType.ANTISHIP,
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
FlightType.BAI,
|
||||
FlightType.EVAC,
|
||||
FlightType.TROOP_TRANSPORT,
|
||||
FlightType.RECON,
|
||||
FlightType.ELINT,
|
||||
FlightType.DEAD,
|
||||
FlightType.SEAD,
|
||||
FlightType.LOGISTICS,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
FlightType.CAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.EWAR,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
for task in task_priorities:
|
||||
@ -178,7 +173,10 @@ class Package:
|
||||
task = self.primary_task
|
||||
if task is None:
|
||||
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:
|
||||
# TODO: Far from perfect. Number packages?
|
||||
|
||||
@ -2,19 +2,20 @@
|
||||
Briefing generation logic
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from theater.frontline import FrontLine
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from datetime import timedelta
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mission import Mission
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft import FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .armor import JtacInfo
|
||||
from theater import ControlPoint
|
||||
from .flights.flight import FlightWaypoint
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from .radios import RadioFrequency
|
||||
from .runways import RunwayData
|
||||
@ -119,6 +120,16 @@ class MissionInfoGenerator:
|
||||
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):
|
||||
|
||||
def __init__(self, mission: Mission, game: Game):
|
||||
@ -134,6 +145,7 @@ class BriefingGenerator(MissionInfoGenerator):
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
env.filters["waypoint_timing"] = format_waypoint_time
|
||||
self.template = env.get_template("briefingtemplate_EN.j2")
|
||||
|
||||
def generate(self) -> None:
|
||||
|
||||
@ -1,58 +1,16 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from dcs.country import Country
|
||||
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_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:
|
||||
def __init__(self,
|
||||
@ -64,12 +22,9 @@ class Conflict:
|
||||
attackers_country: Country,
|
||||
defenders_country: Country,
|
||||
position: Point,
|
||||
heading=None,
|
||||
distance=None,
|
||||
ground_attackers_location: Point = None,
|
||||
ground_defenders_location: Point = None,
|
||||
air_attackers_location: Point = None,
|
||||
air_defenders_location: Point = None):
|
||||
heading: Optional[int] = None,
|
||||
size: Optional[int] = None
|
||||
):
|
||||
|
||||
self.attackers_side = attackers_side
|
||||
self.defenders_side = defenders_side
|
||||
@ -81,307 +36,39 @@ class Conflict:
|
||||
self.theater = theater
|
||||
self.position = position
|
||||
self.heading = heading
|
||||
self.distance = distance
|
||||
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)
|
||||
self.size = size
|
||||
|
||||
@classmethod
|
||||
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
|
||||
return from_cp.has_frontline and to_cp.has_frontline
|
||||
|
||||
@classmethod
|
||||
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
|
||||
attack_heading = from_cp.position.heading_between_point(to_cp.position)
|
||||
attack_distance = from_cp.position.distance_to_point(to_cp.position)
|
||||
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
|
||||
|
||||
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)
|
||||
|
||||
def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
|
||||
frontline = FrontLine(from_cp, to_cp, theater)
|
||||
attack_heading = frontline.attack_heading
|
||||
position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
|
||||
return position, opposite_heading(attack_heading)
|
||||
|
||||
@classmethod
|
||||
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
|
||||
"""
|
||||
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
|
||||
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
|
||||
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
|
||||
Returns a vector for a valid frontline location avoiding exclusion zones.
|
||||
"""
|
||||
frontline = cls.frontline_position(theater, from_cp, to_cp)
|
||||
center_position, heading = frontline
|
||||
left_position, right_position = None, None
|
||||
|
||||
if not theater.is_on_land(center_position):
|
||||
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater)
|
||||
if pos:
|
||||
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),
|
||||
)
|
||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||
left_heading = heading_sum(heading, -90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
|
||||
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
|
||||
distance = int(left_position.distance_to_point(right_position))
|
||||
return left_position, right_heading, distance
|
||||
|
||||
@classmethod
|
||||
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
|
||||
return cls(
|
||||
conflict = cls(
|
||||
position=position,
|
||||
heading=heading,
|
||||
distance=distance,
|
||||
theater=theater,
|
||||
from_cp=from_cp,
|
||||
to_cp=to_cp,
|
||||
@ -389,114 +76,30 @@ class Conflict:
|
||||
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, AIR_DISTANCE),
|
||||
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
|
||||
size=distance
|
||||
)
|
||||
return conflict
|
||||
|
||||
@classmethod
|
||||
def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
assert cls.has_frontline_between(from_cp, to_cp)
|
||||
|
||||
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
|
||||
attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
|
||||
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
|
||||
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
|
||||
|
||||
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,
|
||||
)
|
||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||
pos = initial
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if not theater.is_on_land(pos):
|
||||
return initial.point_from_heading(heading, distance - 100)
|
||||
return pos
|
||||
|
||||
@classmethod
|
||||
def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
|
||||
position = to_cp.position
|
||||
attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
|
||||
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
|
||||
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=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
|
||||
)
|
||||
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
"""Finds the nearest valid ground position along a provided heading and it's inverse"""
|
||||
pos = initial
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
return initial
|
||||
|
||||
@ -14,7 +14,7 @@ from dcs.ships import (
|
||||
from game.factions.faction import Faction
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
@ -38,8 +38,8 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
if include_dd:
|
||||
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, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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 dcs.unittype import ShipType
|
||||
|
||||
@ -16,7 +16,7 @@ from dcs.ships import (
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -42,8 +42,8 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
if include_dd:
|
||||
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, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])
|
||||
|
||||
@ -5,25 +5,53 @@ import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
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.data.radar_db import UNITS_WITH_RADAR
|
||||
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 gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
ANTISHIP_CAPABLE,
|
||||
ANTISHIP_PREFERRED,
|
||||
CAP_CAPABLE,
|
||||
CAP_PREFERRED,
|
||||
CAS_CAPABLE,
|
||||
CAS_PREFERRED,
|
||||
RUNWAY_ATTACK_CAPABLE,
|
||||
RUNWAY_ATTACK_PREFERRED,
|
||||
SEAD_CAPABLE,
|
||||
SEAD_PREFERRED,
|
||||
STRIKE_CAPABLE,
|
||||
STRIKE_PREFERRED,
|
||||
STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task,
|
||||
)
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
@ -35,15 +63,7 @@ from gen.flights.flight import (
|
||||
)
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from theater import (
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
TheaterGroundObject,
|
||||
SamGroundObject,
|
||||
)
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
@ -68,7 +88,7 @@ class ProposedFlight:
|
||||
max_distance: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.task.name} {self.num_aircraft} ship"
|
||||
return f"{self.task} {self.num_aircraft} ship"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -103,7 +123,7 @@ class AircraftAllocator:
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, UnitType]]:
|
||||
) -> Optional[Tuple[ControlPoint, FlyingType]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
@ -123,50 +143,17 @@ class AircraftAllocator:
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
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:
|
||||
return result
|
||||
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(
|
||||
self, flight: ProposedFlight, types: List[Type[FlyingType]],
|
||||
) -> Optional[Tuple[ControlPoint, UnitType]]:
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
flight.max_distance
|
||||
)
|
||||
@ -175,6 +162,8 @@ class AircraftAllocator:
|
||||
continue
|
||||
inventory = self.global_inventory.for_control_point(airfield)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
if aircraft in types and available >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, aircraft
|
||||
@ -190,6 +179,8 @@ class PackageBuilder:
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
start_type: str) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
@ -208,11 +199,32 @@ class PackageBuilder:
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
|
||||
plan.task, self.start_type)
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
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)
|
||||
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:
|
||||
"""Returns the built package."""
|
||||
return self.package
|
||||
@ -243,7 +255,9 @@ class ObjectiveFinder:
|
||||
found_targets: Set[str] = set()
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if not isinstance(ground_object, SamGroundObject):
|
||||
is_ewr = isinstance(ground_object, EwrGroundObject)
|
||||
is_sam = isinstance(ground_object, SamGroundObject)
|
||||
if not is_ewr and not is_sam:
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
@ -262,22 +276,66 @@ class ObjectiveFinder:
|
||||
yield ground_object
|
||||
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.
|
||||
|
||||
SAM sites are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
sams: List[Tuple[TheaterGroundObject, int]] = []
|
||||
for sam in self.enemy_sams():
|
||||
return self._targets_by_range(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] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(sam.distance_to(cp))
|
||||
sams.append((sam, min(ranges)))
|
||||
ranges.append(target.distance_to(cp))
|
||||
target_ranges.append((target, min(ranges)))
|
||||
|
||||
sams = sorted(sams, key=operator.itemgetter(1))
|
||||
for sam, _range in sams:
|
||||
yield sam
|
||||
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
def strike_targets(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over enemy strike targets.
|
||||
@ -286,11 +344,17 @@ class ObjectiveFinder:
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
targets: List[Tuple[TheaterGroundObject, int]] = []
|
||||
# Control points might have the same ground object several times, for
|
||||
# some reason.
|
||||
# Building objectives are made of several individual TGOs (one per
|
||||
# building).
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
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:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
@ -321,7 +385,7 @@ class ObjectiveFinder:
|
||||
continue
|
||||
|
||||
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]:
|
||||
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||
@ -330,6 +394,9 @@ class ObjectiveFinder:
|
||||
CP.
|
||||
"""
|
||||
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_threat_range = airfields_in_proximity.airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
@ -339,6 +406,15 @@ class ObjectiveFinder:
|
||||
yield cp
|
||||
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]:
|
||||
"""Iterates over all friendly control points."""
|
||||
return (c for c in self.game.theater.controlpoints if
|
||||
@ -393,6 +469,9 @@ class CoalitionMissionPlanner:
|
||||
# TODO: Merge into doctrine, also limit by aircraft.
|
||||
MAX_CAP_RANGE = nm_to_meter(100)
|
||||
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_STRIKE_RANGE = nm_to_meter(150)
|
||||
|
||||
@ -401,6 +480,7 @@ class CoalitionMissionPlanner:
|
||||
self.is_player = is_player
|
||||
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
|
||||
self.ato = self.game.blue_ato if is_player else self.game.red_ato
|
||||
self.procurement_requests: List[AircraftProcurementRequest] = []
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
@ -410,7 +490,7 @@ class CoalitionMissionPlanner:
|
||||
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():
|
||||
yield ProposedMission(front_line, [
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||
@ -428,6 +508,29 @@ class CoalitionMissionPlanner:
|
||||
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.
|
||||
for target in self.objective_finder.strike_targets():
|
||||
yield ProposedMission(target, [
|
||||
@ -470,6 +573,12 @@ class CoalitionMissionPlanner:
|
||||
for proposed_flight in mission.flights:
|
||||
if not builder.plan_flight(proposed_flight):
|
||||
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:
|
||||
missing_types_str = ", ".join(
|
||||
@ -496,7 +605,11 @@ class CoalitionMissionPlanner:
|
||||
error = random.randint(-margin, margin)
|
||||
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
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import logging
|
||||
from typing import List, Type
|
||||
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
AH_64A,
|
||||
@ -36,7 +39,6 @@ from dcs.planes import (
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
F_A_18C,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
@ -79,19 +81,24 @@ from dcs.planes import (
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
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.f22a.f22a import F_22A
|
||||
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.
|
||||
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
|
||||
# factions that also have F-4s should not.
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
# Interceptor are the aircraft prioritized for interception tasks
|
||||
# If none is available, the AI will use regular CAP-capable aircraft instead
|
||||
INTERCEPT_CAPABLE = [
|
||||
MiG_21Bis,
|
||||
MiG_25PD,
|
||||
@ -100,7 +107,11 @@ INTERCEPT_CAPABLE = [
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
|
||||
JF_17,
|
||||
J_11A,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
@ -108,6 +119,9 @@ INTERCEPT_CAPABLE = [
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
]
|
||||
|
||||
@ -144,6 +158,7 @@ CAP_CAPABLE = [
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_22A,
|
||||
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
@ -154,6 +169,8 @@ CAP_CAPABLE = [
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
|
||||
I_16,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
@ -170,14 +187,13 @@ CAP_PREFERRED = [
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
MiG_31,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
@ -189,6 +205,8 @@ CAP_PREFERRED = [
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16C_50,
|
||||
F_22A,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
@ -196,6 +214,8 @@ CAP_PREFERRED = [
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
@ -217,6 +237,7 @@ CAS_CAPABLE = [
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
@ -230,14 +251,11 @@ CAS_CAPABLE = [
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
B_1B,
|
||||
F_15E,
|
||||
F_22A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
@ -272,12 +290,15 @@ CAS_CAPABLE = [
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
@ -291,17 +312,14 @@ CAS_PREFERRED = [
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_15E,
|
||||
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
@ -317,9 +335,6 @@ CAS_PREFERRED = [
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
@ -328,9 +343,11 @@ CAS_PREFERRED = [
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
I_16,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
@ -341,7 +358,7 @@ CAS_PREFERRED = [
|
||||
SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
@ -358,18 +375,26 @@ SEAD_CAPABLE = [
|
||||
Tornado_GR4,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
SEAD_PREFERRED = [
|
||||
F_4E,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Tornado_IDS,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Su_24M,
|
||||
]
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
MiG_21Bis,
|
||||
MiG_27K,
|
||||
MB_339PAN,
|
||||
|
||||
@ -378,7 +403,15 @@ STRIKE_CAPABLE = [
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
@ -388,13 +421,13 @@ STRIKE_CAPABLE = [
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
@ -429,7 +462,8 @@ STRIKE_CAPABLE = [
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
|
||||
]
|
||||
|
||||
@ -441,6 +475,10 @@ STRIKE_PREFERRED = [
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
@ -448,27 +486,101 @@ STRIKE_PREFERRED = [
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
F_A_18C,
|
||||
F_15E,
|
||||
FA_18C_hornet,
|
||||
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_10A,
|
||||
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
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 = [
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
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 []
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Objective adjacency lists."""
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
|
||||
from theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
|
||||
@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
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.point import MovingPoint, PointAction
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
@ -17,26 +17,22 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FlightType(Enum):
|
||||
CAP = 0 # Do not use. Use BARCAP or TARCAP.
|
||||
TARCAP = 1
|
||||
BARCAP = 2
|
||||
CAS = 3
|
||||
INTERCEPTION = 4
|
||||
STRIKE = 5
|
||||
ANTISHIP = 6
|
||||
SEAD = 7
|
||||
DEAD = 8
|
||||
ESCORT = 9
|
||||
BAI = 10
|
||||
TARCAP = "TARCAP"
|
||||
BARCAP = "BARCAP"
|
||||
CAS = "CAS"
|
||||
INTERCEPTION = "Intercept"
|
||||
STRIKE = "Strike"
|
||||
ANTISHIP = "Anti-ship"
|
||||
SEAD = "SEAD"
|
||||
DEAD = "DEAD"
|
||||
ESCORT = "Escort"
|
||||
BAI = "BAI"
|
||||
SWEEP = "Fighter sweep"
|
||||
OCA_RUNWAY = "OCA/Runway"
|
||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||
|
||||
# Helos
|
||||
TROOP_TRANSPORT = 11
|
||||
LOGISTICS = 12
|
||||
EVAC = 13
|
||||
|
||||
ELINT = 14
|
||||
RECON = 15
|
||||
EWAR = 16
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class FlightWaypointType(Enum):
|
||||
@ -61,6 +57,11 @@ class FlightWaypointType(Enum):
|
||||
LOITER = 18
|
||||
INGRESS_ESCORT = 19
|
||||
INGRESS_DEAD = 20
|
||||
INGRESS_SWEEP = 21
|
||||
INGRESS_BAI = 22
|
||||
DIVERT = 23
|
||||
INGRESS_OCA_RUNWAY = 24
|
||||
INGRESS_OCA_AIRCRAFT = 25
|
||||
|
||||
|
||||
class FlightWaypoint:
|
||||
@ -87,6 +88,7 @@ class FlightWaypoint:
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.only_for_player = False
|
||||
self.flyover = False
|
||||
|
||||
# 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
|
||||
@ -128,13 +130,16 @@ class FlightWaypoint:
|
||||
|
||||
class Flight:
|
||||
|
||||
def __init__(self, package: Package, unit_type: FlyingType, count: int,
|
||||
from_cp: ControlPoint, flight_type: FlightType,
|
||||
start_type: str) -> None:
|
||||
def __init__(self, package: Package, unit_type: Type[FlyingType],
|
||||
count: int, flight_type: FlightType, start_type: str,
|
||||
departure: ControlPoint, arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint]) -> None:
|
||||
self.package = package
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.from_cp = from_cp
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.divert = divert
|
||||
self.flight_type = flight_type
|
||||
# TODO: Replace with FlightPlan.
|
||||
self.targets: List[MissionTarget] = []
|
||||
@ -153,10 +158,14 @@ class Flight:
|
||||
custom_waypoints=[]
|
||||
)
|
||||
|
||||
@property
|
||||
def from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
|
||||
@property
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def __repr__(self):
|
||||
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
|
||||
+ " (" + str(len(self.points)) + " wpt)"
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
@ -7,20 +7,28 @@ generating the waypoints for the mission.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.utils import nm_to_meter
|
||||
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
|
||||
from game.theater import (
|
||||
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 .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@ -31,7 +39,6 @@ if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
|
||||
|
||||
INGRESS_TYPES = {
|
||||
FlightWaypointType.INGRESS_CAS,
|
||||
FlightWaypointType.INGRESS_ESCORT,
|
||||
@ -47,10 +54,9 @@ class PlanningError(RuntimeError):
|
||||
|
||||
class InvalidObjectiveLocation(PlanningError):
|
||||
"""Raised when the objective location is invalid for the mission type."""
|
||||
|
||||
def __init__(self, task: FlightType, location: MissionTarget) -> None:
|
||||
super().__init__(
|
||||
f"{location.name} is not valid for {task.name} missions."
|
||||
)
|
||||
super().__init__(f"{location.name} is not valid for {task} missions.")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -61,6 +67,10 @@ class FlightPlan:
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
"""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
|
||||
|
||||
@property
|
||||
@ -104,6 +114,47 @@ class FlightPlan:
|
||||
failed to generate. Nevertheless, we have to defend against it.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan
|
||||
"""
|
||||
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.
|
||||
@property
|
||||
@ -147,13 +198,36 @@ class FlightPlan:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FormationFlightPlan(FlightPlan):
|
||||
class LoiterFlightPlan(FlightPlan):
|
||||
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
|
||||
split: FlightWaypoint
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@ -215,12 +289,6 @@ class FormationFlightPlan(FlightPlan):
|
||||
return self.split_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(
|
||||
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
if waypoint == self.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
return self.join_time - TravelTime.between_points(
|
||||
@ -260,8 +328,7 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
return self.patrol_end_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@ -277,15 +344,17 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
return [
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -293,16 +362,18 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
target: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
return [
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.target,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
def request_escort_at(self) -> Optional[FlightWaypoint]:
|
||||
return self.patrol_start
|
||||
@ -312,18 +383,25 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineCapFlightPlan(PatrollingFlightPlan):
|
||||
class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
lead_time: timedelta
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
return [
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
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(
|
||||
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
@ -335,8 +413,8 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan):
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
start = self.package.escort_start_time
|
||||
if start is not None:
|
||||
return start
|
||||
return super().patrol_start_time
|
||||
return start + self.tot_offset
|
||||
return super().patrol_start_time + self.tot_offset
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
@ -356,26 +434,30 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
egress: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
return [
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.join,
|
||||
self.ingress
|
||||
] + self.targets + [
|
||||
]
|
||||
yield from self.targets
|
||||
yield from [
|
||||
self.egress,
|
||||
self.split,
|
||||
self.land,
|
||||
]
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@property
|
||||
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
|
||||
return {
|
||||
self.ingress,
|
||||
self.egress,
|
||||
self.split,
|
||||
self.ingress,
|
||||
self.egress,
|
||||
self.split,
|
||||
} | set(self.targets)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
@ -461,13 +543,72 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
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)
|
||||
class CustomFlightPlan(FlightPlan):
|
||||
custom_waypoints: List[FlightWaypoint]
|
||||
|
||||
@property
|
||||
def waypoints(self) -> List[FlightWaypoint]:
|
||||
return self.custom_waypoints
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from self.custom_waypoints
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||
@ -521,20 +662,18 @@ class FlightPlanBuilder:
|
||||
raise RuntimeError("Flight must be a part of the package")
|
||||
if self.package.waypoints is None:
|
||||
self.regenerate_package_waypoints()
|
||||
|
||||
try:
|
||||
flight_plan = self.generate_flight_plan(flight, custom_targets)
|
||||
except PlanningError:
|
||||
logging.exception(f"Could not create flight plan")
|
||||
return
|
||||
flight.flight_plan = flight_plan
|
||||
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
|
||||
|
||||
def generate_flight_plan(
|
||||
self, flight: Flight,
|
||||
custom_targets: Optional[List[Unit]]) -> FlightPlan:
|
||||
# TODO: Flesh out mission types.
|
||||
task = flight.flight_type
|
||||
if task == FlightType.BARCAP:
|
||||
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)
|
||||
elif task == FlightType.CAS:
|
||||
return self.generate_cas(flight)
|
||||
@ -542,18 +681,20 @@ class FlightPlanBuilder:
|
||||
return self.generate_dead(flight, custom_targets)
|
||||
elif task == FlightType.ESCORT:
|
||||
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:
|
||||
return self.generate_sead(flight, custom_targets)
|
||||
elif task == FlightType.STRIKE:
|
||||
return self.generate_strike(flight)
|
||||
elif task == FlightType.SWEEP:
|
||||
return self.generate_sweep(flight)
|
||||
elif task == FlightType.TARCAP:
|
||||
return self.generate_frontline_cap(flight)
|
||||
elif task == FlightType.TROOP_TRANSPORT:
|
||||
logging.error(
|
||||
"Troop transport flight plan generation not implemented"
|
||||
)
|
||||
return self.generate_tarcap(flight)
|
||||
raise PlanningError(
|
||||
f"{task.name} flight plan generation not implemented")
|
||||
f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
@ -603,7 +744,54 @@ class FlightPlanBuilder:
|
||||
|
||||
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:
|
||||
"""Generate a BARCAP flight at a given location.
|
||||
@ -616,11 +804,56 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start, end = self.racetrack_for_objective(location)
|
||||
patrol_alt = random.randint(
|
||||
self.doctrine.min_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)
|
||||
for airfield in closest_cache.closest_airfields:
|
||||
# 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
|
||||
)
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
return start, end
|
||||
|
||||
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.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)
|
||||
def racetrack_for_frontline(self,
|
||||
front_line: FrontLine) -> Tuple[Point, Point]:
|
||||
ally_cp, enemy_cp = front_line.control_points
|
||||
|
||||
# Find targets waypoints
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
@ -700,26 +910,46 @@ class FlightPlanBuilder:
|
||||
if combat_width < 35000:
|
||||
combat_width = 35000
|
||||
|
||||
radius = combat_width*1.25
|
||||
radius = combat_width * 1.25
|
||||
orbit0p = orbit_center.point_from_heading(heading, 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
|
||||
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,
|
||||
flight=flight,
|
||||
lead_time=timedelta(minutes=2),
|
||||
# Note that this duration only has an effect if there are no
|
||||
# flights in the package that have requested escort. If the package
|
||||
# requests an escort the CAP flight will remain on station for the
|
||||
# duration of the escorted mission, or until it is winchester/bingo.
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
takeoff=builder.takeoff(flight.from_cp),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
patrol_start=start,
|
||||
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,
|
||||
@ -732,8 +962,11 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}")
|
||||
is_ewr = isinstance(location, EwrGroundObject)
|
||||
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)
|
||||
|
||||
# TODO: Unify these.
|
||||
@ -745,7 +978,42 @@ class FlightPlanBuilder:
|
||||
for target in custom_targets:
|
||||
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,
|
||||
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
|
||||
@ -757,9 +1025,6 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
if not isinstance(location, TheaterGroundObject):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
# TODO: Unify these.
|
||||
# There doesn't seem to be any reason to treat the UI fragged missions
|
||||
# different from the automatic missions.
|
||||
@ -769,7 +1034,8 @@ class FlightPlanBuilder:
|
||||
for target in custom_targets:
|
||||
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:
|
||||
assert self.package.waypoints is not None
|
||||
@ -782,14 +1048,15 @@ class FlightPlanBuilder:
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.from_cp),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
egress=egress,
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
land=builder.land(flight.from_cp)
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
|
||||
def generate_cas(self, flight: Flight) -> CasFlightPlan:
|
||||
@ -816,17 +1083,21 @@ class FlightPlanBuilder:
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cas_duration,
|
||||
takeoff=builder.takeoff(flight.from_cp),
|
||||
patrol_start=builder.ingress_cas(ingress, location),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
|
||||
ingress, location),
|
||||
target=builder.cas(center),
|
||||
patrol_end=builder.egress(egress, location),
|
||||
land=builder.land(flight.from_cp)
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def target_waypoint(flight: Flight, builder: WaypointBuilder,
|
||||
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)
|
||||
elif flight.flight_type == FlightType.SEAD:
|
||||
return builder.sead_point(target)
|
||||
@ -840,12 +1111,14 @@ class FlightPlanBuilder:
|
||||
return builder.dead_area(location)
|
||||
elif flight.flight_type == FlightType.SEAD:
|
||||
return builder.sead_area(location)
|
||||
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
|
||||
return builder.oca_strike_area(location)
|
||||
else:
|
||||
return builder.strike_area(location)
|
||||
|
||||
def _hold_point(self, flight: Flight) -> Point:
|
||||
assert self.package.waypoints is not None
|
||||
origin = flight.from_cp.position
|
||||
origin = flight.departure.position
|
||||
target = self.package.target.position
|
||||
join = self.package.waypoints.join
|
||||
origin_to_target = origin.distance_to_point(target)
|
||||
@ -902,22 +1175,12 @@ class FlightPlanBuilder:
|
||||
return builder.land(arrival)
|
||||
|
||||
def strike_flightplan(
|
||||
self, flight: Flight, location: TheaterGroundObject,
|
||||
self, flight: Flight, location: MissionTarget,
|
||||
ingress_type: FlightWaypointType,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
|
||||
targets)
|
||||
# sead_types = {FlightType.DEAD, FlightType.SEAD}
|
||||
if flight.flight_type is FlightType.SEAD:
|
||||
ingress = builder.ingress_sead(self.package.waypoints.ingress,
|
||||
location)
|
||||
|
||||
elif flight.flight_type is FlightType.DEAD:
|
||||
ingress = builder.ingress_dead(self.package.waypoints.ingress,
|
||||
location)
|
||||
else:
|
||||
ingress = builder.ingress_strike(self.package.waypoints.ingress,
|
||||
location)
|
||||
|
||||
target_waypoints: List[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
@ -931,14 +1194,16 @@ class FlightPlanBuilder:
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.from_cp),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
ingress=ingress,
|
||||
ingress=builder.ingress(ingress_type,
|
||||
self.package.waypoints.ingress, location),
|
||||
targets=target_waypoints,
|
||||
egress=builder.egress(self.package.waypoints.egress, location),
|
||||
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:
|
||||
@ -951,8 +1216,8 @@ class FlightPlanBuilder:
|
||||
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that advances toward the target."""
|
||||
heading = self._heading_to_package_airfield(attack_transition)
|
||||
return attack_transition.point_from_heading(heading,
|
||||
-self.doctrine.join_distance)
|
||||
return attack_transition.point_from_heading(
|
||||
heading, -self.doctrine.join_distance)
|
||||
|
||||
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
|
||||
transition_target_distance = attack_transition.distance_to_point(
|
||||
@ -1014,7 +1279,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
for airfield in cache.closest_airfields:
|
||||
for flight in self.package.flights:
|
||||
if flight.from_cp == airfield:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
raise RuntimeError(
|
||||
"Could not find any airfield assigned to this package"
|
||||
|
||||
@ -45,20 +45,21 @@ class GroundSpeed:
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude: The altitude in feet.
|
||||
altitude_m: The altitude in meters.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude
|
||||
altitude_ft = altitude_m * 3.28084
|
||||
if altitude_ft <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude_ft
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
@ -86,6 +87,7 @@ class TravelTime:
|
||||
return timedelta(hours=distance / speed * error_factor)
|
||||
|
||||
|
||||
# TODO: Most if not all of this should move into FlightPlan.
|
||||
class TotEstimator:
|
||||
# An extra five minutes given as wiggle room. Expected to be spent at the
|
||||
# hold point performing any last minute configuration.
|
||||
@ -135,7 +137,14 @@ class TotEstimator:
|
||||
f"time for {flight} will be immediate.")
|
||||
return None
|
||||
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
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
@ -172,9 +181,13 @@ class TotEstimator:
|
||||
# Return 0 so this flight's travel time does not affect the rest
|
||||
# of the package.
|
||||
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)
|
||||
ground_ops = self.estimate_ground_ops(flight)
|
||||
return startup + ground_ops + time_to_target
|
||||
return startup + ground_ops + time_to_target + offset
|
||||
|
||||
@staticmethod
|
||||
def estimate_startup(flight: Flight) -> timedelta:
|
||||
|
||||
@ -5,17 +5,23 @@ from typing import List, Optional, Tuple, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.weather import Conditions
|
||||
from theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[TheaterGroundObject, Unit]
|
||||
target: Union[VehicleGroup, TheaterGroundObject, Unit]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
@ -31,8 +37,7 @@ class WaypointBuilder:
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
|
||||
@staticmethod
|
||||
def takeoff(departure: ControlPoint) -> FlightWaypoint:
|
||||
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Note that the takeoff waypoint will automatically be created by pydcs
|
||||
@ -43,36 +48,93 @@ class WaypointBuilder:
|
||||
departure: Departure airfield or carrier.
|
||||
"""
|
||||
position = departure.position
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
)
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Takeoff"
|
||||
waypoint.pretty_name = "Takeoff"
|
||||
if isinstance(departure, 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 = "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
|
||||
|
||||
@staticmethod
|
||||
def land(arrival: ControlPoint) -> FlightWaypoint:
|
||||
def land(self, arrival: ControlPoint) -> FlightWaypoint:
|
||||
"""Create descent waypoint for the given arrival airfield or carrier.
|
||||
|
||||
Args:
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
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(
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
FlightWaypointType.DIVERT,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Land"
|
||||
waypoint.pretty_name = "Land"
|
||||
waypoint.alt_type = altitude_type
|
||||
waypoint.name = "DIVERT"
|
||||
waypoint.description = "Divert"
|
||||
waypoint.pretty_name = "Divert"
|
||||
waypoint.only_for_player = True
|
||||
return waypoint
|
||||
|
||||
def hold(self, position: Point) -> FlightWaypoint:
|
||||
@ -111,33 +173,8 @@ class WaypointBuilder:
|
||||
waypoint.name = "SPLIT"
|
||||
return waypoint
|
||||
|
||||
def ingress_cas(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
|
||||
objective)
|
||||
|
||||
def ingress_escort(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
|
||||
objective)
|
||||
|
||||
def ingress_dead(self, position:Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_sead(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
|
||||
objective)
|
||||
|
||||
def ingress_strike(self, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
|
||||
objective)
|
||||
|
||||
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
def ingress(self, ingress_type: FlightWaypointType, position: Point,
|
||||
objective: MissionTarget) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
ingress_type,
|
||||
position.x,
|
||||
@ -163,6 +200,9 @@ class WaypointBuilder:
|
||||
waypoint.name = "EGRESS"
|
||||
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:
|
||||
return self._target_point(target, f"STRIKE {target.name}")
|
||||
|
||||
@ -183,6 +223,7 @@ class WaypointBuilder:
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
waypoint.name = target.name
|
||||
waypoint.alt_type = "RADIO"
|
||||
# The target waypoints are only for the player's benefit. AI tasks for
|
||||
# the target are set on the ingress point so they begin their attack
|
||||
# *before* reaching the target.
|
||||
@ -198,8 +239,12 @@ class WaypointBuilder:
|
||||
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
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
|
||||
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
|
||||
def _target_area(name: str, location: MissionTarget,
|
||||
flyover: bool = False) -> FlightWaypoint:
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
@ -209,10 +254,19 @@ class WaypointBuilder:
|
||||
waypoint.description = name
|
||||
waypoint.pretty_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
|
||||
# *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
|
||||
|
||||
def cas(self, position: Point) -> FlightWaypoint:
|
||||
@ -278,6 +332,56 @@ class WaypointBuilder:
|
||||
return (self.race_track_start(start, 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) -> \
|
||||
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
@ -293,8 +397,8 @@ class WaypointBuilder:
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||
target)
|
||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
|
||||
target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
|
||||
@ -1,55 +1,44 @@
|
||||
import logging
|
||||
import typing
|
||||
from enum import IntEnum
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
from dcs.mission import Mission
|
||||
|
||||
from .conflictgen import *
|
||||
|
||||
|
||||
class Labels(IntEnum):
|
||||
Off = 0
|
||||
Full = 1
|
||||
Abbreviated = 2
|
||||
Dot = 3
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class ForcedOptionsGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game):
|
||||
def __init__(self, mission: Mission, game: Game) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
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:
|
||||
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):
|
||||
def _set_external_views(self) -> None:
|
||||
if not self.game.settings.external_views_allowed:
|
||||
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
|
||||
|
||||
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":
|
||||
self.mission.forced_options.labels = int(Labels.Abbreviated)
|
||||
self.mission.forced_options.labels = ForcedOptions.Labels.Abbreviate
|
||||
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":
|
||||
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):
|
||||
self._set_options_view()
|
||||
self._set_external_views()
|
||||
self._set_labels()
|
||||
|
||||
|
||||
|
||||
self._set_unrestricted_satnav()
|
||||
|
||||
@ -2,12 +2,12 @@ import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from theater import ControlPoint
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
|
||||
@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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.country import Country
|
||||
@ -20,20 +20,21 @@ from dcs.task import (
|
||||
EPLRS,
|
||||
OptAlarmState,
|
||||
)
|
||||
from dcs.unit import Ship, Vehicle, Unit
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup
|
||||
from dcs.unit import Ship, Unit, Vehicle
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from theater import ControlPoint, TheaterGroundObject
|
||||
from theater.theatergroundobject import (
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject, CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
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 .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
@ -52,11 +53,12 @@ class GenericGroundObjectGenerator:
|
||||
Currently used only for SAM and missile (V1/V2) sites.
|
||||
"""
|
||||
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.country = country
|
||||
self.game = game
|
||||
self.m = mission
|
||||
self.unit_map = unit_map
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
@ -89,9 +91,10 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
self.enable_eplrs(vg, unit_type)
|
||||
self.set_alarm_state(vg)
|
||||
self._register_unit_group(group, vg)
|
||||
|
||||
@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 unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
@ -102,6 +105,11 @@ class GenericGroundObjectGenerator:
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
def _register_unit_group(self, persistence_group: Group,
|
||||
miz_group: Group) -> None:
|
||||
self.unit_map.add_ground_object_units(self.ground_object,
|
||||
persistence_group, miz_group)
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
"""Generator for building sites.
|
||||
@ -133,16 +141,17 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
def generate_vehicle_group(self, unit_type: UnitType) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
self.m.vehicle_group(
|
||||
group = self.m.vehicle_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=unit_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading,
|
||||
)
|
||||
self._register_fortification(group)
|
||||
|
||||
def generate_static(self, static_type: StaticType) -> None:
|
||||
self.m.static_group(
|
||||
group = self.m.static_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=static_type,
|
||||
@ -150,6 +159,15 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
heading=self.ground_object.heading,
|
||||
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):
|
||||
@ -161,8 +179,8 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
control_point: ControlPoint, country: Country, game: Game,
|
||||
mission: Mission, radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
|
||||
runways: Dict[str, RunwayData]) -> None:
|
||||
super().__init__(ground_object, country, game, mission)
|
||||
runways: Dict[str, RunwayData], unit_map: UnitMap) -> None:
|
||||
super().__init__(ground_object, country, game, mission, unit_map)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
self.radio_registry = radio_registry
|
||||
@ -187,11 +205,16 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
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)
|
||||
self.activate_beacons(ship_group, 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)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(
|
||||
@ -221,12 +244,16 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
return ship
|
||||
|
||||
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):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
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 None
|
||||
|
||||
@ -328,8 +355,9 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
|
||||
self.generate_group(group, unit_type)
|
||||
|
||||
def generate_group(self, group_def: Group, unit_type: UnitType):
|
||||
group = self.m.ship_group(self.country, group_def.name, unit_type,
|
||||
def generate_group(self, group_def: Group,
|
||||
first_unit_type: Type[UnitType]) -> None:
|
||||
group = self.m.ship_group(self.country, group_def.name, first_unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading)
|
||||
group.units[0].name = self.m.string(group_def.units[0].name)
|
||||
@ -343,6 +371,7 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
ship.heading = unit.heading
|
||||
group.add_unit(ship)
|
||||
self.set_alarm_state(group)
|
||||
self._register_unit_group(group_def, group)
|
||||
|
||||
|
||||
class GroundObjectsGenerator:
|
||||
@ -353,40 +382,18 @@ class GroundObjectsGenerator:
|
||||
locations for spawning ground objects, determining their types, and creating
|
||||
the appropriate generators.
|
||||
"""
|
||||
FARP_CAPACITY = 4
|
||||
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game,
|
||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
|
||||
def __init__(self, mission: Mission, game: Game,
|
||||
radio_registry: RadioRegistry, tacan_registry: TacanRegistry,
|
||||
unit_map: UnitMap) -> None:
|
||||
self.m = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.unit_map = unit_map
|
||||
self.icls_alloc = iter(range(1, 21))
|
||||
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):
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
@ -397,25 +404,26 @@ class GroundObjectsGenerator:
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
if isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
generator = BuildingSiteGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
elif isinstance(ground_object, CarrierGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
generator = CarrierGenerator(
|
||||
ground_object, cp, country, self.game, self.m,
|
||||
self.radio_registry, self.tacan_registry,
|
||||
self.icls_alloc, self.runways, self.unit_map)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
generator = CarrierGenerator(ground_object, cp, country,
|
||||
self.game, self.m,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.icls_alloc, self.runways)
|
||||
generator = CarrierGenerator(
|
||||
ground_object, cp, country, self.game, self.m,
|
||||
self.radio_registry, self.tacan_registry,
|
||||
self.icls_alloc, self.runways, self.unit_map)
|
||||
elif isinstance(ground_object, ShipGroundObject):
|
||||
generator = ShipObjectGenerator(ground_object, country,
|
||||
self.game, self.m)
|
||||
generator = ShipObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
else:
|
||||
generator = GenericGroundObjectGenerator(ground_object,
|
||||
country, self.game,
|
||||
self.m)
|
||||
|
||||
generator = GenericGroundObjectGenerator(
|
||||
ground_object, country, self.game, self.m,
|
||||
self.unit_map)
|
||||
generator.generate()
|
||||
|
||||
@ -26,7 +26,7 @@ import datetime
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
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 dcs.mission import Mission
|
||||
@ -44,6 +44,8 @@ from .runways import RunwayData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class KneeboardPageWriter:
|
||||
"""Creates kneeboard images."""
|
||||
|
||||
@ -191,7 +193,15 @@ class FlightPlanBuilder:
|
||||
waypoint.position
|
||||
))
|
||||
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]]:
|
||||
return self.rows
|
||||
@ -230,28 +240,37 @@ class BriefingPage(KneeboardPage):
|
||||
"#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"
|
||||
])
|
||||
|
||||
writer.heading("Comm Ladder")
|
||||
comms = []
|
||||
flight_plan_builder
|
||||
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:
|
||||
comms.append([comm.name, self.format_frequency(comm.freq)])
|
||||
writer.table(comms, headers=["Name", "UHF"])
|
||||
comm_ladder.append([comm.name, '', '', '', self.format_frequency(comm.freq)])
|
||||
|
||||
writer.heading("AWACS")
|
||||
awacs = []
|
||||
for a in self.awacs:
|
||||
awacs.append([a.callsign, self.format_frequency(a.freq)])
|
||||
writer.table(awacs, headers=["Callsign", "UHF"])
|
||||
|
||||
writer.heading("Tankers")
|
||||
tankers = []
|
||||
comm_ladder.append([
|
||||
a.callsign,
|
||||
'AWACS',
|
||||
'',
|
||||
'',
|
||||
self.format_frequency(a.freq)
|
||||
])
|
||||
for tanker in self.tankers:
|
||||
tankers.append([
|
||||
comm_ladder.append([
|
||||
tanker.callsign,
|
||||
"Tanker",
|
||||
tanker.variant,
|
||||
str(tanker.tacan),
|
||||
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")
|
||||
jtacs = []
|
||||
|
||||
@ -8,7 +8,7 @@ from gen.locations.preset_control_point_locations import PresetControlPointLocat
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
class PresetLocationFinder:
|
||||
class MizDataLocationFinder:
|
||||
|
||||
@staticmethod
|
||||
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
|
||||
|
||||
@ -134,7 +134,7 @@ RADIOS: List[Radio] = [
|
||||
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
|
||||
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", MHz(100), MHz(150), step=MHz(1)),
|
||||
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
|
||||
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
|
||||
@ -8,7 +8,6 @@ from typing import Iterator, Optional
|
||||
from dcs.terrain.terrain import Airport
|
||||
|
||||
from game.weather import Conditions
|
||||
from theater import ControlPoint, ControlPointType
|
||||
from .airfields import AIRFIELD_DATA
|
||||
from .radios import RadioFrequency
|
||||
from .tacan import TacanChannel
|
||||
@ -117,23 +116,3 @@ class RunwayAssigner:
|
||||
# Otherwise the only difference between the two is the distance from
|
||||
# parking, which we don't know, so just pick the first one.
|
||||
return best_runways[0]
|
||||
|
||||
def takeoff_heading(self, departure: ControlPoint) -> int:
|
||||
if departure.cptype == ControlPointType.AIRBASE:
|
||||
return self.get_preferred_runway(departure.airport).runway_heading
|
||||
elif departure.is_fleet:
|
||||
# The carrier will be angled into the wind automatically.
|
||||
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
|
||||
logging.warning(
|
||||
f"Unhandled departure control point: {departure.cptype}")
|
||||
return 0
|
||||
|
||||
def landing_heading(self, arrival: ControlPoint) -> int:
|
||||
if arrival.cptype == ControlPointType.AIRBASE:
|
||||
return self.get_preferred_runway(arrival.airport).runway_heading
|
||||
elif arrival.is_fleet:
|
||||
# The carrier will be angled into the wind automatically.
|
||||
return (self.conditions.weather.wind.at_0m.direction + 180) % 360
|
||||
logging.warning(
|
||||
f"Unhandled departure control point: {arrival.cptype}")
|
||||
return 0
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -25,4 +28,8 @@ class BoforsGenerator(GroupGenerator):
|
||||
index = index+1
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "AAA#" + str(index),
|
||||
self.position.x + spacing*i,
|
||||
self.position.y + spacing*j, self.heading)
|
||||
self.position.y + spacing*j, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,11 +2,22 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -18,7 +29,7 @@ class FlakGenerator(GroupGenerator):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
spacing = random.randint(20, 35)
|
||||
|
||||
index = 0
|
||||
mixed = random.choice([True, False])
|
||||
@ -35,7 +46,7 @@ class FlakGenerator(GroupGenerator):
|
||||
unit_type = random.choice(GFLAK)
|
||||
|
||||
# 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):
|
||||
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
|
||||
for i in range(int(max(1,grid_x/2))):
|
||||
for j in range(int(max(1,grid_x/2))):
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "AAA#" + str(index),
|
||||
self.position.x + 200 + 15*i + random.randint(1,5),
|
||||
self.position.y + 15*j + random.randint(1,5), 90)
|
||||
self.add_unit(Unarmed.Blitz_3_6_6700A, "BLITZ#" + str(index),
|
||||
self.position.x + 125 + 15*i + random.randint(1,5),
|
||||
self.position.y + 15*j + random.randint(1,5), 75)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -27,3 +30,7 @@ class Flak18Generator(GroupGenerator):
|
||||
|
||||
# Add a commander truck
|
||||
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
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
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
|
||||
"""
|
||||
@ -15,15 +18,15 @@ class AllyWW2FlakGenerator(GroupGenerator):
|
||||
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
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.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
|
||||
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -25,4 +28,8 @@ class ZU23InsurgentGenerator(GroupGenerator):
|
||||
index = index+1
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Insurgent_Closed, "AAA#" + str(index),
|
||||
self.position.x + spacing*i,
|
||||
self.position.y + spacing*j, self.heading)
|
||||
self.position.y + spacing*j, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
27
gen/sam/airdefensegroupgenerator.py
Normal file
27
gen/sam/airdefensegroupgenerator.py
Normal 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:
|
||||
...
|
||||
@ -2,10 +2,14 @@ import random
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
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.
|
||||
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
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
|
||||
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#2",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
|
||||
# Add a truck
|
||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
|
||||
@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.
|
||||
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
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
||||
self.position.x - 40, self.position.y - 40, self.heading + 180),
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#2",
|
||||
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
|
||||
|
||||
# Add a P19 Radar for EWR
|
||||
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import random
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed
|
||||
|
||||
from dcs.vehicles import AirDefence, Unarmed, Infantry
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
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
|
||||
"""
|
||||
@ -36,4 +37,8 @@ class FreyaGenerator(GroupGenerator):
|
||||
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
|
||||
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)
|
||||
self.add_unit(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
|
||||
|
||||
@ -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)
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from dcs import unitgroup
|
||||
from dcs.point import PointAction
|
||||
@ -9,7 +9,7 @@ from dcs.unit import Vehicle, Ship
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
@ -38,7 +38,7 @@ class GroupGenerator:
|
||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
||||
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:
|
||||
unit = Vehicle(self.game.next_unit_id(),
|
||||
f"{self.go.group_name}|{name}", unit_type.id)
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,3 +23,7 @@ class AvengerGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Avenger_M1097, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,3 +23,7 @@ class ChaparralGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Chaparral_M48, "SPAA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -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(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -1,18 +1,26 @@
|
||||
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.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_flak import FlakGenerator
|
||||
from gen.sam.aaa_flak18 import Flak18Generator
|
||||
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
||||
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
||||
from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator
|
||||
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseGroupGenerator,
|
||||
AirDefenseRange,
|
||||
)
|
||||
from gen.sam.cold_war_flak import (
|
||||
ColdWarFlakGenerator,
|
||||
EarlyColdWarFlakGenerator,
|
||||
)
|
||||
from gen.sam.ewrs import (
|
||||
BigBirdGenerator,
|
||||
BoxSpringGenerator,
|
||||
@ -25,6 +33,7 @@ from gen.sam.ewrs import (
|
||||
StraightFlushGenerator,
|
||||
TallRackGenerator,
|
||||
)
|
||||
from gen.sam.freya_ewr import FreyaGenerator
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.sam_avenger import AvengerGenerator
|
||||
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_rapier import RapierGenerator
|
||||
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_sa13 import SA13Generator
|
||||
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_ural import ZU23UralGenerator
|
||||
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
|
||||
from gen.sam.freya_ewr import FreyaGenerator
|
||||
from theater import TheaterGroundObject
|
||||
from theater.theatergroundobject import SamGroundObject
|
||||
|
||||
SAM_MAP = {
|
||||
SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
||||
"HawkGenerator": HawkGenerator,
|
||||
"ZU23Generator": ZU23Generator,
|
||||
"ZU23UralGenerator": ZU23UralGenerator,
|
||||
@ -77,6 +87,8 @@ SAM_MAP = {
|
||||
"SA8Generator": SA8Generator,
|
||||
"SA9Generator": SA9Generator,
|
||||
"SA10Generator": SA10Generator,
|
||||
"Tier2SA10Generator": Tier2SA10Generator,
|
||||
"Tier3SA10Generator": Tier3SA10Generator,
|
||||
"SA11Generator": SA11Generator,
|
||||
"SA13Generator": SA13Generator,
|
||||
"SA15Generator": SA15Generator,
|
||||
@ -89,6 +101,7 @@ SAM_MAP = {
|
||||
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
|
||||
}
|
||||
|
||||
|
||||
SAM_PRICES = {
|
||||
AirDefence.SAM_Hawk_PCP: 35,
|
||||
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
|
||||
: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
|
||||
: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,
|
||||
faction: str) -> Optional[VehicleGroup]:
|
||||
def _generate_anti_air_from(
|
||||
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
|
||||
:param game: The Game.
|
||||
:param ground_object: The ground object which will own the sam group.
|
||||
: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.
|
||||
"""
|
||||
possible_sams_generators = get_faction_possible_sams_generator(faction)
|
||||
if len(possible_sams_generators) > 0:
|
||||
sam_generator_class = random.choice(possible_sams_generators)
|
||||
generator = sam_generator_class(game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
generators = get_faction_possible_sams_generator(faction)
|
||||
if ranges is None:
|
||||
ranges = [{
|
||||
AirDefenseRange.Long,
|
||||
AirDefenseRange.Medium,
|
||||
AirDefenseRange.Short,
|
||||
}]
|
||||
|
||||
for range_options in ranges:
|
||||
generators_for_range = [g for g in generators if
|
||||
g.range() in range_options]
|
||||
group = _generate_anti_air_from(generators_for_range, game,
|
||||
ground_object)
|
||||
if group is not None:
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
|
||||
faction: str) -> Optional[VehicleGroup]:
|
||||
faction: Faction) -> Optional[VehicleGroup]:
|
||||
"""Generates an early warning radar group.
|
||||
|
||||
:param game: The Game.
|
||||
@ -187,16 +233,3 @@ def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return None
|
||||
|
||||
|
||||
def generate_shorad_group(game: Game, ground_object: SamGroundObject,
|
||||
faction_name: str) -> Optional[VehicleGroup]:
|
||||
faction = db.FACTIONS[faction_name]
|
||||
|
||||
if len(faction.shorads) > 0:
|
||||
sam = random.choice(faction.shorads)
|
||||
generator = SAM_MAP[sam](game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
else:
|
||||
return generate_anti_air_group(game, ground_object, faction_name)
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -25,4 +28,8 @@ class HawkGenerator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Hawk_LN_M192, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.SAM_Hawk_LN_M192, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -25,4 +28,8 @@ class HQ7Generator(GenericSamGroupGenerator):
|
||||
if num_launchers > 0:
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,3 +23,7 @@ class LinebackerGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=110, coverage=180)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_Linebacker_M6, "M6#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -15,7 +18,7 @@ class PatriotGenerator(GenericSamGroupGenerator):
|
||||
|
||||
def generate(self):
|
||||
# Command Post
|
||||
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading)
|
||||
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "STR", self.position.x + 30, self.position.y + 30, self.heading)
|
||||
self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(AirDefence.SAM_Patriot_ECS_AN_MSQ_104, "MSQ", self.position.x + 30, self.position.y, self.heading)
|
||||
self.add_unit(AirDefence.SAM_Patriot_ICC, "ICC", self.position.x + 60, self.position.y, self.heading)
|
||||
@ -30,4 +33,8 @@ class PatriotGenerator(GenericSamGroupGenerator):
|
||||
num_launchers = random.randint(3, 4)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Long
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,4 +24,8 @@ class RapierGenerator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=240)
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
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
|
||||
"""
|
||||
@ -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(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,16 +2,19 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
name = "SA-10/S-300PS Battery"
|
||||
price = 450
|
||||
price = 550
|
||||
|
||||
def generate(self):
|
||||
# Search Radar
|
||||
@ -38,15 +41,55 @@ class SA10Generator(GenericSamGroupGenerator):
|
||||
else:
|
||||
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
|
||||
# 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])
|
||||
self.generate_defensive_groups()
|
||||
|
||||
# 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)
|
||||
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):
|
||||
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])
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,4 +24,8 @@ class SA11Generator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=180)
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_LN_9A310M1, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_LN_9A310M1, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,4 +23,8 @@ class SA13Generator(GroupGenerator):
|
||||
num_launchers = random.randint(2, 3)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
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
|
||||
"""
|
||||
@ -14,4 +17,8 @@ class SA15Generator(GroupGenerator):
|
||||
def generate(self):
|
||||
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading)
|
||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -22,3 +25,7 @@ class SA19Generator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
for i, position in enumerate(positions):
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,4 +24,8 @@ class SA2Generator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -21,4 +24,8 @@ class SA3Generator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,4 +23,8 @@ class SA6Generator(GenericSamGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
|
||||
|
||||
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
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -16,3 +17,7 @@ class SA8Generator(GroupGenerator):
|
||||
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_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -20,4 +23,8 @@ class SA9Generator(GroupGenerator):
|
||||
num_launchers = random.randint(2, 3)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -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(Unarmed.Transport_M818, "TRUCK", self.position.x + 80, self.position.y, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -19,3 +22,7 @@ class ZSU23Generator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
for i, position in enumerate(positions):
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -25,4 +28,8 @@ class ZU23Generator(GroupGenerator):
|
||||
index = index+1
|
||||
self.add_unit(AirDefence.AAA_ZU_23_Closed, "AAA#" + str(index),
|
||||
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
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -19,3 +22,7 @@ class ZU23UralGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360)
|
||||
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])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
@ -2,10 +2,13 @@ import random
|
||||
|
||||
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
|
||||
"""
|
||||
@ -19,3 +22,8 @@ class ZU23UralInsurgentGenerator(GroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=80, coverage=360)
|
||||
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])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
|
||||
|
||||
@ -1,12 +1,38 @@
|
||||
from dcs.action import MarkToAll
|
||||
from dcs.condition import TimeAfter
|
||||
from __future__ import annotations
|
||||
|
||||
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.task import Option
|
||||
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 .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_ACTIVATION_AGL = 25
|
||||
@ -30,9 +56,11 @@ class Silence(Option):
|
||||
|
||||
|
||||
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.conflict = conflict
|
||||
self.game = game
|
||||
|
||||
def _set_allegiances(self, player_coalition: str, enemy_coalition: str):
|
||||
@ -56,9 +84,8 @@ class TriggersGenerator:
|
||||
airport.operating_level_fuel = 0
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.is_global:
|
||||
continue
|
||||
self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition)
|
||||
if isinstance(cp, Airfield):
|
||||
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):
|
||||
"""
|
||||
@ -73,8 +100,9 @@ class TriggersGenerator:
|
||||
continue
|
||||
|
||||
for country in coalition.countries.values():
|
||||
for plane_group in country.plane_group:
|
||||
for plane_unit in plane_group.units:
|
||||
flying_groups = country.plane_group + country.helicopter_group # type: FlyingGroup
|
||||
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:
|
||||
plane_unit.skill = Skill(skill_level[0])
|
||||
|
||||
@ -103,16 +131,71 @@ class TriggersGenerator:
|
||||
added.append(ground_object.obj_name)
|
||||
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):
|
||||
player_coalition = "blue"
|
||||
enemy_coalition = "red"
|
||||
|
||||
self.mission.coalition["blue"].bullseye = {"x": self.conflict.position.x,
|
||||
"y": self.conflict.position.y}
|
||||
self.mission.coalition["red"].bullseye = {"x": self.conflict.position.x,
|
||||
"y": self.conflict.position.y}
|
||||
player_cp, enemy_cp = self.game.theater.closest_opposing_control_points()
|
||||
self.mission.coalition["blue"].bullseye = {"x": enemy_cp.position.x,
|
||||
"y": enemy_cp.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_allegiances(player_coalition, enemy_coalition)
|
||||
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
|
||||
|
||||
@ -92,9 +92,8 @@ def turn_heading(heading, fac):
|
||||
|
||||
|
||||
class VisualGenerator:
|
||||
def __init__(self, mission: Mission, conflict: Conflict, game: Game):
|
||||
def __init__(self, mission: Mission, game: Game):
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
|
||||
def _generate_frontline_smokes(self):
|
||||
@ -104,15 +103,12 @@ class VisualGenerator:
|
||||
if from_cp.is_global or to_cp.is_global:
|
||||
continue
|
||||
|
||||
frontline = Conflict.frontline_position(self.game.theater, from_cp, to_cp)
|
||||
if not frontline:
|
||||
plane_start, heading, distance = Conflict.frontline_vector(from_cp, to_cp, self.game.theater)
|
||||
if not plane_start:
|
||||
continue
|
||||
|
||||
point, heading = frontline
|
||||
plane_start = point.point_from_heading(turn_heading(heading, 90), FRONTLINE_LENGTH / 2)
|
||||
|
||||
for offset in range(0, FRONTLINE_LENGTH, FRONT_SMOKE_SPACING):
|
||||
position = plane_start.point_from_heading(turn_heading(heading, - 90), offset)
|
||||
for offset in range(0, distance, FRONT_SMOKE_SPACING):
|
||||
position = plane_start.point_from_heading(heading, offset)
|
||||
|
||||
for k, v in FRONT_SMOKE_TYPE_CHANCES.items():
|
||||
if random.randint(0, 100) <= k:
|
||||
|
||||
3
mypy.ini
3
mypy.ini
@ -9,4 +9,7 @@ ignore_missing_imports = True
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-winreg.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-shapely.*]
|
||||
ignore_missing_imports = True
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit 2883be31c2eb80834b93efd8d20ca17913986e9b
|
||||
Subproject commit f924289c9cbe6e21a01906bdf11c1933110a32de
|
||||
357
pydcs_extensions/f22a/f22a.py
Normal file
357
pydcs_extensions/f22a/f22a.py
Normal 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
|
||||
739
pydcs_extensions/hercules/hercules.py
Normal file
739
pydcs_extensions/hercules/hercules.py
Normal 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
|
||||
@ -1,11 +1,13 @@
|
||||
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.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
|
||||
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 = [
|
||||
frenchpack._FIELD_HIDE,
|
||||
frenchpack._FIELD_HIDE_SMALL,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
from theater.missiontarget import MissionTarget
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from .models import GameModel, PackageModel
|
||||
from .windows.mission.QEditFlightDialog import QEditFlightDialog
|
||||
from .windows.mission.QPackageDialog import (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user