mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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
|
||||
|
||||
34
changelog.md
34
changelog.md
@@ -1,3 +1,37 @@
|
||||
# 2.3.0
|
||||
|
||||
# Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI proucurement for Blue
|
||||
* **[Campaign]** New Campaign: "Black Sea"
|
||||
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
|
||||
* **[Mission Generator]** Infantry squads on frontline can have manpads
|
||||
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
|
||||
* **[Mission Generator]** Opfor now obeys parking limits
|
||||
* **[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.
|
||||
* **[Flight Planner]** Added OCA missions
|
||||
* **[Flight Planner]** Added Alternate/divert airfields
|
||||
* **[Culling]** Added possibility to include/exclude carriers from culling zones
|
||||
* **[QOL]** On liberation startup, your latest save game is loaded automatically
|
||||
* **[Units]** Reduced starting fuel load for C101
|
||||
* **[UI]** Inform the user of the weather
|
||||
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
|
||||
|
||||
## Fixes :
|
||||
* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
|
||||
* **[Mission Generator]** Ground unit waypoints improperly set to "On Road" - fixed
|
||||
* **[Mission Generator]** Target waypoints not at ground level - fixed
|
||||
* **[Mission Generator]** Selected skill not applied to Helicopters - fixed
|
||||
* **[Mission Generator]** Ground units do not always spawn - fixed
|
||||
* **[Kneeboard]** Briefing waypoints off by one - fixed
|
||||
* **[Game]** Destroyed buildings still granting budget - fixed
|
||||
|
||||
# 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),
|
||||
)
|
||||
|
||||
97
game/db.py
97
game/db.py
@@ -104,7 +104,8 @@ from dcs.planes import (
|
||||
Tu_95MS,
|
||||
WingLoong_I,
|
||||
Yak_40,
|
||||
plane_map
|
||||
plane_map,
|
||||
I_16
|
||||
)
|
||||
from dcs.ships import (
|
||||
Armed_speedboat,
|
||||
@@ -113,6 +114,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,
|
||||
@@ -138,6 +140,7 @@ from dcs.task import (
|
||||
SEAD,
|
||||
Task,
|
||||
Transport,
|
||||
RunwayAttack,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport
|
||||
from dcs.unit import Ship, Unit, Vehicle
|
||||
@@ -157,15 +160,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
|
||||
@@ -223,6 +230,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)
|
||||
@@ -245,6 +257,7 @@ PRICES = {
|
||||
|
||||
SpitfireLFMkIX: 14,
|
||||
SpitfireLFMkIXCW: 14,
|
||||
I_16: 10,
|
||||
Bf_109K_4: 14,
|
||||
FW_190D9: 16,
|
||||
FW_190A8: 14,
|
||||
@@ -272,6 +285,7 @@ PRICES = {
|
||||
F_16A: 14,
|
||||
F_14A_135_GR: 20,
|
||||
F_14B: 24,
|
||||
F_22A: 40,
|
||||
Tornado_IDS: 20,
|
||||
Tornado_GR4: 20,
|
||||
|
||||
@@ -326,6 +340,7 @@ PRICES = {
|
||||
KJ_2000: 50,
|
||||
E_3A: 50,
|
||||
C_130: 25,
|
||||
Hercules: 25,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA: 18,
|
||||
@@ -343,6 +358,7 @@ PRICES = {
|
||||
# Modded
|
||||
Rafale_M: 26,
|
||||
Rafale_A_S: 26,
|
||||
Rafale_B: 26,
|
||||
|
||||
# armor
|
||||
Armor.APC_MTLB: 4,
|
||||
@@ -575,6 +591,7 @@ UNIT_BY_TASK = {
|
||||
MiG_31,
|
||||
FA_18C_hornet,
|
||||
F_15C,
|
||||
F_22A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_16A,
|
||||
@@ -589,6 +606,7 @@ UNIT_BY_TASK = {
|
||||
JF_17,
|
||||
F_4E,
|
||||
C_101CC,
|
||||
I_16,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
@@ -630,6 +648,7 @@ UNIT_BY_TASK = {
|
||||
P_47D_40,
|
||||
RQ_1A_Predator,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
@@ -646,14 +665,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,
|
||||
@@ -692,6 +711,7 @@ UNIT_BY_TASK = {
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
Armor.ZBD_04A,
|
||||
Armor.ZBD_04A,
|
||||
@@ -794,6 +814,8 @@ UNIT_BY_TASK = {
|
||||
Armor.StuG_III_Ausf__G,
|
||||
Artillery.M12_GMC,
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
Armor.Daimler_Armoured_Car,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
@@ -957,6 +979,7 @@ COMMON_OVERRIDE = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP",
|
||||
RunwayAttack: "RUNWAY_ATTACK"
|
||||
}
|
||||
|
||||
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
@@ -999,6 +1022,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
F_14A_135_GR: COMMON_OVERRIDE,
|
||||
F_14B: COMMON_OVERRIDE,
|
||||
F_15C: COMMON_OVERRIDE,
|
||||
F_22A: COMMON_OVERRIDE,
|
||||
F_16C_50: COMMON_OVERRIDE,
|
||||
JF_17: COMMON_OVERRIDE,
|
||||
M_2000C: COMMON_OVERRIDE,
|
||||
@@ -1043,6 +1067,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,
|
||||
@@ -1050,6 +1075,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,
|
||||
@@ -1058,6 +1084,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",
|
||||
@@ -1119,7 +1146,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
|
||||
}
|
||||
@@ -1190,6 +1217,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:
|
||||
@@ -1210,29 +1239,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]
|
||||
|
||||
|
||||
@@ -1244,7 +1289,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)
|
||||
|
||||
244
game/game.py
244
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,11 @@ 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.__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 +106,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 +166,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 +199,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,17 +219,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()
|
||||
self.compute_conflicts_position()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
@@ -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
|
||||
@@ -394,12 +391,17 @@ class Game:
|
||||
if cpoint is not None:
|
||||
points.append(cpoint)
|
||||
|
||||
for package in self.blue_ato.packages:
|
||||
points.append(package.target.position)
|
||||
for package in self.red_ato.packages:
|
||||
points.append(package.target.position)
|
||||
|
||||
# Else 0,0, since we need a default value
|
||||
# (in this case this means the whole map is owned by the same player, so it is not an issue)
|
||||
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 +449,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]:
|
||||
@@ -118,8 +113,10 @@ class Base:
|
||||
elif for_task == AirDefence:
|
||||
target_dict = self.aa
|
||||
|
||||
assert target_dict is not None
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
if target_dict is not None:
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
else:
|
||||
logging.error("Unable to determine target dict for " + str(unit_type))
|
||||
|
||||
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
|
||||
|
||||
@@ -155,7 +152,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 +164,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]:
|
||||
936
game/theater/conflicttheater.py
Normal file
936
game/theater/conflicttheater.py
Normal file
@@ -0,0 +1,936 @@
|
||||
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 shapely import geometry
|
||||
from shapely import ops
|
||||
|
||||
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 nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
|
||||
"""Returns the nearest point inside a land exclusion zone from point
|
||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||
if self.is_on_land(point):
|
||||
return point
|
||||
point = geometry.Point(point.x, point.y)
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = None # type: Optional[geometry.Point]
|
||||
nearest_point = None # type: Optional[geometry.Point]
|
||||
for pt in nearest_points:
|
||||
distance = point.distance(pt)
|
||||
if not min_distance or distance < min_distance:
|
||||
min_distance = distance
|
||||
nearest_point = pt
|
||||
assert isinstance(nearest_point, geometry.Point)
|
||||
point = Point(point.x, point.y)
|
||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
||||
new_point = point.point_from_heading(
|
||||
point.heading_between_point(nearest_point),
|
||||
point.distance_to_point(nearest_point) + extend_dist
|
||||
)
|
||||
return new_point
|
||||
|
||||
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
|
||||
722
game/theater/controlpoint.py
Normal file
722
game/theater/controlpoint.py
Normal file
@@ -0,0 +1,722 @@
|
||||
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)
|
||||
from gen.flights.flight import FlightType
|
||||
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):
|
||||
|
||||
433
gen/aircraft.py
433
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,38 @@ 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,
|
||||
PinpointStrike,
|
||||
)
|
||||
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 +75,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 +99,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 +298,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 +323,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 +666,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
|
||||
@@ -703,6 +729,11 @@ class AircraftConflictGenerator:
|
||||
if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
|
||||
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
|
||||
group.load_loadout(payload_name)
|
||||
if not group.units[0].pylons and for_task == RunwayAttack:
|
||||
if PinpointStrike in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
|
||||
logging.warning("No loadout for \"Runway Attack\" for the {}, defaulting to Strike loadout".format(str(unit_type)))
|
||||
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][PinpointStrike]
|
||||
group.load_loadout(payload_name)
|
||||
did_load_loadout = True
|
||||
logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
|
||||
|
||||
@@ -739,25 +770,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 +788,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 +821,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 +902,6 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def _setup_custom_payload(self, flight, group:FlyingGroup):
|
||||
if flight.use_custom_loadout:
|
||||
|
||||
@@ -895,28 +921,72 @@ 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:
|
||||
continue
|
||||
for flight in package.flights:
|
||||
culled = self.game.position_culled(flight.from_cp.position)
|
||||
if flight.client_count == 0 and culled:
|
||||
logging.info("Flight not generated: culled")
|
||||
continue
|
||||
logging.info(f"Generating flight: {flight.unit_type}")
|
||||
group = self.generate_planned_flight(flight.from_cp, country,
|
||||
flight)
|
||||
self.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 +1041,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 +1053,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 +1072,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 +1132,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 +1204,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 +1239,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 +1249,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 +1289,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 +1305,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 +1383,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 +1407,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 +1429,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 +1456,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 +1471,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 +1530,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 +1548,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 +1673,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 +1765,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
|
||||
|
||||
@@ -83,7 +83,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="BTM",
|
||||
atc=AtcData(MHz(4, 250), MHz(131, 0), MHz(40, 400), MHz(260, 0)),
|
||||
ils={
|
||||
"13": ("ILU", MHz(110, 30)),
|
||||
"13": ("ILU", MHz(110, 300)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -96,7 +96,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="KBL",
|
||||
atc=AtcData(MHz(4, 350), MHz(133, 0), MHz(40, 800), MHz(262, 0)),
|
||||
ils={
|
||||
"07": ("IKB", MHz(111, 50)),
|
||||
"07": ("IKB", MHz(111, 500)),
|
||||
},
|
||||
outer_ndb={
|
||||
"07": ("KT", MHz(870, 0)),
|
||||
@@ -115,7 +115,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="TSK",
|
||||
atc=AtcData(MHz(4, 300), MHz(132, 0), MHz(40, 600), MHz(261, 0)),
|
||||
ils={
|
||||
"09": ("ITS", MHz(108, 90)),
|
||||
"09": ("ITS", MHz(108, 900)),
|
||||
},
|
||||
outer_ndb={
|
||||
"09": ("BI", MHz(335, 0)),
|
||||
@@ -134,7 +134,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="KTS",
|
||||
atc=AtcData(MHz(4, 400), MHz(134, 0), MHz(41, 0), MHz(263, 0)),
|
||||
ils={
|
||||
"08": ("IKS", MHz(109, 75)),
|
||||
"08": ("IKS", MHz(109, 750)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -167,7 +167,7 @@ AIRFIELD_DATA = {
|
||||
runway_length=9686,
|
||||
atc=AtcData(MHz(4, 50), MHz(127, 0), MHz(39, 600), MHz(256, 0)),
|
||||
ils={
|
||||
"06": ("ISO", MHz(111, 10)),
|
||||
"06": ("ISO", MHz(111, 100)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -290,8 +290,8 @@ AIRFIELD_DATA = {
|
||||
vor=("MN", MHz(117, 10)),
|
||||
atc=AtcData(MHz(4, 450), MHz(135, 0), MHz(41, 200), MHz(264, 0)),
|
||||
ils={
|
||||
"30": ("IMW", MHz(109, 30)),
|
||||
"12": ("IMD", MHz(111, 70)),
|
||||
"30": ("IMW", MHz(109, 300)),
|
||||
"12": ("IMD", MHz(111, 700)),
|
||||
},
|
||||
outer_ndb={
|
||||
"30": ("NR", MHz(583, 0)),
|
||||
@@ -310,7 +310,7 @@ AIRFIELD_DATA = {
|
||||
runway_length=7082,
|
||||
atc=AtcData(MHz(4, 500), MHz(136, 0), MHz(41, 400), MHz(265, 0)),
|
||||
ils={
|
||||
"24": ("INL", MHz(110, 50)),
|
||||
"24": ("INL", MHz(110, 500)),
|
||||
},
|
||||
outer_ndb={
|
||||
"24": ("NL", MHz(718, 0)),
|
||||
@@ -348,7 +348,7 @@ AIRFIELD_DATA = {
|
||||
runway_length=9327,
|
||||
atc=AtcData(MHz(4, 750), MHz(141, 0), MHz(42, 400), MHz(270, 0)),
|
||||
ils={
|
||||
"10": ("ICH", MHz(110, 50)),
|
||||
"10": ("ICH", MHz(110, 500)),
|
||||
},
|
||||
outer_ndb={
|
||||
"10": ("CX", MHz(1, 5)),
|
||||
@@ -367,8 +367,8 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="GTB",
|
||||
atc=AtcData(MHz(4, 600), MHz(138, 0), MHz(41, 800), MHz(267, 0)),
|
||||
ils={
|
||||
"13": ("INA", MHz(110, 30)),
|
||||
"30": ("INA", MHz(108, 90)),
|
||||
"13": ("INA", MHz(110, 300)),
|
||||
"30": ("INA", MHz(108, 900)),
|
||||
},
|
||||
outer_ndb={
|
||||
"13": ("BP", MHz(342, 0)),
|
||||
@@ -399,8 +399,8 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="VAS",
|
||||
atc=AtcData(MHz(4, 700), MHz(140, 0), MHz(42, 200), MHz(269, 0)),
|
||||
ils={
|
||||
"13": ("IVZ", MHz(108, 75)),
|
||||
"31": ("IVZ", MHz(108, 75)),
|
||||
"13": ("IVZ", MHz(108, 750)),
|
||||
"31": ("IVZ", MHz(108, 750)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1016,8 +1016,8 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="TQQ",
|
||||
atc=AtcData(MHz(3, 800), MHz(124, 750), MHz(38, 500), MHz(257, 950)),
|
||||
ils={
|
||||
"32": ("I-UVV", MHz(111, 70)),
|
||||
"14": ("I-RVP", MHz(108, 30)),
|
||||
"32": ("I-UVV", MHz(111, 700)),
|
||||
"14": ("I-RVP", MHz(108, 300)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1043,7 +1043,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="GRL",
|
||||
atc=AtcData(MHz(3, 850), MHz(118, 0), MHz(38, 600), MHz(250, 50)),
|
||||
ils={
|
||||
"32": ("GLRI", MHz(109, 30)),
|
||||
"32": ("GLRI", MHz(109, 300)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1069,7 +1069,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="INS",
|
||||
atc=AtcData(MHz(3, 825), MHz(118, 300), MHz(38, 550), MHz(360, 600)),
|
||||
ils={
|
||||
"8": ("ICRR", MHz(108, 70)),
|
||||
"8": ("ICRR", MHz(108, 700)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1092,7 +1092,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="LSV",
|
||||
atc=AtcData(MHz(3, 900), MHz(132, 550), MHz(38, 700), MHz(327, 0)),
|
||||
ils={
|
||||
"21": ("IDIQ", MHz(109, 10)),
|
||||
"21": ("IDIQ", MHz(109, 100)),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1113,7 +1113,7 @@ AIRFIELD_DATA = {
|
||||
tacan_callsign="LAS",
|
||||
atc=AtcData(MHz(3, 875), MHz(119, 900), MHz(38, 650), MHz(257, 800)),
|
||||
ils={
|
||||
"25": ("I-LAS", MHz(110, 30)),
|
||||
"25": ("I-LAS", MHz(110, 300)),
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Type
|
||||
from typing import List, Type, Tuple
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
@@ -67,13 +69,24 @@ class AirSupportConflictGenerator:
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
return [Refueling, AWACS]
|
||||
|
||||
def generate(self, is_awacs_enabled):
|
||||
@staticmethod
|
||||
def _get_tanker_params(unit_type: Type[UnitType]) -> Tuple[int, int]:
|
||||
if unit_type is KC130:
|
||||
return (TANKER_ALT - 500, 596)
|
||||
elif unit_type is KC_135:
|
||||
return (TANKER_ALT, 770)
|
||||
elif unit_type is KC135MPRS:
|
||||
return (TANKER_ALT + 500, 596)
|
||||
return (TANKER_ALT, 574)
|
||||
|
||||
def generate(self):
|
||||
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
|
||||
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
|
||||
@@ -84,11 +97,11 @@ class AirSupportConflictGenerator:
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=TANKER_ALT,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=574,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
@@ -120,26 +133,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")
|
||||
684
gen/armor.py
684
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,14 +169,23 @@ 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):
|
||||
def gen_infantry_group_for_group(
|
||||
self,
|
||||
group: VehicleGroup,
|
||||
is_player: bool,
|
||||
side: Country,
|
||||
forward_heading: int
|
||||
) -> None:
|
||||
|
||||
# Disable infantry unit gen if disabled
|
||||
if not self.game.settings.perf_infantry:
|
||||
infantry_position = self.conflict.find_ground_position(
|
||||
group.points[0].position.random_point_within(250, 50),
|
||||
500,
|
||||
forward_heading,
|
||||
self.conflict.theater
|
||||
)
|
||||
if not infantry_position:
|
||||
logging.warning("Could not find infantry position")
|
||||
return
|
||||
|
||||
infantry_position = group.points[0].position.random_point_within(250, 50)
|
||||
|
||||
if side == self.conflict.attackers_country:
|
||||
cp = self.conflict.from_cp
|
||||
else:
|
||||
@@ -180,7 +196,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 +237,217 @@ class GroundConflictGenerator:
|
||||
heading=forward_heading,
|
||||
move_formation=PointAction.OffRoad)
|
||||
|
||||
def _set_reform_waypoint(
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: int
|
||||
) -> None:
|
||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||
rather than spin
|
||||
"""
|
||||
reform_point = dcs_group.position.point_from_heading(forward_heading, 50)
|
||||
dcs_group.add_waypoint(reform_point)
|
||||
|
||||
def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp):
|
||||
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.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
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))
|
||||
# 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[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3))
|
||||
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
|
||||
dcs_group.points[2].tasks.append(Hold())
|
||||
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
|
||||
|
||||
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
|
||||
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.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
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
|
||||
)
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position + rand_offset
|
||||
)
|
||||
dcs_group.add_waypoint(target_point)
|
||||
dcs_group.points[2].tasks.append(AttackGroup(target.id))
|
||||
|
||||
if (
|
||||
to_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<=
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
):
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group,
|
||||
forward_heading,
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
)
|
||||
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 = self.conflict.theater.nearest_land_pos(
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, 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
|
||||
)
|
||||
)
|
||||
target_point = self.conflict.theater.nearest_land_pos(
|
||||
target.points[0].position+rand_offset
|
||||
)
|
||||
dcs_group.add_waypoint(target_point, PointAction.OffRoad)
|
||||
dcs_group.points[i + 1].tasks.append(AttackGroup(target.id))
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = self.conflict.theater.nearest_land_pos(
|
||||
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.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
|
||||
# APC & ATGM will never move too much forward, but will follow along any offensive
|
||||
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
|
||||
attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0))
|
||||
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 +457,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 +477,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,17 +499,30 @@ class GroundConflictGenerator:
|
||||
|
||||
self.mission.triggerrules.triggers.append(fallback)
|
||||
|
||||
|
||||
def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE):
|
||||
def find_retreat_point(
|
||||
self,
|
||||
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
|
||||
:param frontline_heading: Heading of the frontline
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
|
||||
def find_offensive_point(self, dcs_group, frontline_heading, distance):
|
||||
|
||||
def find_offensive_point(
|
||||
self,
|
||||
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
|
||||
@@ -390,26 +530,41 @@ class GroundConflictGenerator:
|
||||
:param distance: Distance of the offensive (how far unit should move)
|
||||
:return: dcs.mapping.Point object with the desired position
|
||||
"""
|
||||
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
||||
desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
|
||||
if self.conflict.theater.is_on_land(desired_point):
|
||||
return desired_point
|
||||
return self.conflict.theater.nearest_land_pos(desired_point)
|
||||
|
||||
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,57 +572,130 @@ 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):
|
||||
@staticmethod
|
||||
def get_artilery_group_distance_from_frontline(group: CombatGroup) -> int:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = group.units[0].threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]:
|
||||
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]
|
||||
if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]:
|
||||
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]
|
||||
)
|
||||
elif rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][0],
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK][1]
|
||||
)
|
||||
return rg
|
||||
|
||||
def get_valid_position_for_group(
|
||||
self,
|
||||
conflict_position: Point,
|
||||
combat_width: int,
|
||||
distance_from_frontline: int,
|
||||
heading: int,
|
||||
spawn_heading: int
|
||||
):
|
||||
shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
|
||||
desired_point = shifted.point_from_heading(
|
||||
spawn_heading,
|
||||
distance_from_frontline
|
||||
)
|
||||
return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater)
|
||||
|
||||
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
|
||||
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)
|
||||
|
||||
if self.conflict.theater.is_on_land(final_position):
|
||||
return final_position
|
||||
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:
|
||||
i = i + 1
|
||||
continue
|
||||
return None
|
||||
distance_from_frontline = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[group.role][0],
|
||||
DISTANCE_FROM_FRONTLINE[group.role][1]
|
||||
)
|
||||
|
||||
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0):
|
||||
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 +706,15 @@ class GroundConflictGenerator:
|
||||
group = self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_unit_name(side, cp.id, unit), unit,
|
||||
position=self._group_point(at),
|
||||
position=at,
|
||||
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,38 @@ 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, coerce=True) -> Optional[Point]:
|
||||
"""
|
||||
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
||||
`coerce=True` will return the closest land position to `initial` regardless of heading or distance
|
||||
`coerce=False` will return None if a point isn't found
|
||||
"""
|
||||
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)
|
||||
if coerce:
|
||||
pos = theater.nearest_land_pos(initial)
|
||||
return pos
|
||||
logging.error("Didn't find ground position ({})!".format(initial))
|
||||
return None
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -193,13 +234,17 @@ class WaypointBuilder:
|
||||
return self._target_area(f"STRIKE {target.name}", target)
|
||||
|
||||
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
|
||||
return self._target_area(f"SEAD on {target.name}", target)
|
||||
return self._target_area(f"SEAD on {target.name}", target, flyover=True)
|
||||
|
||||
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,
|
||||
@@ -187,14 +187,14 @@ class CombatGroupRole(Enum):
|
||||
|
||||
|
||||
DISTANCE_FROM_FRONTLINE = {
|
||||
CombatGroupRole.TANK:3200,
|
||||
CombatGroupRole.APC:8000,
|
||||
CombatGroupRole.IFV:3700,
|
||||
CombatGroupRole.ARTILLERY:18000,
|
||||
CombatGroupRole.SHORAD:13000,
|
||||
CombatGroupRole.LOGI:20000,
|
||||
CombatGroupRole.INFANTRY:3000,
|
||||
CombatGroupRole.ATGM:6200
|
||||
CombatGroupRole.TANK: (2200, 3200),
|
||||
CombatGroupRole.APC: (7500, 8500),
|
||||
CombatGroupRole.IFV: (2700, 3700),
|
||||
CombatGroupRole.ARTILLERY: (16000, 18000),
|
||||
CombatGroupRole.SHORAD: (12000, 13000),
|
||||
CombatGroupRole.LOGI: (18000, 20000),
|
||||
CombatGroupRole.INFANTRY: (2800, 3300),
|
||||
CombatGroupRole.ATGM: (5200, 6200),
|
||||
}
|
||||
|
||||
GROUP_SIZES_BY_COMBAT_STANCE = {
|
||||
|
||||
@@ -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):
|
||||
@@ -54,11 +82,16 @@ class TriggersGenerator:
|
||||
airport.operating_level_air = 0
|
||||
airport.operating_level_equipment = 0
|
||||
airport.operating_level_fuel = 0
|
||||
|
||||
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.id not in cp_ids:
|
||||
airport.unlimited_fuel = True
|
||||
airport.unlimited_munitions = True
|
||||
airport.unlimited_aircrafts = True
|
||||
|
||||
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 +106,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 +137,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
Submodule pydcs updated: 2883be31c2...c9751f54e0
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
|
||||
742
pydcs_extensions/hercules/hercules.py
Normal file
742
pydcs_extensions/hercules/hercules.py
Normal file
@@ -0,0 +1,742 @@
|
||||
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}
|
||||
Herc_GBU_43_B_MOAB_ = {"clsid": "Herc_GBU-43/B(MOAB)", "name": "GBU-43/B(MOAB)", "weight": 9800}
|
||||
|
||||
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
|
||||
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_TOW_missiles = (10, HerculesWeapons.Herc_Ammo_TOW_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_TOW_missiles = (11, HerculesWeapons.Herc_Ammo_TOW_missiles)
|
||||
Herc_GBU_43_B_MOAB_ = (11, HerculesWeapons.Herc_GBU_43_B_MOAB_)
|
||||
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_TOW_missiles = (12, HerculesWeapons.Herc_Ammo_TOW_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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user