diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 60cbf719..0ac6fc4f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ca8a238e..f8346069 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/changelog.md b/changelog.md
index 20b0555a..6ff1821d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,21 @@
+# 2.3.0
+
+# Features/Improvements
+* **[Campaign Map]** Overhauled the campaign model
+* **[Campaign Map]** Possible to add FOB as control points
+* **[Campaign AI]** Overhauled AI recruiting behaviour
+* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
+* **[Mission Generator]** Infantry squads on frontline can have manpads
+* **[Flight Planner]** Added fighter sweep missions.
+* **[Flight Planner]** Added BAI missions.
+* **[Flight Planner]** Added anti-ship missions.
+* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
+* **[Culling]** Added possibility to include/exclude carriers from culling zones
+* **[QOL]** On liberation startup, your latest save game is loaded automatically
+
+## Fixes :
+* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
+
# 2.2.1
# Features/Improvements
diff --git a/game/data/building_data.py b/game/data/building_data.py
index ab2555c3..ae4e2236 100644
--- a/game/data/building_data.py
+++ b/game/data/building_data.py
@@ -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',
diff --git a/game/data/doctrine.py b/game/data/doctrine.py
index 99bb254a..fce67b1b 100644
--- a/game/data/doctrine.py
+++ b/game/data/doctrine.py
@@ -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),
)
diff --git a/game/db.py b/game/db.py
index 7bbc9a81..092672b4 100644
--- a/game/db.py
+++ b/game/db.py
@@ -106,7 +106,8 @@ from dcs.planes import (
Tu_95MS,
WingLoong_I,
Yak_40,
- plane_map
+ plane_map,
+ I_16
)
from dcs.ships import (
Armed_speedboat,
@@ -115,6 +116,7 @@ from dcs.ships import (
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
+ CVN_75_Harry_S__Truman,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
Dry_cargo_ship_Ivanov,
@@ -159,15 +161,19 @@ import pydcs_extensions.frenchpack.frenchpack as frenchpack
# PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C
+from pydcs_extensions.f22a.f22a import F_22A
+from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN
-from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
+from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57
plane_map["A-4E-C"] = A_4E_C
plane_map["MB-339PAN"] = MB_339PAN
plane_map["Rafale_M"] = Rafale_M
plane_map["Rafale_A_S"] = Rafale_A_S
+plane_map["Rafale_B"] = Rafale_B
plane_map["Su-57"] = Su_57
+plane_map["Hercules"] = Hercules
vehicle_map["FieldHL"] = frenchpack._FIELD_HIDE
vehicle_map["HARRIERH"] = frenchpack._FIELD_HIDE_SMALL
@@ -225,6 +231,11 @@ from this example `Identifier` should be used (which may or may not include cate
For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla is called `AirDefence.SAM_SA_18_Igla_S_MANPADS`
"""
+# This should probably be much higher, but the AI doesn't rollover their budget
+# and isn't smart enough to save to repair a critical runway anyway, so it has
+# to be cheap enough to repair with a single turn's income.
+RUNWAY_REPAIR_COST = 100
+
"""
Prices for the aircraft.
This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable)
@@ -247,6 +258,7 @@ PRICES = {
SpitfireLFMkIX: 14,
SpitfireLFMkIXCW: 14,
+ I_16: 10,
Bf_109K_4: 14,
FW_190D9: 16,
FW_190A8: 14,
@@ -274,6 +286,7 @@ PRICES = {
F_16A: 14,
F_14A_135_GR: 20,
F_14B: 24,
+ F_22A: 40,
Tornado_IDS: 20,
Tornado_GR4: 20,
@@ -330,6 +343,7 @@ PRICES = {
KJ_2000: 50,
E_3A: 50,
C_130: 25,
+ Hercules: 25,
# WW2
P_51D_30_NA: 18,
@@ -347,6 +361,7 @@ PRICES = {
# Modded
Rafale_M: 26,
Rafale_A_S: 26,
+ Rafale_B: 26,
# armor
Armor.APC_MTLB: 4,
@@ -579,6 +594,7 @@ UNIT_BY_TASK = {
MiG_31,
FA_18C_hornet,
F_15C,
+ F_22A,
F_14A_135_GR,
F_14B,
F_16A,
@@ -593,6 +609,7 @@ UNIT_BY_TASK = {
JF_17,
F_4E,
C_101CC,
+ I_16,
Bf_109K_4,
FW_190D9,
FW_190A8,
@@ -635,6 +652,7 @@ UNIT_BY_TASK = {
P_47D_40,
RQ_1A_Predator,
Rafale_A_S,
+ Rafale_B,
SA342L,
SA342M,
SA342Minigun,
@@ -651,14 +669,14 @@ UNIT_BY_TASK = {
Tu_95MS,
UH_1H,
WingLoong_I,
+ Hercules
],
Transport: [
IL_76MD,
An_26B,
An_30M,
Yak_40,
-
- C_130,
+ C_130
],
Refueling: [
IL_78M,
@@ -1010,6 +1028,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE,
F_111F: COMMON_OVERRIDE,
+ F_22A: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE,
JF_17: COMMON_OVERRIDE,
M_2000C: COMMON_OVERRIDE,
@@ -1054,6 +1073,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
FW_190D9: COMMON_OVERRIDE,
FW_190A8: COMMON_OVERRIDE,
Bf_109K_4: COMMON_OVERRIDE,
+ I_16: COMMON_OVERRIDE,
SpitfireLFMkIXCW: COMMON_OVERRIDE,
SpitfireLFMkIX: COMMON_OVERRIDE,
A_20G: COMMON_OVERRIDE,
@@ -1061,6 +1081,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
MB_339PAN: COMMON_OVERRIDE,
Rafale_M: COMMON_OVERRIDE,
Rafale_A_S: COMMON_OVERRIDE,
+ Rafale_B: COMMON_OVERRIDE,
OH_58D: COMMON_OVERRIDE,
F_16A: COMMON_OVERRIDE,
MQ_9_Reaper: COMMON_OVERRIDE,
@@ -1069,6 +1090,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
AH_1W: COMMON_OVERRIDE,
AH_64D: COMMON_OVERRIDE,
AH_64A: COMMON_OVERRIDE,
+ Hercules: COMMON_OVERRIDE,
Su_25TM: {
SEAD: "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410",
@@ -1130,7 +1152,7 @@ TIME_PERIODS = {
}
REWARDS = {
- "power": 4, "warehouse": 2, "fuel": 2, "ammo": 2,
+ "power": 4, "warehouse": 2, "ware": 2, "fuel": 2, "ammo": 2,
"farp": 1, "fob": 1, "factory": 10, "comms": 10, "oil": 10,
"derrick": 8
}
@@ -1201,6 +1223,8 @@ def upgrade_to_supercarrier(unit, name: str):
return CVN_72_Abraham_Lincoln
elif name == "CVN-73 George Washington":
return CVN_73_George_Washington
+ elif name == "CVN-75 Harry S. Truman":
+ return CVN_75_Harry_S__Truman
else:
return CVN_71_Theodore_Roosevelt
elif unit == CV_1143_5_Admiral_Kuznetsov:
@@ -1221,29 +1245,45 @@ def unit_task(unit: UnitType) -> Optional[Task]:
return None
-def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
+def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]:
return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units]
-def find_infantry(country_name: str) -> List[UnitType]:
- inf = [
- Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
- Infantry.Paratrooper_AKS,
- Infantry.Soldier_RPG,
- Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
- Infantry.Soldier_M249,
- Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
- Infantry.Paratrooper_RPG_16,
- Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
- Infantry.Georgian_soldier_with_M4,
- Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
- Infantry.Infantry_Soldier_Rus,
- Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
- Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
- Infantry.Infantry_Mauser_98,
- Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
- Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
- ]
+MANPADS: List[VehicleType] = [
+ AirDefence.SAM_SA_18_Igla_MANPADS,
+ AirDefence.SAM_SA_18_Igla_S_MANPADS,
+ AirDefence.Stinger_MANPADS
+]
+
+INFANTRY: List[VehicleType] = [
+ Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
+ Infantry.Paratrooper_AKS,
+ Infantry.Soldier_RPG,
+ Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
+ Infantry.Soldier_M249,
+ Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
+ Infantry.Paratrooper_RPG_16,
+ Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
+ Infantry.Georgian_soldier_with_M4,
+ Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
+ Infantry.Infantry_Soldier_Rus,
+ Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
+ Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
+ Infantry.Infantry_Mauser_98,
+ Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
+ Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
+]
+
+
+def find_manpad(country_name: str) -> List[VehicleType]:
+ return [x for x in MANPADS if x in FACTIONS[country_name].infantry_units]
+
+
+def find_infantry(country_name: str, allow_manpad: bool = False) -> List[VehicleType]:
+ if allow_manpad:
+ inf = INFANTRY + MANPADS
+ else:
+ inf = INFANTRY
return [x for x in inf if x in FACTIONS[country_name].infantry_units]
@@ -1255,7 +1295,7 @@ def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
-def unit_type_from_name(name: str) -> Optional[UnitType]:
+def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:
diff --git a/game/debriefing.py b/game/debriefing.py
index 6bf15569..2bf879c6 100644
--- a/game/debriefing.py
+++ b/game/debriefing.py
@@ -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
-
diff --git a/game/event/airwar.py b/game/event/airwar.py
new file mode 100644
index 00000000..ed22f3af
--- /dev/null
+++ b/game/event/airwar.py
@@ -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"
diff --git a/game/event/event.py b/game/event/event.py
index 8cc4aea7..4ecd5329 100644
--- a/game/event/event.py
+++ b/game/event/event.py
@@ -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)
diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py
index 0046526d..6dab825d 100644
--- a/game/event/frontlineattack.py
+++ b/game/event/frontlineattack.py
@@ -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
diff --git a/game/factions/faction.py b/game/factions/faction.py
index b0caf4bb..d3b14134 100644
--- a/game/factions/faction.py
+++ b/game/factions/faction.py
@@ -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)
diff --git a/game/game.py b/game/game.py
index 7308a128..286b8dc3 100644
--- a/game/game.py
+++ b/game/game.py
@@ -1,14 +1,13 @@
import logging
-import math
import random
import sys
from datetime import date, datetime, timedelta
+from enum import Enum
from typing import Dict, List
from dcs.action import Coalition
from dcs.mapping import Point
-from dcs.task import CAP, CAS, PinpointStrike, Task
-from dcs.unittype import UnitType
+from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import db
@@ -21,15 +20,16 @@ from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner import GroundPlanner
-from theater import ConflictTheater, ControlPoint
-from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
+from .procurement import ProcurementAi
from .settings import Settings
+from .theater import ConflictTheater, ControlPoint
+from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
@@ -62,17 +62,19 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
# cost of AWACS for single operation
AWACS_BUDGET_COST = 4
-# Initial budget value
-PLAYER_BUDGET_INITIAL = 650
-
# Bonus multiplier logarithm base
PLAYER_BUDGET_IMPORTANCE_LOG = 2
+class TurnState(Enum):
+ WIN = 0
+ LOSS = 1
+ CONTINUE = 2
class Game:
def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime,
- settings: Settings):
+ settings: Settings, player_budget: int,
+ enemy_budget: int) -> None:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
@@ -87,10 +89,12 @@ class Game:
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
- self.__culling_points = self.compute_conflicts_position()
+ self.__culling_points: List[Point] = []
+ self.compute_conflicts_position()
self.__destroyed_units: List[str] = []
self.savepath = ""
- self.budget = PLAYER_BUDGET_INITIAL
+ self.budget = player_budget
+ self.enemy_budget = enemy_budget
self.current_unit_id = 0
self.current_group_id = 0
@@ -103,9 +107,24 @@ class Game:
self.theater.controlpoints
)
+ for cp in self.theater.controlpoints:
+ cp.pending_unit_deliveries = self.units_delivery_event(cp)
+
self.sanitize_sides()
+
self.on_load()
+ # Turn 0 procurement. We don't actually have any missions to plan, but
+ # the planner will tell us what it would like to plan so we can use that
+ # to drive purchase decisions.
+ blue_planner = CoalitionMissionPlanner(self, is_player=True)
+ blue_planner.plan_missions()
+
+ red_planner = CoalitionMissionPlanner(self, is_player=False)
+ red_planner.plan_missions()
+
+ self.plan_procurement(blue_planner, red_planner)
+
def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings)
@@ -148,23 +167,29 @@ class Game:
front_line.control_point_b)
@property
- def budget_reward_amount(self):
- reward = 0
- if len(self.theater.player_points()) > 0:
- reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
- for cp in self.theater.player_points():
- for g in cp.ground_objects:
- if g.category in REWARDS.keys():
- reward = reward + REWARDS[g.category]
- return reward
- else:
- return reward
+ def budget_reward_amount(self) -> int:
+ reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
+ for cp in self.theater.player_points():
+ for g in cp.ground_objects:
+ if g.category in REWARDS.keys() and not g.is_dead:
+ reward += REWARDS[g.category]
+ return int(reward * self.settings.player_income_multiplier)
- def _budget_player(self):
+ def process_player_income(self):
self.budget += self.budget_reward_amount
- def awacs_expense_commit(self):
- self.budget -= AWACS_BUDGET_COST
+ def process_enemy_income(self):
+ # TODO: Clean up save compat.
+ if not hasattr(self, "enemy_budget"):
+ self.enemy_budget = 0
+
+ production = 0.0
+ for enemy_point in self.theater.enemy_points():
+ for g in enemy_point.ground_objects:
+ if g.category in REWARDS.keys() and not g.is_dead:
+ production = production + REWARDS[g.category]
+
+ self.enemy_budget += production * self.settings.enemy_income_multiplier
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
event = UnitsDeliveryEvent(attacker_name=self.player_name,
@@ -175,20 +200,16 @@ class Game:
self.events.append(event)
return event
- def units_delivery_remove(self, event: Event):
- if event in self.events:
- self.events.remove(event)
-
- def initiate_event(self, event: Event):
+ def initiate_event(self, event: Event) -> UnitMap:
#assert event in self.events
logging.info("Generating {} (regular)".format(event))
- event.generate()
+ return event.generate()
def finish_event(self, event: Event, debriefing: Debriefing):
logging.info("Finishing event {}".format(event))
event.commit(debriefing)
- if event.is_successfull(debriefing):
- self.budget += event.bonus()
+ self.budget += int(event.bonus() *
+ self.settings.player_income_multiplier)
if event in self.events:
self.events.remove(event)
@@ -199,18 +220,12 @@ class Game:
if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name
else:
- return event and event.name and event.name == self.player_name
+ raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self) -> None:
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
- # Save game compatibility.
-
- # TODO: Remove in 2.3.
- if not hasattr(self, "conditions"):
- self.conditions = self.generate_conditions()
-
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@@ -224,8 +239,12 @@ class Game:
else:
event.skip()
- self._enemy_reinforcement()
- self._budget_player()
+ for control_point in self.theater.controlpoints:
+ control_point.process_turn()
+
+ self.process_enemy_income()
+
+ self.process_player_income()
if not no_action and self.turn > 1:
for cp in self.theater.player_points():
@@ -242,6 +261,14 @@ class Game:
# Autosave progress
persistency.autosave(self)
+ def check_win_loss(self):
+ captured_states = {i.captured for i in self.theater.controlpoints}
+ if True not in captured_states:
+ return TurnState.LOSS
+ if False not in captured_states:
+ return TurnState.WIN
+ return TurnState.CONTINUE
+
def initialize_turn(self) -> None:
self.events = []
self._generate_events()
@@ -251,92 +278,56 @@ class Game:
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
+ cp.pending_unit_deliveries = self.units_delivery_event(cp)
self.aircraft_inventory.set_from_control_point(cp)
+ # Check for win or loss condition
+ turn_state = self.check_win_loss()
+ if turn_state in (TurnState.LOSS,TurnState.WIN):
+ return self.process_win_loss(turn_state)
+
# Plan flights & combat for next turn
- self.__culling_points = self.compute_conflicts_position()
+ self.compute_conflicts_position()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
- CoalitionMissionPlanner(self, is_player=True).plan_missions()
- CoalitionMissionPlanner(self, is_player=False).plan_missions()
+
+ blue_planner = CoalitionMissionPlanner(self, is_player=True)
+ blue_planner.plan_missions()
+
+ red_planner = CoalitionMissionPlanner(self, is_player=False)
+ red_planner.plan_missions()
+
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
- def _enemy_reinforcement(self):
- """
- Compute and commision reinforcement for enemy bases
- """
+ self.plan_procurement(blue_planner, red_planner)
- MAX_ARMOR = 30 * self.settings.multiplier
- MAX_AIRCRAFT = 25 * self.settings.multiplier
+ def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
+ red_planner: CoalitionMissionPlanner) -> None:
+ self.budget = ProcurementAi(
+ self,
+ for_player=True,
+ faction=self.player_faction,
+ manage_runways=self.settings.automate_runway_repair,
+ manage_front_line=self.settings.automate_front_line_reinforcements,
+ manage_aircraft=self.settings.automate_aircraft_reinforcements
+ ).spend_budget(self.budget, blue_planner.procurement_requests)
- production = 0.0
- for enemy_point in self.theater.enemy_points():
- for g in enemy_point.ground_objects:
- if g.category in REWARDS.keys():
- production = production + REWARDS[g.category]
+ self.enemy_budget = ProcurementAi(
+ self,
+ for_player=False,
+ faction=self.enemy_faction,
+ manage_runways=True,
+ manage_front_line=True,
+ manage_aircraft=True
+ ).spend_budget(self.enemy_budget, red_planner.procurement_requests)
- production = production * 0.75
- budget_for_armored_units = production / 2
- budget_for_aircraft = production / 2
-
- potential_cp_armor = []
- for cp in self.theater.enemy_points():
- for cpe in cp.connected_points:
- if cpe.captured and cp.base.total_armor < MAX_ARMOR:
- potential_cp_armor.append(cp)
- if len(potential_cp_armor) == 0:
- potential_cp_armor = self.theater.enemy_points()
-
- i = 0
- potential_units = db.FACTIONS[self.enemy_name].frontline_units
-
- print("Enemy Recruiting")
- print(potential_cp_armor)
- print(budget_for_armored_units)
- print(potential_units)
-
- if len(potential_units) > 0 and len(potential_cp_armor) > 0:
- while budget_for_armored_units > 0:
- i = i + 1
- if i > 50 or budget_for_armored_units <= 0:
- break
- target_cp = random.choice(potential_cp_armor)
- if target_cp.base.total_armor >= MAX_ARMOR:
- continue
- unit = random.choice(potential_units)
- price = db.PRICES[unit] * 2
- budget_for_armored_units -= price * 2
- target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
- info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
- print(str(info))
- self.informations.append(info)
-
- if budget_for_armored_units > 0:
- budget_for_aircraft += budget_for_armored_units
-
- potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
- if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
-
- if len(potential_units) > 0 and len(potential_cp_armor) > 0:
- while budget_for_aircraft > 0:
- i = i + 1
- if i > 50 or budget_for_aircraft <= 0:
- break
- target_cp = random.choice(potential_cp_armor)
- if target_cp.base.total_planes >= MAX_AIRCRAFT:
- continue
- unit = random.choice(potential_units)
- price = db.PRICES[unit] * 2
- budget_for_aircraft -= price * 2
- target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
- info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
- print(str(info))
- self.informations.append(info)
+ def message(self, text: str) -> None:
+ self.informations.append(Information(text, turn=self.turn))
@property
def current_turn_time_of_day(self) -> TimeOfDay:
@@ -369,13 +360,19 @@ class Game:
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
- position = Conflict.frontline_position(self.theater,
- front_line.control_point_a,
- front_line.control_point_b)
+ position = Conflict.frontline_position(front_line.control_point_a,
+ front_line.control_point_b,
+ self.theater)
points.append(position[0])
points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position)
+ # If do_not_cull_carrier is enabled, add carriers as culling point
+ if self.settings.perf_do_not_cull_carrier:
+ for cp in self.theater.controlpoints:
+ if cp.is_carrier or cp.is_lha:
+ points.append(cp.position)
+
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
cpoint = None
@@ -399,7 +396,7 @@ class Game:
if len(points) == 0:
points.append(Point(0, 0))
- return points
+ self.__culling_points = points
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])
@@ -447,4 +444,10 @@ class Game:
return "blue"
def get_enemy_color(self):
- return "red"
\ No newline at end of file
+ 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.")
diff --git a/game/infos/information.py b/game/infos/information.py
index 4fd12d2f..35e94f92 100644
--- a/game/infos/information.py
+++ b/game/infos/information.py
@@ -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
\ No newline at end of file
+ return '[{}][{}] {} {}'.format(
+ self.timestamp.strftime("%Y-%m-%d %H:%M:%S") if self.timestamp is not None else '',
+ self.turn,
+ self.title,
+ self.text
+ )
\ No newline at end of file
diff --git a/game/inventory.py b/game/inventory.py
index 89f5afa1..b369ad8b 100644
--- a/game/inventory.py
+++ b/game/inventory.py
@@ -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
diff --git a/game/models/frontline_data.py b/game/models/frontline_data.py
index 94947135..586ebd58 100644
--- a/game/models/frontline_data.py
+++ b/game/models/frontline_data.py
@@ -1,4 +1,4 @@
-from theater import ControlPoint
+from game.theater import ControlPoint
class FrontlineData:
diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py
deleted file mode 100644
index 4dc18dae..00000000
--- a/game/operation/frontlineattack.py
+++ /dev/null
@@ -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()
diff --git a/game/operation/operation.py b/game/operation/operation.py
index 0ff06ebe..d2c46f58 100644
--- a/game/operation/operation.py
+++ b/game/operation/operation.py
@@ -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)
diff --git a/game/persistency.py b/game/persistency.py
index 617274ea..38bd5550 100644
--- a/game/persistency.py
+++ b/game/persistency.py
@@ -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:
diff --git a/game/plugins/luaplugin.py b/game/plugins/luaplugin.py
index f48bc185..f14d9e08 100644
--- a/game/plugins/luaplugin.py
+++ b/game/plugins/luaplugin.py
@@ -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 = []
diff --git a/game/procurement.py b/game/procurement.py
new file mode 100644
index 00000000..6852c534
--- /dev/null
+++ b/game/procurement.py
@@ -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
diff --git a/game/settings.py b/game/settings.py
index 764e5ff5..42e23761 100644
--- a/game/settings.py
+++ b/game/settings.py
@@ -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:
diff --git a/theater/__init__.py b/game/theater/__init__.py
similarity index 71%
rename from theater/__init__.py
rename to game/theater/__init__.py
index 209a6646..c5b83a16 100644
--- a/theater/__init__.py
+++ b/game/theater/__init__.py
@@ -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
diff --git a/theater/base.py b/game/theater/base.py
similarity index 90%
rename from theater/base.py
rename to game/theater/base.py
index 47b3580e..14b96ce2 100644
--- a/theater/base.py
+++ b/game/theater/base.py
@@ -4,9 +4,8 @@ import math
import typing
from typing import Dict, Type
-from dcs.planes import PlaneType
from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
-from dcs.unittype import UnitType, VehicleType
+from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game import db
@@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0
class Base:
- aircraft = {} # type: typing.Dict[PlaneType, int]
- armor = {} # type: typing.Dict[VehicleType, int]
- aa = {} # type: typing.Dict[AirDefence, int]
- strength = 1 # type: float
def __init__(self):
- self.aircraft = {}
- self.armor = {}
- self.aa = {}
+ self.aircraft: Dict[Type[FlyingType], int] = {}
+ self.armor: Dict[Type[VehicleType], int] = {}
+ self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@property
- def total_planes(self) -> int:
+ def total_aircraft(self) -> int:
return sum(self.aircraft.values())
@property
@@ -83,7 +78,7 @@ class Base:
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
- def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]:
+ def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
@@ -155,7 +150,7 @@ class Base:
if task:
count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task])
else:
- count = self.total_planes
+ count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count)
@@ -167,18 +162,18 @@ class Base:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
- def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
- return self._find_best_planes(CAP, min(self.total_planes, 20))
+ return self._find_best_planes(CAP, min(self.total_aircraft, 20))
- def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
- def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]:
+ def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:
diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py
new file mode 100644
index 00000000..d38e62a6
--- /dev/null
+++ b/game/theater/conflicttheater.py
@@ -0,0 +1,905 @@
+from __future__ import annotations
+
+import itertools
+import json
+import logging
+from dataclasses import dataclass
+from functools import cached_property
+from itertools import tee
+from pathlib import Path
+from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
+
+from dcs import Mission
+from dcs.countries import (
+ CombinedJointTaskForcesBlue,
+ CombinedJointTaskForcesRed,
+)
+from dcs.country import Country
+from dcs.mapping import Point
+from dcs.planes import F_15C
+from dcs.ships import (
+ CVN_74_John_C__Stennis,
+ LHA_1_Tarawa,
+ USS_Arleigh_Burke_IIa,
+)
+from dcs.statics import Fortification
+from dcs.terrain import (
+ caucasus,
+ nevada,
+ normandy,
+ persiangulf,
+ syria,
+ thechannel,
+)
+from dcs.terrain.terrain import Airport, Terrain
+from dcs.unitgroup import (
+ FlyingGroup,
+ Group,
+ ShipGroup,
+ StaticGroup,
+ VehicleGroup,
+)
+from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
+
+from gen.flights.flight import FlightType
+from .controlpoint import (
+ Airfield,
+ Carrier,
+ ControlPoint,
+ Lha,
+ MissionTarget,
+ OffMapSpawn,
+ Fob,
+)
+from .landmap import Landmap, load_landmap, poly_contains
+from ..utils import nm_to_meter
+
+Numeric = Union[int, float]
+
+SIZE_TINY = 150
+SIZE_SMALL = 600
+SIZE_REGULAR = 1000
+SIZE_BIG = 2000
+SIZE_LARGE = 3000
+
+IMPORTANCE_LOW = 1
+IMPORTANCE_MEDIUM = 1.2
+IMPORTANCE_HIGH = 1.4
+
+FRONTLINE_MIN_CP_DISTANCE = 5000
+
+def pairwise(iterable):
+ """
+ itertools recipe
+ s -> (s0,s1), (s1,s2), (s2, s3), ...
+ """
+ a, b = tee(iterable)
+ next(b, None)
+ return zip(a, b)
+
+
+class MizCampaignLoader:
+ BLUE_COUNTRY = CombinedJointTaskForcesBlue()
+ RED_COUNTRY = CombinedJointTaskForcesRed()
+
+ OFF_MAP_UNIT_TYPE = F_15C.id
+
+ CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
+ LHA_UNIT_TYPE = LHA_1_Tarawa.id
+ FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
+
+ FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
+
+ EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
+ SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
+ GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
+ OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
+ SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
+ MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
+ COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
+
+ # Multiple options for the required SAMs so campaign designers can more
+ # accurately see the coverage of their IADS for the expected type.
+ REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
+ AirDefence.SAM_Patriot_LN_M901.id,
+ AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
+ AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
+ }
+
+ REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
+ AirDefence.SAM_Hawk_LN_M192.id,
+ AirDefence.SAM_SA_2_LN_SM_90.id,
+ AirDefence.SAM_SA_3_S_125_LN_5P73.id,
+ }
+
+ BASE_DEFENSE_RADIUS = nm_to_meter(2)
+
+ def __init__(self, miz: Path, theater: ConflictTheater) -> None:
+ self.theater = theater
+ self.mission = Mission()
+ self.mission.load_file(str(miz))
+ self.control_point_id = itertools.count(1000)
+
+ # If there are no red carriers there usually aren't red units. Make sure
+ # both countries are initialized so we don't have to deal with None.
+ if self.mission.country(self.BLUE_COUNTRY.name) is None:
+ self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
+ if self.mission.country(self.RED_COUNTRY.name) is None:
+ self.mission.coalition["red"].add_country(self.RED_COUNTRY)
+
+ @staticmethod
+ def control_point_from_airport(airport: Airport) -> ControlPoint:
+
+ # The wiki says this is a legacy property and to just use regular.
+ size = SIZE_REGULAR
+
+ # The importance is taken from the periodicity of the airport's
+ # warehouse divided by 10. 30 is the default, and out of range (valid
+ # values are between 1.0 and 1.4). If it is used, pick the default
+ # importance.
+ if airport.periodicity == 30:
+ importance = IMPORTANCE_MEDIUM
+ else:
+ importance = airport.periodicity / 10
+
+ cp = Airfield(airport, size, importance)
+ cp.captured = airport.is_blue()
+
+ # Use the unlimited aircraft option to determine if an airfield should
+ # be owned by the player when the campaign is "inverted".
+ cp.captured_invert = airport.unlimited_aircrafts
+
+ return cp
+
+ def country(self, blue: bool) -> Country:
+ country = self.mission.country(
+ self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
+ # Should be guaranteed because we initialized them.
+ assert country
+ return country
+
+ @property
+ def blue(self) -> Country:
+ return self.country(blue=True)
+
+ @property
+ def red(self) -> Country:
+ return self.country(blue=False)
+
+ def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
+ for group in self.country(blue).plane_group:
+ if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
+ yield group
+
+ def carriers(self, blue: bool) -> Iterator[ShipGroup]:
+ for group in self.country(blue).ship_group:
+ if group.units[0].type == self.CV_UNIT_TYPE:
+ yield group
+
+ def lhas(self, blue: bool) -> Iterator[ShipGroup]:
+ for group in self.country(blue).ship_group:
+ if group.units[0].type == self.LHA_UNIT_TYPE:
+ yield group
+
+ def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
+ for group in self.country(blue).vehicle_group:
+ if group.units[0].type == self.FOB_UNIT_TYPE:
+ yield group
+
+ @property
+ def ships(self) -> Iterator[ShipGroup]:
+ for group in self.blue.ship_group:
+ if group.units[0].type == self.SHIP_UNIT_TYPE:
+ yield group
+
+ @property
+ def ewrs(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.EWR_UNIT_TYPE:
+ yield group
+
+ @property
+ def sams(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.SAM_UNIT_TYPE:
+ yield group
+
+ @property
+ def garrisons(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.GARRISON_UNIT_TYPE:
+ yield group
+
+ @property
+ def offshore_strike_targets(self) -> Iterator[StaticGroup]:
+ for group in self.blue.static_group:
+ if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
+ yield group
+
+ @property
+ def missile_sites(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
+ yield group
+
+ @property
+ def coastal_defenses(self) -> Iterator[VehicleGroup]:
+ for group in self.blue.vehicle_group:
+ if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
+ yield group
+
+ @property
+ def required_long_range_sams(self) -> Iterator[VehicleGroup]:
+ for group in self.red.vehicle_group:
+ if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
+ yield group
+
+ @property
+ def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
+ for group in self.red.vehicle_group:
+ if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
+ yield group
+
+ @cached_property
+ def control_points(self) -> Dict[int, ControlPoint]:
+ control_points = {}
+ for airport in self.mission.terrain.airport_list():
+ if airport.is_blue() or airport.is_red():
+ control_point = self.control_point_from_airport(airport)
+ control_points[control_point.id] = control_point
+
+ for blue in (False, True):
+ for group in self.off_map_spawns(blue):
+ control_point = OffMapSpawn(next(self.control_point_id),
+ str(group.name), group.position)
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+ for group in self.carriers(blue):
+ # TODO: Name the carrier.
+ control_point = Carrier(
+ "carrier", group.position, next(self.control_point_id))
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+ for group in self.lhas(blue):
+ # TODO: Name the LHA.
+ control_point = Lha(
+ "lha", group.position, next(self.control_point_id))
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+ for group in self.fobs(blue):
+ control_point = Fob(
+ str(group.name), group.position, next(self.control_point_id)
+ )
+ control_point.captured = blue
+ control_point.captured_invert = group.late_activation
+ control_points[control_point.id] = control_point
+
+ return control_points
+
+ @property
+ def front_line_path_groups(self) -> Iterator[VehicleGroup]:
+ for group in self.country(blue=True).vehicle_group:
+ if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
+ yield group
+
+ @cached_property
+ def front_lines(self) -> Dict[str, ComplexFrontLine]:
+ # Dict of front line ID to a front line.
+ front_lines = {}
+ for group in self.front_line_path_groups:
+ # The unit will have its first waypoint at the source CP and the
+ # final waypoint at the destination CP. Intermediate waypoints
+ # define the curve of the front line.
+ waypoints = [p.position for p in group.points]
+ origin = self.theater.closest_control_point(waypoints[0])
+ if origin is None:
+ raise RuntimeError(
+ f"No control point near the first waypoint of {group.name}")
+ destination = self.theater.closest_control_point(waypoints[-1])
+ if destination is None:
+ raise RuntimeError(
+ f"No control point near the final waypoint of {group.name}")
+
+ # Snap the begin and end points to the control points.
+ waypoints[0] = origin.position
+ waypoints[-1] = destination.position
+ front_line_id = f"{origin.id}|{destination.id}"
+ front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
+ self.control_points[origin.id].connect(
+ self.control_points[destination.id])
+ self.control_points[destination.id].connect(
+ self.control_points[origin.id])
+ return front_lines
+
+ def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
+ closest = self.theater.closest_control_point(group.position)
+ distance = closest.position.distance_to_point(group.position)
+ return closest, distance
+
+ def add_preset_locations(self) -> None:
+ for group in self.garrisons:
+ closest, distance = self.objective_info(group)
+ if distance < self.BASE_DEFENSE_RADIUS:
+ closest.preset_locations.base_garrisons.append(group.position)
+ else:
+ logging.warning(
+ f"Found garrison unit too far from base: {group.name}")
+
+ for group in self.sams:
+ closest, distance = self.objective_info(group)
+ if distance < self.BASE_DEFENSE_RADIUS:
+ closest.preset_locations.base_air_defense.append(group.position)
+ else:
+ closest.preset_locations.strike_locations.append(group.position)
+
+ for group in self.ewrs:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.ewrs.append(group.position)
+
+ for group in self.offshore_strike_targets:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.offshore_strike_locations.append(
+ group.position)
+
+ for group in self.ships:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.ships.append(group.position)
+
+ for group in self.missile_sites:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.missile_sites.append(group.position)
+
+ for group in self.coastal_defenses:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.coastal_defenses.append(group.position)
+
+ for group in self.required_long_range_sams:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.required_long_range_sams.append(
+ group.position
+ )
+
+ for group in self.required_medium_range_sams:
+ closest, distance = self.objective_info(group)
+ closest.preset_locations.required_medium_range_sams.append(
+ group.position
+ )
+
+ def populate_theater(self) -> None:
+ for control_point in self.control_points.values():
+ self.theater.add_controlpoint(control_point)
+ self.add_preset_locations()
+ self.theater.set_frontline_data(self.front_lines)
+
+
+@dataclass
+class ReferencePoint:
+ world_coordinates: Point
+ image_coordinates: Point
+
+
+class ConflictTheater:
+ terrain: Terrain
+
+ reference_points: Tuple[ReferencePoint, ReferencePoint]
+ overview_image: str
+ landmap: Optional[Landmap]
+ """
+ land_poly = None # type: Polygon
+ """
+ daytime_map: Dict[str, Tuple[int, int]]
+ _frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
+
+ def __init__(self):
+ self.controlpoints: List[ControlPoint] = []
+ self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
+ """
+ self.land_poly = geometry.Polygon(self.landmap[0][0])
+ for x in self.landmap[1]:
+ self.land_poly = self.land_poly.difference(geometry.Polygon(x))
+ """
+
+ @property
+ def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
+ if self._frontline_data is None:
+ self.load_frontline_data_from_file()
+ return self._frontline_data
+
+ def load_frontline_data_from_file(self) -> None:
+ if self._frontline_data is not None:
+ logging.warning("Replacing existing frontline data from file")
+ self._frontline_data = FrontLine.load_json_frontlines(self)
+ if self._frontline_data is None:
+ self._frontline_data = {}
+
+ def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
+ if self._frontline_data is not None:
+ logging.warning("Replacing existing frontline data")
+ self._frontline_data = data
+
+ def add_controlpoint(self, point: ControlPoint,
+ connected_to: Optional[List[ControlPoint]] = None):
+ if connected_to is None:
+ connected_to = []
+ for connected_point in connected_to:
+ point.connect(to=connected_point)
+
+ self.controlpoints.append(point)
+
+ def find_ground_objects_by_obj_name(self, obj_name):
+ found = []
+ for cp in self.controlpoints:
+ for g in cp.ground_objects:
+ if g.obj_name == obj_name:
+ found.append(g)
+ return found
+
+ def is_in_sea(self, point: Point) -> bool:
+ if not self.landmap:
+ return False
+
+ if self.is_on_land(point):
+ return False
+
+ for exclusion_zone in self.landmap[1]:
+ if poly_contains(point.x, point.y, exclusion_zone):
+ return False
+
+ for sea in self.landmap[2]:
+ if poly_contains(point.x, point.y, sea):
+ return True
+
+ return False
+
+ def is_on_land(self, point: Point) -> bool:
+ if not self.landmap:
+ return True
+
+ is_point_included = False
+ for inclusion_zone in self.landmap[0]:
+ if poly_contains(point.x, point.y, inclusion_zone):
+ is_point_included = True
+
+ if not is_point_included:
+ return False
+
+ for exclusion_zone in self.landmap[1]:
+ if poly_contains(point.x, point.y, exclusion_zone):
+ return False
+
+ return True
+
+ def player_points(self) -> List[ControlPoint]:
+ return [point for point in self.controlpoints if point.captured]
+
+ def conflicts(self, from_player=True) -> Iterator[FrontLine]:
+ for cp in [x for x in self.controlpoints if x.captured == from_player]:
+ for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
+ yield FrontLine(cp, connected_point, self)
+
+ def enemy_points(self) -> List[ControlPoint]:
+ return [point for point in self.controlpoints if not point.captured]
+
+ def closest_control_point(self, point: Point) -> ControlPoint:
+ closest = self.controlpoints[0]
+ closest_distance = point.distance_to_point(closest.position)
+ for control_point in self.controlpoints[1:]:
+ distance = point.distance_to_point(control_point.position)
+ if distance < closest_distance:
+ closest = control_point
+ closest_distance = distance
+ return closest
+
+ def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
+ """
+ Returns a tuple of the two nearest opposing ControlPoints in theater.
+ (player_cp, enemy_cp)
+ """
+ all_cp_min_distances = {}
+ for idx, control_point in enumerate(self.controlpoints):
+ distances = {}
+ closest_distance = None
+ for i, cp in enumerate(self.controlpoints):
+ if i != idx and cp.captured is not control_point.captured:
+ dist = cp.position.distance_to_point(control_point.position)
+ if not closest_distance:
+ closest_distance = dist
+ distances[cp.id] = dist
+ if dist < closest_distance:
+ distances[cp.id] = dist
+ closest_cp_id = min(distances, key=distances.get) # type: ignore
+
+ all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[closest_cp_id]
+ closest_opposing_cps = [
+ self.find_control_point_by_id(i)
+ for i
+ in min(all_cp_min_distances, key=all_cp_min_distances.get) # type: ignore
+ ] # type: List[ControlPoint]
+ assert len(closest_opposing_cps) == 2
+ if closest_opposing_cps[0].captured:
+ return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
+ else:
+ return cast(Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)))
+
+ def find_control_point_by_id(self, id: int) -> ControlPoint:
+ for i in self.controlpoints:
+ if i.id == id:
+ return i
+ raise RuntimeError(f"Cannot find ControlPoint with ID {id}")
+
+ def add_json_cp(self, theater, p: dict) -> ControlPoint:
+ cp: ControlPoint
+ if p["type"] == "airbase":
+
+ airbase = theater.terrain.airports[p["id"]]
+
+ if "size" in p.keys():
+ size = p["size"]
+ else:
+ size = SIZE_REGULAR
+
+ if "importance" in p.keys():
+ importance = p["importance"]
+ else:
+ importance = IMPORTANCE_MEDIUM
+
+ cp = Airfield(airbase, size, importance)
+ elif p["type"] == "carrier":
+ cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
+ else:
+ cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
+
+ if "captured_invert" in p.keys():
+ cp.captured_invert = p["captured_invert"]
+ else:
+ cp.captured_invert = False
+
+ return cp
+
+ @staticmethod
+ def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
+ theaters = {
+ "Caucasus": CaucasusTheater,
+ "Nevada": NevadaTheater,
+ "Persian Gulf": PersianGulfTheater,
+ "Normandy": NormandyTheater,
+ "The Channel": TheChannelTheater,
+ "Syria": SyriaTheater,
+ }
+ theater = theaters[data["theater"]]
+ t = theater()
+
+ miz = data.get("miz", None)
+ if miz is not None:
+ MizCampaignLoader(directory / miz, t).populate_theater()
+ return t
+
+ cps = {}
+ for p in data["player_points"]:
+ cp = t.add_json_cp(theater, p)
+ cp.captured = True
+ cps[p["id"]] = cp
+ t.add_controlpoint(cp)
+
+ for p in data["enemy_points"]:
+ cp = t.add_json_cp(theater, p)
+ cps[p["id"]] = cp
+ t.add_controlpoint(cp)
+
+ for l in data["links"]:
+ cps[l[0]].connect(cps[l[1]])
+ cps[l[1]].connect(cps[l[0]])
+
+ return t
+
+
+class CaucasusTheater(ConflictTheater):
+ terrain = caucasus.Caucasus()
+ overview_image = "caumap.gif"
+ reference_points = (
+ ReferencePoint(caucasus.Gelendzhik.position, Point(176, 298)),
+ ReferencePoint(caucasus.Batumi.position, Point(1307, 1205)),
+ )
+
+ landmap = load_landmap("resources\\caulandmap.p")
+ daytime_map = {
+ "dawn": (6, 9),
+ "day": (9, 18),
+ "dusk": (18, 20),
+ "night": (0, 5),
+ }
+
+
+class PersianGulfTheater(ConflictTheater):
+ terrain = persiangulf.PersianGulf()
+ overview_image = "persiangulf.gif"
+ reference_points = (
+ ReferencePoint(persiangulf.Jiroft_Airport.position,
+ Point(1692, 1343)),
+ ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
+ )
+ landmap = load_landmap("resources\\gulflandmap.p")
+ daytime_map = {
+ "dawn": (6, 8),
+ "day": (8, 16),
+ "dusk": (16, 18),
+ "night": (0, 5),
+ }
+
+
+class NevadaTheater(ConflictTheater):
+ terrain = nevada.Nevada()
+ overview_image = "nevada.gif"
+ reference_points = (
+ ReferencePoint(nevada.Mina_Airport_3Q0.position, Point(252, 295)),
+ ReferencePoint(nevada.Laughlin_Airport.position, Point(844, 909)),
+ )
+ landmap = load_landmap("resources\\nevlandmap.p")
+ daytime_map = {
+ "dawn": (4, 6),
+ "day": (6, 17),
+ "dusk": (17, 18),
+ "night": (0, 5),
+ }
+
+
+class NormandyTheater(ConflictTheater):
+ terrain = normandy.Normandy()
+ overview_image = "normandy.gif"
+ reference_points = (
+ ReferencePoint(normandy.Needs_Oar_Point.position, Point(515, 329)),
+ ReferencePoint(normandy.Evreux.position, Point(2029, 1709)),
+ )
+ landmap = load_landmap("resources\\normandylandmap.p")
+ daytime_map = {
+ "dawn": (6, 8),
+ "day": (10, 17),
+ "dusk": (17, 18),
+ "night": (0, 5),
+ }
+
+
+class TheChannelTheater(ConflictTheater):
+ terrain = thechannel.TheChannel()
+ overview_image = "thechannel.gif"
+ reference_points = (
+ ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)),
+ ReferencePoint(thechannel.Detling.position, Point(706, 382))
+ )
+ landmap = load_landmap("resources\\channellandmap.p")
+ daytime_map = {
+ "dawn": (6, 8),
+ "day": (10, 17),
+ "dusk": (17, 18),
+ "night": (0, 5),
+ }
+
+
+class SyriaTheater(ConflictTheater):
+ terrain = syria.Syria()
+ overview_image = "syria.gif"
+ reference_points = (
+ ReferencePoint(syria.Eyn_Shemer.position, Point(564, 1289)),
+ ReferencePoint(syria.Tabqa.position, Point(1329, 491)),
+ )
+ landmap = load_landmap("resources\\syrialandmap.p")
+ daytime_map = {
+ "dawn": (6, 8),
+ "day": (8, 16),
+ "dusk": (16, 18),
+ "night": (0, 5),
+ }
+
+
+@dataclass
+class ComplexFrontLine:
+ """
+ Stores data necessary for building a multi-segment frontline.
+ "points" should be ordered from closest to farthest distance originating from start_cp.position
+ """
+
+ start_cp: ControlPoint
+ points: List[Point]
+
+
+@dataclass
+class FrontLineSegment:
+ """
+ Describes a line segment of a FrontLine
+ """
+
+ point_a: Point
+ point_b: Point
+
+ @property
+ def attack_heading(self) -> Numeric:
+ """The heading of the frontline segment from player to enemy control point"""
+ return self.point_a.heading_between_point(self.point_b)
+
+ @property
+ def attack_distance(self) -> Numeric:
+ """Length of the segment"""
+ return self.point_a.distance_to_point(self.point_b)
+
+
+class FrontLine(MissionTarget):
+ """Defines a front line location between two control points.
+ Front lines are the area where ground combat happens.
+ Overwrites the entirety of MissionTarget __init__ method to allow for
+ dynamic position calculation.
+ """
+
+ def __init__(
+ self,
+ control_point_a: ControlPoint,
+ control_point_b: ControlPoint,
+ theater: ConflictTheater
+ ) -> None:
+ self.control_point_a = control_point_a
+ self.control_point_b = control_point_b
+ self.segments: List[FrontLineSegment] = []
+ self.theater = theater
+ self._build_segments()
+ self.name = f"Front line {control_point_a}/{control_point_b}"
+
+ def is_friendly(self, to_player: bool) -> bool:
+ """Returns True if the objective is in friendly territory."""
+ return False
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ yield from [
+ FlightType.CAS,
+ # TODO: FlightType.TROOP_TRANSPORT
+ # TODO: FlightType.EVAC
+ ]
+ yield from super().mission_types(for_player)
+
+ @property
+ def position(self):
+ """
+ The position where the conflict should occur
+ according to the current strength of each control point.
+ """
+ return self.point_from_a(self._position_distance)
+
+ @property
+ def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
+ """Returns a tuple of the two control points."""
+ return self.control_point_a, self.control_point_b
+
+ @property
+ def attack_distance(self):
+ """The total distance of all segments"""
+ return sum(i.attack_distance for i in self.segments)
+
+ @property
+ def attack_heading(self):
+ """The heading of the active attack segment from player to enemy control point"""
+ return self.active_segment.attack_heading
+
+ @property
+ def active_segment(self) -> FrontLineSegment:
+ """The FrontLine segment where there can be an active conflict"""
+ if self._position_distance <= self.segments[0].attack_distance:
+ return self.segments[0]
+
+ remaining_dist = self._position_distance
+ for segment in self.segments:
+ if remaining_dist <= segment.attack_distance:
+ return segment
+ else:
+ remaining_dist -= segment.attack_distance
+ logging.error(
+ "Frontline attack distance is greater than the sum of its segments"
+ )
+ return self.segments[0]
+
+ def point_from_a(self, distance: Numeric) -> Point:
+ """
+ Returns a point {distance} away from control_point_a along the frontline segments.
+ """
+ if distance < self.segments[0].attack_distance:
+ return self.control_point_a.position.point_from_heading(
+ self.segments[0].attack_heading, distance
+ )
+ remaining_dist = distance
+ for segment in self.segments:
+ if remaining_dist < segment.attack_distance:
+ return segment.point_a.point_from_heading(
+ segment.attack_heading, remaining_dist
+ )
+ else:
+ remaining_dist -= segment.attack_distance
+
+ @property
+ def _position_distance(self) -> float:
+ """
+ The distance from point "a" where the conflict should occur
+ according to the current strength of each control point
+ """
+ total_strength = (
+ self.control_point_a.base.strength + self.control_point_b.base.strength
+ )
+ if self.control_point_a.base.strength == 0:
+ return self._adjust_for_min_dist(0)
+ if self.control_point_b.base.strength == 0:
+ return self._adjust_for_min_dist(self.attack_distance)
+ strength_pct = self.control_point_a.base.strength / total_strength
+ return self._adjust_for_min_dist(strength_pct * self.attack_distance)
+
+ def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
+ """
+ Ensures the frontline conflict is never located within the minimum distance
+ constant of either end control point.
+ """
+ if (distance > self.attack_distance / 2) and (
+ distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
+ ):
+ distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
+ elif (distance < self.attack_distance / 2) and (
+ distance < FRONTLINE_MIN_CP_DISTANCE
+ ):
+ distance = FRONTLINE_MIN_CP_DISTANCE
+ return distance
+
+ def _build_segments(self) -> None:
+ """Create line segments for the frontline"""
+ control_point_ids = "|".join(
+ [str(self.control_point_a.id), str(self.control_point_b.id)]
+ ) # from_cp.id|to_cp.id
+ reversed_cp_ids = "|".join(
+ [str(self.control_point_b.id), str(self.control_point_a.id)]
+ )
+ complex_frontlines = self.theater.frontline_data
+ if (complex_frontlines) and (
+ (control_point_ids in complex_frontlines)
+ or (reversed_cp_ids in complex_frontlines)
+ ):
+ # The frontline segments must be stored in the correct order for the distance algorithms to work.
+ # The points in the frontline are ordered from the id before the | to the id after.
+ # First, check if control point id pair matches in order, and create segments if a match is found.
+ if control_point_ids in complex_frontlines:
+ point_pairs = pairwise(complex_frontlines[control_point_ids].points)
+ for i in point_pairs:
+ self.segments.append(FrontLineSegment(i[0], i[1]))
+ # Check the reverse order and build in reverse if found.
+ elif reversed_cp_ids in complex_frontlines:
+ point_pairs = pairwise(
+ reversed(complex_frontlines[reversed_cp_ids].points)
+ )
+ for i in point_pairs:
+ self.segments.append(FrontLineSegment(i[0], i[1]))
+ # If no complex frontline has been configured, fall back to the old straight line method.
+ else:
+ self.segments.append(
+ FrontLineSegment(
+ self.control_point_a.position, self.control_point_b.position
+ )
+ )
+
+
+ @staticmethod
+ def load_json_frontlines(
+ theater: ConflictTheater
+ ) -> Optional[Dict[str, ComplexFrontLine]]:
+ """Load complex frontlines from json"""
+ try:
+ path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
+ with open(path, "r") as file:
+ logging.debug(f"Loading frontline from {path}...")
+ data = json.load(file)
+ return {
+ frontline: ComplexFrontLine(
+ data[frontline]["start_cp"],
+ [Point(i[0], i[1]) for i in data[frontline]["points"]],
+ )
+ for frontline in data
+ }
+ except OSError:
+ logging.warning(
+ f"Unable to load preset frontlines for {theater.terrain.name}"
+ )
+ return None
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
new file mode 100644
index 00000000..82f1b385
--- /dev/null
+++ b/game/theater/controlpoint.py
@@ -0,0 +1,721 @@
+from __future__ import annotations
+
+import itertools
+import logging
+import random
+import re
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
+
+from dcs.mapping import Point
+from dcs.ships import (
+ CVN_74_John_C__Stennis,
+ CV_1143_5_Admiral_Kuznetsov,
+ LHA_1_Tarawa,
+ Type_071_Amphibious_Transport_Dock,
+)
+from dcs.terrain.terrain import Airport, ParkingSlot
+from dcs.unittype import FlyingType
+
+from game import db
+from gen.runways import RunwayAssigner, RunwayData
+from gen.ground_forces.combat_stance import CombatStance
+from .base import Base
+from .missiontarget import MissionTarget
+from .theatergroundobject import (
+ BaseDefenseGroundObject,
+ EwrGroundObject,
+ SamGroundObject,
+ TheaterGroundObject,
+ VehicleGroupGroundObject, GenericCarrierGroundObject,
+)
+from ..weather import Conditions
+
+if TYPE_CHECKING:
+ from game import Game
+ from gen.flights.flight import FlightType
+ from ..event import UnitsDeliveryEvent
+
+
+class ControlPointType(Enum):
+ #: An airbase with slots for everything.
+ AIRBASE = 0
+ #: A group with a Stennis type carrier (F/A-18, F-14 compatible).
+ AIRCRAFT_CARRIER_GROUP = 1
+ #: A group with a Tarawa carrier (Helicopters & Harrier).
+ LHA_GROUP = 2
+ #: A FARP, with slots for helicopters
+ FARP = 4
+ #: A FOB (ground units only)
+ FOB = 5
+ OFF_MAP = 6
+
+
+class LocationType(Enum):
+ BaseAirDefense = "base air defense"
+ Coastal = "coastal defense"
+ Ewr = "EWR"
+ Garrison = "garrison"
+ MissileSite = "missile site"
+ OffshoreStrikeTarget = "offshore strike target"
+ Sam = "SAM"
+ Ship = "ship"
+ Shorad = "SHORAD"
+ StrikeTarget = "strike target"
+
+
+@dataclass
+class PresetLocations:
+ """Defines the preset locations loaded from the campaign mission file."""
+
+ #: Locations used for spawning ground defenses for bases.
+ base_garrisons: List[Point] = field(default_factory=list)
+
+ #: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
+ #: and SHORADs.
+ base_air_defense: List[Point] = field(default_factory=list)
+
+ #: Locations used by EWRs.
+ ewrs: List[Point] = field(default_factory=list)
+
+ #: Locations used by non-carrier ships. Carriers and LHAs are not random.
+ ships: List[Point] = field(default_factory=list)
+
+ #: Locations used by coastal defenses.
+ coastal_defenses: List[Point] = field(default_factory=list)
+
+ #: Locations used by ground based strike objectives.
+ strike_locations: List[Point] = field(default_factory=list)
+
+ #: Locations used by offshore strike objectives.
+ offshore_strike_locations: List[Point] = field(default_factory=list)
+
+ #: Locations used by missile sites like scuds and V-2s.
+ missile_sites: List[Point] = field(default_factory=list)
+
+ #: Locations of long range SAMs which should always be spawned.
+ required_long_range_sams: List[Point] = field(default_factory=list)
+
+ #: Locations of medium range SAMs which should always be spawned.
+ required_medium_range_sams: List[Point] = field(default_factory=list)
+
+ @staticmethod
+ def _random_from(points: List[Point]) -> Optional[Point]:
+ """Finds, removes, and returns a random position from the given list."""
+ if not points:
+ return None
+ point = random.choice(points)
+ points.remove(point)
+ return point
+
+ def random_for(self, location_type: LocationType) -> Optional[Point]:
+ """Returns a position suitable for the given location type.
+
+ The location, if found, will be claimed by the caller and not available
+ to subsequent calls.
+ """
+ if location_type == LocationType.BaseAirDefense:
+ return self._random_from(self.base_air_defense)
+ if location_type == LocationType.Coastal:
+ return self._random_from(self.coastal_defenses)
+ if location_type == LocationType.Ewr:
+ return self._random_from(self.ewrs)
+ if location_type == LocationType.Garrison:
+ return self._random_from(self.base_garrisons)
+ if location_type == LocationType.MissileSite:
+ return self._random_from(self.missile_sites)
+ if location_type == LocationType.OffshoreStrikeTarget:
+ return self._random_from(self.offshore_strike_locations)
+ if location_type == LocationType.Sam:
+ return self._random_from(self.strike_locations)
+ if location_type == LocationType.Ship:
+ return self._random_from(self.ships)
+ if location_type == LocationType.Shorad:
+ return self._random_from(self.base_garrisons)
+ if location_type == LocationType.StrikeTarget:
+ return self._random_from(self.strike_locations)
+ logging.error(f"Unknown location type: {location_type}")
+ return None
+
+
+@dataclass(frozen=True)
+class PendingOccupancy:
+ present: int
+ ordered: int
+ transferring: int
+
+ @property
+ def total(self) -> int:
+ return self.present + self.ordered + self.transferring
+
+
+@dataclass
+class RunwayStatus:
+ damaged: bool = False
+ repair_turns_remaining: Optional[int] = None
+
+ def damage(self) -> None:
+ self.damaged = True
+ # If the runway is already under repair and is damaged again, progress
+ # is reset.
+ self.repair_turns_remaining = None
+
+ def begin_repair(self) -> None:
+ if self.repair_turns_remaining is not None:
+ logging.error("Runway already under repair. Restarting.")
+ self.repair_turns_remaining = 4
+
+ def process_turn(self) -> None:
+ if self.repair_turns_remaining is not None:
+ if self.repair_turns_remaining == 1:
+ self.repair_turns_remaining = None
+ self.damaged = False
+ else:
+ self.repair_turns_remaining -= 1
+
+ @property
+ def needs_repair(self) -> bool:
+ return self.damaged and self.repair_turns_remaining is None
+
+ def __str__(self) -> str:
+ if not self.damaged:
+ return "Runway operational"
+
+ turns_remaining = self.repair_turns_remaining
+ if turns_remaining is None:
+ return "Runway damaged"
+
+ return f"Runway repairing, {turns_remaining} turns remaining"
+
+
+class ControlPoint(MissionTarget, ABC):
+
+ position = None # type: Point
+ name = None # type: str
+
+ captured = False
+ has_frontline = True
+
+ alt = 0
+
+ # TODO: Only airbases have IDs.
+ # TODO: has_frontline is only reasonable for airbases.
+ # TODO: cptype is obsolete.
+ def __init__(self, cp_id: int, name: str, position: Point,
+ at: db.StartingPosition, size: int,
+ importance: float, has_frontline=True,
+ cptype=ControlPointType.AIRBASE):
+ super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
+ # TODO: Should be Airbase specific.
+ self.id = cp_id
+ self.full_name = name
+ self.at = at
+ self.connected_objectives: List[TheaterGroundObject] = []
+ self.base_defenses: List[BaseDefenseGroundObject] = []
+ self.preset_locations = PresetLocations()
+
+ # TODO: Should be Airbase specific.
+ self.size = size
+ self.importance = importance
+ self.captured = False
+ self.captured_invert = False
+ # TODO: Should be Airbase specific.
+ self.has_frontline = has_frontline
+ self.connected_points: List[ControlPoint] = []
+ self.base: Base = Base()
+ self.cptype = cptype
+ # TODO: Should be Airbase specific.
+ self.stances: Dict[int, CombatStance] = {}
+ self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
+
+ self.target_position: Optional[Point] = None
+
+ def __repr__(self):
+ return f"<{__class__}: {self.name}>"
+
+ @property
+ def ground_objects(self) -> List[TheaterGroundObject]:
+ return list(
+ itertools.chain(self.connected_objectives, self.base_defenses))
+
+ @property
+ @abstractmethod
+ def heading(self) -> int:
+ ...
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def is_global(self):
+ return not self.connected_points
+
+ @property
+ def is_carrier(self):
+ """
+ :return: Whether this control point is an aircraft carrier
+ """
+ return False
+
+ @property
+ def is_fleet(self):
+ """
+ :return: Whether this control point is a boat (mobile)
+ """
+ return False
+
+ @property
+ def is_lha(self):
+ """
+ :return: Whether this control point is an LHA
+ """
+ return False
+
+ @property
+ def moveable(self) -> bool:
+ """
+ :return: Whether this control point can be moved around
+ """
+ return False
+
+ @property
+ @abstractmethod
+ def can_deploy_ground_units(self) -> bool:
+ ...
+
+ @property
+ @abstractmethod
+ def total_aircraft_parking(self):
+ """
+ :return: The maximum number of aircraft that can be stored in this
+ control point
+ """
+ ...
+
+ # TODO: Should be Airbase specific.
+ def connect(self, to: ControlPoint) -> None:
+ self.connected_points.append(to)
+ self.stances[to.id] = CombatStance.DEFENSIVE
+
+ @abstractmethod
+ def runway_is_operational(self) -> bool:
+ """
+ Check whether this control point supports taking offs and landings.
+ :return:
+ """
+ ...
+
+ # TODO: Should be naval specific.
+ def get_carrier_group_name(self):
+ """
+ Get the carrier group name if the airbase is a carrier
+ :return: Carrier group name
+ """
+ if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP,
+ ControlPointType.LHA_GROUP]:
+ for g in self.ground_objects:
+ if g.dcs_identifier == "CARRIER":
+ for group in g.groups:
+ for u in group.units:
+ if db.unit_type_from_name(u.type) in [
+ CVN_74_John_C__Stennis,
+ CV_1143_5_Admiral_Kuznetsov]:
+ return group.name
+ elif g.dcs_identifier == "LHA":
+ for group in g.groups:
+ for u in group.units:
+ if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]:
+ return group.name
+ return None
+
+ # TODO: Should be Airbase specific.
+ def is_connected(self, to) -> bool:
+ return to in self.connected_points
+
+ def find_ground_objects_by_obj_name(self, obj_name):
+ found = []
+ for g in self.ground_objects:
+ if g.obj_name == obj_name:
+ found.append(g)
+ return found
+
+ def is_friendly(self, to_player: bool) -> bool:
+ return self.captured == to_player
+
+ # TODO: Should be Airbase specific.
+ def clear_base_defenses(self) -> None:
+ for base_defense in self.base_defenses:
+ if isinstance(base_defense, EwrGroundObject):
+ self.preset_locations.ewrs.append(base_defense.position)
+ elif isinstance(base_defense, SamGroundObject):
+ self.preset_locations.base_air_defense.append(
+ base_defense.position)
+ elif isinstance(base_defense, VehicleGroupGroundObject):
+ self.preset_locations.base_garrisons.append(
+ base_defense.position)
+ else:
+ logging.error(
+ "Could not determine preset location type for "
+ f"{base_defense}. Assuming garrison type.")
+ self.preset_locations.base_garrisons.append(
+ base_defense.position)
+ self.base_defenses = []
+
+ # TODO: Should be Airbase specific.
+ def capture(self, game: Game, for_player: bool) -> None:
+ if for_player:
+ self.captured = True
+ else:
+ self.captured = False
+
+ self.base.set_strength_to_minimum()
+
+ self.base.aircraft = {}
+ self.base.armor = {}
+
+ self.clear_base_defenses()
+ from .start_generator import BaseDefenseGenerator
+ BaseDefenseGenerator(game, self).generate()
+
+ @abstractmethod
+ def can_operate(self, aircraft: Type[FlyingType]) -> bool:
+ ...
+
+ def aircraft_transferring(self, game: Game) -> int:
+ if self.captured:
+ ato = game.blue_ato
+ else:
+ ato = game.red_ato
+
+ total = 0
+ for package in ato.packages:
+ for flight in package.flights:
+ if flight.departure == flight.arrival:
+ continue
+ if flight.departure == self:
+ total -= flight.count
+ elif flight.arrival == self:
+ total += flight.count
+ return total
+
+ def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
+ assert self.pending_unit_deliveries
+ on_order = 0
+ for unit_bought in self.pending_unit_deliveries.units:
+ if issubclass(unit_bought, FlyingType):
+ on_order += self.pending_unit_deliveries.units[unit_bought]
+
+ return PendingOccupancy(self.base.total_aircraft, on_order,
+ self.aircraft_transferring(game))
+
+ def unclaimed_parking(self, game: Game) -> int:
+ return (self.total_aircraft_parking -
+ self.expected_aircraft_next_turn(game).total)
+
+ @abstractmethod
+ def active_runway(self, conditions: Conditions,
+ dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
+ ...
+
+ @property
+ def parking_slots(self) -> Iterator[ParkingSlot]:
+ yield from []
+
+ @property
+ @abstractmethod
+ def runway_status(self) -> RunwayStatus:
+ ...
+
+ @property
+ def runway_can_be_repaired(self) -> bool:
+ return self.runway_status.needs_repair
+
+ def begin_runway_repair(self) -> None:
+ if not self.runway_can_be_repaired:
+ logging.error(f"Cannot repair runway at {self}")
+ return
+ self.runway_status.begin_repair()
+
+ def process_turn(self) -> None:
+ runway_status = self.runway_status
+ if runway_status is not None:
+ runway_status.process_turn()
+
+ # Process movements for ships control points group
+ if self.target_position is not None:
+ delta = self.target_position - self.position
+ self.position = self.target_position
+ self.target_position = None
+
+ # Move the linked unit groups
+ for ground_object in self.ground_objects:
+ if isinstance(ground_object, GenericCarrierGroundObject):
+ for group in ground_object.groups:
+ for u in group.units:
+ u.position.x = u.position.x + delta.x
+ u.position.y = u.position.y + delta.y
+
+
+class Airfield(ControlPoint):
+
+ def __init__(self, airport: Airport, size: int,
+ importance: float, has_frontline=True):
+ super().__init__(airport.id, airport.name, airport.position, airport,
+ size, importance, has_frontline,
+ cptype=ControlPointType.AIRBASE)
+ self.airport = airport
+ self._runway_status = RunwayStatus()
+
+ def can_operate(self, aircraft: FlyingType) -> bool:
+ # TODO: Allow helicopters.
+ # Need to implement ground spawns so the helos don't use the runway.
+ # TODO: Allow harrier.
+ # Needs ground spawns just like helos do, but also need to be able to
+ # limit takeoff weight to ~20500 lbs or it won't be able to take off.
+ return self.runway_is_operational()
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ from gen.flights.flight import FlightType
+ if self.is_friendly(for_player):
+ yield from [
+ # TODO: FlightType.INTERCEPTION
+ # TODO: FlightType.LOGISTICS
+ ]
+ else:
+ yield from [
+ FlightType.OCA_AIRCRAFT,
+ FlightType.OCA_RUNWAY,
+ ]
+ yield from super().mission_types(for_player)
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return len(self.airport.parking_slots)
+
+ @property
+ def heading(self) -> int:
+ return self.airport.runways[0].heading
+
+ def runway_is_operational(self) -> bool:
+ return not self.runway_status.damaged
+
+ @property
+ def runway_status(self) -> RunwayStatus:
+ return self._runway_status
+
+ def damage_runway(self) -> None:
+ self.runway_status.damage()
+
+ def active_runway(self, conditions: Conditions,
+ dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
+ assigner = RunwayAssigner(conditions)
+ return assigner.get_preferred_runway(self.airport)
+
+ @property
+ def parking_slots(self) -> Iterator[ParkingSlot]:
+ yield from self.airport.parking_slots
+
+ @property
+ def can_deploy_ground_units(self) -> bool:
+ return True
+
+
+class NavalControlPoint(ControlPoint, ABC):
+
+ @property
+ def is_fleet(self) -> bool:
+ return True
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ yield from super().mission_types(for_player)
+ if self.is_friendly(for_player):
+ yield from [
+ # TODO: FlightType.INTERCEPTION
+ # TODO: Buddy tanking for the A-4?
+ # TODO: Rescue chopper?
+ # TODO: Inter-ship logistics?
+ ]
+ else:
+ yield FlightType.ANTISHIP
+
+ @property
+ def heading(self) -> int:
+ return 0 # TODO compute heading
+
+ def runway_is_operational(self) -> bool:
+ # Necessary because it's possible for the carrier itself to have sunk
+ # while its escorts are still alive.
+ for g in self.ground_objects:
+ if g.dcs_identifier in ["CARRIER", "LHA"]:
+ for group in g.groups:
+ for u in group.units:
+ if db.unit_type_from_name(u.type) in [
+ CVN_74_John_C__Stennis, LHA_1_Tarawa,
+ CV_1143_5_Admiral_Kuznetsov,
+ Type_071_Amphibious_Transport_Dock]:
+ return True
+ return False
+
+ def active_runway(self, conditions: Conditions,
+ dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
+ # TODO: Assign TACAN and ICLS earlier so we don't need this.
+ fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
+ return dynamic_runways.get(self.name, fallback)
+
+ @property
+ def runway_status(self) -> RunwayStatus:
+ return RunwayStatus(damaged=not self.runway_is_operational())
+
+ @property
+ def runway_can_be_repaired(self) -> bool:
+ return False
+
+ @property
+ def moveable(self) -> bool:
+ return True
+
+ @property
+ def can_deploy_ground_units(self) -> bool:
+ return False
+
+
+class Carrier(NavalControlPoint):
+
+ def __init__(self, name: str, at: Point, cp_id: int):
+ import game.theater.conflicttheater
+ super().__init__(cp_id, name, at, at,
+ game.theater.conflicttheater.SIZE_SMALL, 1,
+ has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
+
+ def capture(self, game: Game, for_player: bool) -> None:
+ raise RuntimeError("Carriers cannot be captured")
+
+ @property
+ def is_carrier(self):
+ return True
+
+ def can_operate(self, aircraft: FlyingType) -> bool:
+ return aircraft in db.CARRIER_CAPABLE
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return 90
+
+
+class Lha(NavalControlPoint):
+
+ def __init__(self, name: str, at: Point, cp_id: int):
+ import game.theater.conflicttheater
+ super().__init__(cp_id, name, at, at,
+ game.theater.conflicttheater.SIZE_SMALL, 1,
+ has_frontline=False, cptype=ControlPointType.LHA_GROUP)
+
+ def capture(self, game: Game, for_player: bool) -> None:
+ raise RuntimeError("LHAs cannot be captured")
+
+ @property
+ def is_lha(self) -> bool:
+ return True
+
+ def can_operate(self, aircraft: FlyingType) -> bool:
+ return aircraft in db.LHA_CAPABLE
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return 20
+
+
+class OffMapSpawn(ControlPoint):
+
+ def runway_is_operational(self) -> bool:
+ return True
+
+ def __init__(self, cp_id: int, name: str, position: Point):
+ from . import IMPORTANCE_MEDIUM, SIZE_REGULAR
+ super().__init__(cp_id, name, position, at=position,
+ size=SIZE_REGULAR, importance=IMPORTANCE_MEDIUM,
+ has_frontline=False, cptype=ControlPointType.OFF_MAP)
+
+ def capture(self, game: Game, for_player: bool) -> None:
+ raise RuntimeError("Off map control points cannot be captured")
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ yield from []
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return 1000
+
+ def can_operate(self, aircraft: FlyingType) -> bool:
+ return True
+
+ @property
+ def heading(self) -> int:
+ return 0
+
+ def active_runway(self, conditions: Conditions,
+ dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
+ logging.warning("TODO: Off map spawns have no runways.")
+ return RunwayData(self.full_name, runway_heading=0, runway_name="")
+
+ @property
+ def runway_status(self) -> RunwayStatus:
+ return RunwayStatus()
+
+ @property
+ def can_deploy_ground_units(self) -> bool:
+ return False
+
+
+class Fob(ControlPoint):
+
+ def __init__(self, name: str, at: Point, cp_id: int):
+ import game.theater.conflicttheater
+ super().__init__(cp_id, name, at, at,
+ game.theater.conflicttheater.SIZE_SMALL, 1,
+ has_frontline=True, cptype=ControlPointType.FOB)
+ self.name = name
+
+ def runway_is_operational(self) -> bool:
+ return False
+
+ def active_runway(self, conditions: Conditions,
+ dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
+ logging.warning("TODO: FOBs have no runways.")
+ return RunwayData(self.full_name, runway_heading=0, runway_name="")
+
+ @property
+ def runway_status(self) -> RunwayStatus:
+ return RunwayStatus()
+
+ def mission_types(self, for_player: bool) -> Iterator[FlightType]:
+ from gen.flights.flight import FlightType
+ if self.is_friendly(for_player):
+ yield from [
+ FlightType.BARCAP,
+ # TODO: FlightType.LOGISTICS
+ ]
+ else:
+ yield from [
+ FlightType.STRIKE,
+ FlightType.SWEEP,
+ FlightType.ESCORT,
+ FlightType.SEAD,
+ ]
+
+ @property
+ def total_aircraft_parking(self) -> int:
+ return 0
+
+ def can_operate(self, aircraft: FlyingType) -> bool:
+ return False
+
+ @property
+ def heading(self) -> int:
+ return 0
+
+ @property
+ def can_deploy_ground_units(self) -> bool:
+ return True
diff --git a/game/theater/landmap.py b/game/theater/landmap.py
new file mode 100644
index 00000000..6e510087
--- /dev/null
+++ b/game/theater/landmap.py
@@ -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)
+
diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py
new file mode 100644
index 00000000..c442fe42
--- /dev/null
+++ b/game/theater/missiontarget.py
@@ -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,
+ ]
diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py
new file mode 100644
index 00000000..f9ea1d47
--- /dev/null
+++ b/game/theater/start_generator.py
@@ -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()
diff --git a/theater/theatergroundobject.py b/game/theater/theatergroundobject.py
similarity index 75%
rename from theater/theatergroundobject.py
rename to game/theater/theatergroundobject.py
index a0694974..2908d5f3 100644
--- a/theater/theatergroundobject.py
+++ b/game/theater/theatergroundobject.py
@@ -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__(
diff --git a/game/unitmap.py b/game/unitmap.py
new file mode 100644
index 00000000..79cad92f
--- /dev/null
+++ b/game/unitmap.py
@@ -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)
diff --git a/game/utils.py b/game/utils.py
index 44652472..b570e355 100644
--- a/game/utils.py
+++ b/game/utils.py
@@ -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)
\ No newline at end of file
diff --git a/game/version.py b/game/version.py
index f3b1d1f4..5955ac0a 100644
--- a/game/version.py
+++ b/game/version.py
@@ -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:
diff --git a/game/weather.py b/game/weather.py
index d6775614..34b19e2d 100644
--- a/game/weather.py
+++ b/game/weather.py
@@ -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):
diff --git a/gen/aircraft.py b/gen/aircraft.py
index c0c5c370..a50c9b69 100644
--- a/gen/aircraft.py
+++ b/gen/aircraft.py
@@ -5,7 +5,7 @@ import random
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
-from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup
@@ -13,17 +13,22 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country
from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map
+from dcs.mapping import Point
from dcs.mission import Mission, StartType
from dcs.planes import (
AJS37,
B_17G,
+ B_52H,
Bf_109K_4,
+ C_101EB,
+ C_101CC,
FW_190A8,
FW_190D9,
F_14B,
I_16,
JF_17,
Ju_88A4,
+ PlaneType,
P_47D_30,
P_47D_30bl1,
P_47D_40,
@@ -31,34 +36,37 @@ from dcs.planes import (
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
- Su_33, A_20G, Tu_22M3, B_52H,
+ Su_33,
+ Tu_22M3,
)
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AntishipStrike,
AttackGroup,
Bombing,
+ BombingRunway,
CAP,
CAS,
ControlledTask,
EPLRS,
EngageTargets,
EngageTargetsInZone,
+ FighterSweep,
GroundAttack,
OptROE,
OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo,
OptReactOnThreat,
- OptRestrictAfterburner,
OptRestrictJettison,
OrbitAction,
- PinpointStrike,
+ RunwayAttack,
SEAD,
StartCommand,
Targets,
- Task, WeaponType,
+ Task,
+ WeaponType,
)
-from dcs.terrain.terrain import Airport
+from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
@@ -66,11 +74,22 @@ from dcs.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
+from game.factions.faction import Faction
from game.settings import Settings
-from game.utils import nm_to_meter
+from game.theater.controlpoint import (
+ Airfield,
+ ControlPoint,
+ ControlPointType,
+ NavalControlPoint,
+ OffMapSpawn,
+)
+from game.theater.theatergroundobject import TheaterGroundObject
+from game.unitmap import UnitMap
+from game.utils import knots_to_kph, nm_to_meter
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
+from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import (
Flight,
FlightType,
@@ -79,17 +98,14 @@ from gen.flights.flight import (
)
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData
-from gen.conflictgen import FRONTLINE_LENGTH
-from dcs.mapping import Point
-from theater import TheaterGroundObject
-from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
- FormationFlightPlan,
+ LoiterFlightPlan,
PatrollingFlightPlan,
+ SweepFlightPlan,
)
-from .flights.traveltime import TotEstimator
+from .flights.traveltime import GroundSpeed, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
@@ -281,12 +297,19 @@ class FlightData:
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
+ #: Bingo fuel value in lbs.
+ bingo_fuel: Optional[int]
+
+ joker_fuel: Optional[int]
+
def __init__(self, package: Package, flight_type: FlightType,
units: List[FlyingUnit], size: int, friendly: bool,
departure_delay: timedelta, departure: RunwayData,
arrival: RunwayData, divert: Optional[RunwayData],
waypoints: List[FlightWaypoint],
- intra_flight_channel: RadioFrequency) -> None:
+ intra_flight_channel: RadioFrequency,
+ bingo_fuel: Optional[int],
+ joker_fuel: Optional[int]) -> None:
self.package = package
self.flight_type = flight_type
self.units = units
@@ -299,6 +322,8 @@ class FlightData:
self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {}
+ self.bingo_fuel = bingo_fuel
+ self.joker_fuel = joker_fuel
self.callsign = create_group_callsign_from_unit(self.units[0])
@property
@@ -640,13 +665,13 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
- def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
- game: Game, radio_registry: RadioRegistry):
+ def __init__(self, mission: Mission, settings: Settings, game: Game,
+ radio_registry: RadioRegistry, unit_map: UnitMap) -> None:
self.m = mission
self.game = game
self.settings = settings
- self.conflict = conflict
self.radio_registry = radio_registry
+ self.unit_map = unit_map
self.flights: List[FlightData] = []
@cached_property
@@ -739,25 +764,15 @@ class AircraftConflictGenerator:
if unit_type is F_14B:
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
-
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz)
- # TODO: Support for different departure/arrival airfields.
- cp = flight.from_cp
- fallback_runway = RunwayData(cp.full_name, runway_heading=0,
- runway_name="")
- if cp.cptype == ControlPointType.AIRBASE:
- assigner = RunwayAssigner(self.game.conditions)
- departure_runway = assigner.get_preferred_runway(
- flight.from_cp.airport)
- elif cp.is_fleet:
- departure_runway = dynamic_runways.get(cp.name, fallback_runway)
- else:
- logging.warning(f"Unhandled departure control point: {cp.cptype}")
- departure_runway = fallback_runway
+ divert = None
+ if flight.divert is not None:
+ divert = flight.divert.active_runway(self.game.conditions,
+ dynamic_runways)
self.flights.append(FlightData(
package=package,
@@ -767,26 +782,25 @@ class AircraftConflictGenerator:
friendly=flight.from_cp.captured,
# Set later.
departure_delay=timedelta(),
- departure=departure_runway,
- arrival=departure_runway,
- # TODO: Support for divert airfields.
- divert=None,
+ departure=flight.departure.active_runway(self.game.conditions,
+ dynamic_runways),
+ arrival=flight.arrival.active_runway(self.game.conditions,
+ dynamic_runways),
+ divert=divert,
# Waypoints are added later, after they've had their TOTs set.
waypoints=[],
- intra_flight_channel=channel
+ intra_flight_channel=channel,
+ bingo_fuel=flight.flight_plan.bingo_fuel,
+ joker_fuel=flight.flight_plan.joker_fuel
))
- # Special case so Su 33 carrier take off
- if unit_type is Su_33:
- if flight.flight_type is not CAP:
- for unit in group.units:
- unit.fuel = Su_33.fuel_max / 2.2
- else:
- for unit in group.units:
- unit.fuel = Su_33.fuel_max * 0.8
+ # Special case so Su 33 and C101 can take off
+ if unit_type in [Su_33, C_101EB, C_101CC]:
+ self.set_reduced_fuel(flight, group, unit_type)
def _generate_at_airport(self, name: str, side: Country,
- unit_type: FlyingType, count: int, start_type: str,
+ unit_type: Type[FlyingType], count: int,
+ start_type: str,
airport: Optional[Airport] = None) -> FlyingGroup:
assert count > 0
@@ -801,35 +815,42 @@ class AircraftConflictGenerator:
group_size=count,
parking_slots=None)
- def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, at: Point) -> FlyingGroup:
- assert count > 0
+ def _generate_inflight(self, name: str, side: Country, flight: Flight,
+ origin: ControlPoint) -> FlyingGroup:
+ assert flight.count > 0
+ at = origin.position
- if unit_type in helicopters.helicopter_map.values():
+ alt_type = "RADIO"
+ if isinstance(origin, OffMapSpawn):
+ alt = flight.flight_plan.waypoints[0].alt
+ alt_type = flight.flight_plan.waypoints[0].alt_type
+ elif flight.unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
- speed = WARM_START_HELI_AIRSPEED
else:
alt = WARM_START_ALTITUDE
- speed = WARM_START_AIRSPEED
+
+ speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
- logging.info("airgen: {} for {} at {} at {}".format(unit_type, side.id, alt, speed))
+ logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
group = self.m.flight_group(
country=side,
name=name,
- aircraft_type=unit_type,
+ aircraft_type=flight.unit_type,
airport=None,
position=pos,
altitude=alt,
speed=speed,
maintask=None,
- group_size=count)
+ group_size=flight.count)
- group.points[0].alt_type = "RADIO"
+ group.points[0].alt_type = alt_type
return group
def _generate_at_group(self, name: str, side: Country,
- unit_type: FlyingType, count: int, start_type: str,
+ unit_type: Type[FlyingType], count: int,
+ start_type: str,
at: Union[ShipGroup, StaticGroup]) -> FlyingGroup:
assert count > 0
@@ -875,7 +896,6 @@ class AircraftConflictGenerator:
else:
assert False
-
def _setup_custom_payload(self, flight, group:FlyingGroup):
if flight.use_custom_loadout:
@@ -895,13 +915,11 @@ class AircraftConflictGenerator:
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
- if cp.airport is not None:
- for parking_slot in cp.airport.parking_slots:
- parking_slot.unit_id = None
+ for parking_slot in cp.parking_slots:
+ parking_slot.unit_id = None
def generate_flights(self, country, ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData]) -> None:
- self.clear_parking_slots()
for package in ato.packages:
if not package.flights:
@@ -914,9 +932,59 @@ class AircraftConflictGenerator:
logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country,
flight)
+ self.unit_map.add_aircraft(group, flight)
self.setup_flight_group(group, package, flight, dynamic_runways)
self.create_waypoints(group, package, flight)
+ def spawn_unused_aircraft(self, player_country: Country,
+ enemy_country: Country) -> None:
+ inventories = self.game.aircraft_inventory.inventories
+ for control_point, inventory in inventories.items():
+ if not isinstance(control_point, Airfield):
+ continue
+
+ if control_point.captured:
+ country = player_country
+ faction = self.game.player_faction
+ else:
+ country = enemy_country
+ faction = self.game.enemy_faction
+
+ for aircraft, available in inventory.all_aircraft:
+ try:
+ self._spawn_unused_at(control_point, country, faction, aircraft,
+ available)
+ except NoParkingSlotError:
+ # If we run out of parking, stop spawning aircraft.
+ return
+
+ def _spawn_unused_at(self, control_point: Airfield, country: Country, faction: Faction,
+ aircraft: Type[FlyingType], number: int) -> None:
+ for _ in range(number):
+ # Creating a flight even those this isn't a fragged mission lets us
+ # reuse the existing debriefing code.
+ # TODO: Special flight type?
+ flight = Flight(Package(control_point), aircraft, 1,
+ FlightType.BARCAP, "Cold", departure=control_point,
+ arrival=control_point, divert=None)
+
+ group = self._generate_at_airport(
+ name=namegen.next_unit_name(country, control_point.id,
+ aircraft),
+ side=country,
+ unit_type=aircraft,
+ count=1,
+ start_type="Cold",
+ airport=control_point.airport)
+
+ if aircraft in faction.liveries_overrides:
+ livery = random.choice(faction.liveries_overrides[aircraft])
+ for unit in group.units:
+ unit.livery_id = livery
+
+ group.uncontrolled = True
+ self.unit_map.add_aircraft(group, flight)
+
def set_activation_time(self, flight: Flight, group: FlyingGroup,
delay: timedelta) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
@@ -971,10 +1039,9 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- at=cp.position)
- elif cp.is_fleet:
+ flight=flight,
+ origin=cp)
+ elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
group = self._generate_at_group(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
@@ -984,8 +1051,12 @@ class AircraftConflictGenerator:
start_type=flight.start_type,
at=self.m.find_group(group_name))
else:
+ if not isinstance(cp, Airfield):
+ raise RuntimeError(
+ f"Attempted to spawn at airfield for non-airfield {cp}")
group = self._generate_at_airport(
- name=namegen.next_unit_name(country, cp.id, flight.unit_type),
+ name=namegen.next_unit_name(country, cp.id,
+ flight.unit_type),
side=country,
unit_type=flight.unit_type,
count=flight.count,
@@ -999,13 +1070,26 @@ class AircraftConflictGenerator:
group = self._generate_inflight(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
side=country,
- unit_type=flight.unit_type,
- count=flight.count,
- at=cp.position)
+ flight=flight,
+ origin=cp)
group.points[0].alt = 1500
return group
+ @staticmethod
+ def set_reduced_fuel(flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]) -> None:
+ if unit_type is Su_33:
+ for unit in group.units:
+ if flight.flight_type is not CAP:
+ unit.fuel = Su_33.fuel_max / 2.2
+ else:
+ unit.fuel = Su_33.fuel_max * 0.8
+ elif unit_type in [C_101EB, C_101CC]:
+ for unit in group.units:
+ unit.fuel = unit_type.fuel_max * 0.5
+ else:
+ raise RuntimeError(f"No reduced fuel case for type {unit_type}")
+
@staticmethod
def configure_behavior(
group: FlyingGroup,
@@ -1046,8 +1130,18 @@ class AircraftConflictGenerator:
self.configure_behavior(group, rtb_winchester=ammo_type)
- group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
- targets=[Targets.All.Air]))
+ def configure_sweep(self, group: FlyingGroup, package: Package,
+ flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = FighterSweep.name
+ self._setup_group(group, FighterSweep, package, flight, dynamic_runways)
+
+ if flight.unit_type not in GUNFIGHTERS:
+ ammo_type = OptRTBOnOutOfAmmo.Values.AAM
+ else:
+ ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
+
+ self.configure_behavior(group, rtb_winchester=ammo_type)
def configure_cas(self, group: FlyingGroup, package: Package,
flight: Flight,
@@ -1108,6 +1202,28 @@ class AircraftConflictGenerator:
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
+ def configure_runway_attack(
+ self, group: FlyingGroup, package: Package, flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = RunwayAttack.name
+ self._setup_group(group, RunwayAttack, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ restrict_jettison=True)
+
+ def configure_oca_strike(
+ self, group: FlyingGroup, package: Package, flight: Flight,
+ dynamic_runways: Dict[str, RunwayData]) -> None:
+ group.task = CAS.name
+ self._setup_group(group, CAS, package, flight, dynamic_runways)
+ self.configure_behavior(
+ group,
+ react_on_threat=OptReactOnThreat.Values.EvadeFire,
+ roe=OptROE.Values.OpenFire,
+ restrict_jettison=True)
+
def configure_escort(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
@@ -1121,7 +1237,7 @@ class AircraftConflictGenerator:
def configure_unknown_task(self, group: FlyingGroup,
flight: Flight) -> None:
- logging.error(f"Unhandled flight type: {flight.flight_type.name}")
+ logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(group)
def setup_flight_group(self, group: FlyingGroup, package: Package,
@@ -1131,18 +1247,25 @@ class AircraftConflictGenerator:
if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]:
self.configure_cap(group, package, flight, dynamic_runways)
+ elif flight_type == FlightType.SWEEP:
+ self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways)
- elif flight_type in [FlightType.DEAD, ]:
+ elif flight_type == FlightType.DEAD:
self.configure_dead(group, package, flight, dynamic_runways)
- elif flight_type in [FlightType.SEAD, ]:
+ elif flight_type == FlightType.SEAD:
self.configure_sead(group, package, flight, dynamic_runways)
- elif flight_type in [FlightType.STRIKE]:
+ elif flight_type == FlightType.STRIKE:
self.configure_strike(group, package, flight, dynamic_runways)
- elif flight_type in [FlightType.ANTISHIP]:
+ elif flight_type == FlightType.ANTISHIP:
self.configure_anti_ship(group, package, flight, dynamic_runways)
elif flight_type == FlightType.ESCORT:
self.configure_escort(group, package, flight, dynamic_runways)
+ elif flight_type == FlightType.OCA_RUNWAY:
+ self.configure_runway_attack(group, package, flight,
+ dynamic_runways)
+ elif flight_type == FlightType.OCA_AIRCRAFT:
+ self.configure_oca_strike(group, package, flight, dynamic_runways)
else:
self.configure_unknown_task(group, flight)
@@ -1164,10 +1287,10 @@ class AircraftConflictGenerator:
if point.only_for_player and not flight.client_count:
continue
filtered_points.append(point)
- # Only add 1 target waypoint for Viggens. This only affects player flights,
+ # Only add 1 target waypoint for Viggens. This only affects player flights,
# the Viggen can't have more than 9 waypoints which leaves us with two target point
# under the current flight plans.
- # TODO: Make this smarter, it currently selects a random unit in the group for target,
+ # TODO: Make this smarter, it currently selects a random unit in the group for target,
# this could be updated to make it pick the "best" two targets in the group.
if flight.unit_type is AJS37 and flight.client_count:
viggen_target_points = [
@@ -1180,7 +1303,7 @@ class AircraftConflictGenerator:
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
)
]
-
+
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint(
point, group, package, flight, self.m
@@ -1258,10 +1381,13 @@ class PydcsWaypointBuilder:
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(
- Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt)
+ Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt,
+ name=self.mission.string(self.waypoint.name))
+
+ if self.waypoint.flyover:
+ waypoint.type = PointAction.FlyOverPoint.value
waypoint.alt_type = self.waypoint.alt_type
- waypoint.name = String(self.waypoint.name)
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
if tot is not None:
self.set_waypoint_tot(waypoint, tot)
@@ -1279,13 +1405,18 @@ class PydcsWaypointBuilder:
package: Package, flight: Flight,
mission: Mission) -> PydcsWaypointBuilder:
builders = {
+ FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
+ FlightWaypointType.INGRESS_OCA_AIRCRAFT: OcaAircraftIngressBuilder,
+ FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
+ FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder,
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder,
+ FlightWaypointType.PATROL: RaceTrackEndBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
@@ -1296,7 +1427,7 @@ class PydcsWaypointBuilder:
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
"""
if (
- (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
+ (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
):
return True
@@ -1323,7 +1454,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle
))
- if not isinstance(self.flight.flight_plan, FormationFlightPlan):
+ if not isinstance(self.flight.flight_plan, LoiterFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot configure hold for for {self.flight} because "
@@ -1338,6 +1469,32 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return waypoint
+class BaiIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ target_group = self.package.target
+ if isinstance(target_group, TheaterGroundObject):
+ # Match search is used due to TheaterGroundObject.name not matching
+ # the Mission group name because of SkyNet prefixes.
+ tgroup = self.mission.find_group(target_group.group_name,
+ search="match")
+ if tgroup is not None:
+ task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
+ task.params["attackQtyLimit"] = False
+ task.params["directionEnabled"] = False
+ task.params["altitudeEnabled"] = False
+ task.params["groupAttack"] = True
+ waypoint.tasks.append(task)
+ else:
+ logging.error("Could not find group for BAI mission %s",
+ target_group.group_name)
+ else:
+ logging.error("Unexpected target type for BAI mission: %s",
+ target_group.__class__.__name__)
+ return waypoint
+
+
class CasIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
@@ -1371,14 +1528,16 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
- tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
- if tgroup is not None: # the Mission group name because of SkyNet prefixes.
- task = AttackGroup(tgroup.id)
+ # Match search is used due to TheaterGroundObject.name not matching
+ # the Mission group name because of SkyNet prefixes.
+ tgroup = self.mission.find_group(target_group.group_name,
+ search="match")
+ if tgroup is not None:
+ task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
- task.params["weaponType"] = 268402702 # Guided Weapons
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
@@ -1387,14 +1546,59 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return waypoint
+class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ target = self.package.target
+ if not isinstance(target, Airfield):
+ logging.error(
+ "Unexpected target type for OCA Strike mission: %s",
+ target.__class__.__name__)
+ return waypoint
+
+ task = EngageTargetsInZone(
+ position=target.position,
+ # Al Dhafra is 4 nm across at most. Add a little wiggle room in case
+ # the airport position from DCS is not centered.
+ radius=nm_to_meter(3),
+ targets=[Targets.All.Air]
+ )
+ task.params["attackQtyLimit"] = False
+ task.params["directionEnabled"] = False
+ task.params["altitudeEnabled"] = False
+ task.params["groupAttack"] = True
+ waypoint.tasks.append(task)
+ return waypoint
+
+
+class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ target = self.package.target
+ if not isinstance(target, Airfield):
+ logging.error(
+ "Unexpected target type for runway bombing mission: %s",
+ target.__class__.__name__)
+ return waypoint
+
+ waypoint.tasks.append(
+ BombingRunway(airport_id=target.airport.id, group_attack=True))
+ return waypoint
+
+
class SeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
- tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
- if tgroup is not None: # the Mission group name because of SkyNet prefixes.
+ # Match search is used due to TheaterGroundObject.name not matching
+ # the Mission group name because of SkyNet prefixes.
+ tgroup = self.mission.find_group(target_group.group_name,
+ search="match")
+ if tgroup is not None:
waypoint.add_task(EngageTargetsInZone(
position=tgroup.position,
radius=nm_to_meter(30),
@@ -1467,6 +1671,24 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
return waypoint
+class SweepIngressBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ if not isinstance(self.flight.flight_plan, SweepFlightPlan):
+ flight_plan_type = self.flight.flight_plan.__class__.__name__
+ logging.error(
+ f"Cannot create sweep for {self.flight} because "
+ f"{flight_plan_type} is not a sweep flight plan.")
+ return waypoint
+
+ waypoint.tasks.append(EngageTargets(
+ max_distance=nm_to_meter(50),
+ targets=[Targets.All.Air.Planes.Fighters]))
+
+ return waypoint
+
+
class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
@@ -1541,4 +1763,29 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack)
+
+ # TODO: Move the properties of this task into the flight plan?
+ # CAP is the only current user of this so it's not a big deal, but might
+ # be good to make this usable for things like BAI when we add that
+ # later.
+ cap_types = {FlightType.BARCAP, FlightType.TARCAP}
+ if self.flight.flight_type in cap_types:
+ waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
+ targets=[Targets.All.Air]))
+
+ return waypoint
+
+
+class RaceTrackEndBuilder(PydcsWaypointBuilder):
+ def build(self) -> MovingPoint:
+ waypoint = super().build()
+
+ if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
+ flight_plan_type = self.flight.flight_plan.__class__.__name__
+ logging.error(
+ f"Cannot create race track for {self.flight} because "
+ f"{flight_plan_type} does not define a patrol.")
+ return waypoint
+
+ self.waypoint.departure_time = self.flight.flight_plan.patrol_end_time
return waypoint
diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py
index 97aeea1f..fef24cc6 100644
--- a/gen/airsupportgen.py
+++ b/gen/airsupportgen.py
@@ -1,3 +1,4 @@
+import logging
from dataclasses import dataclass, field
from typing import List, Type
@@ -67,7 +68,7 @@ class AirSupportConflictGenerator:
def support_tasks(cls) -> List[Type[MainTask]]:
return [Refueling, AWACS]
- def generate(self, is_awacs_enabled):
+ def generate(self):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
fallback_tanker_number = 0
@@ -120,26 +121,28 @@ class AirSupportConflictGenerator:
self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan))
- if is_awacs_enabled:
- try:
- freq = self.radio_registry.alloc_uhf()
- awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
- awacs_flight = self.mission.awacs_flight(
- country=self.mission.country(self.game.player_country),
- name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
- plane_type=awacs_unit,
- altitude=AWACS_ALT,
- airport=None,
- position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
- frequency=freq.mhz,
- start_type=StartType.Warm,
- )
- awacs_flight.set_frequency(freq.mhz)
+ possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
- awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
- awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
+ if len(possible_awacs) > 0:
+ awacs_unit = possible_awacs[0]
+ freq = self.radio_registry.alloc_uhf()
+
+ awacs_flight = self.mission.awacs_flight(
+ country=self.mission.country(self.game.player_country),
+ name=namegen.next_awacs_name(self.mission.country(self.game.player_country)),
+ plane_type=awacs_unit,
+ altitude=AWACS_ALT,
+ airport=None,
+ position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
+ frequency=freq.mhz,
+ start_type=StartType.Warm,
+ )
+ awacs_flight.set_frequency(freq.mhz)
- self.air_support.awacs.append(AwacsInfo(
- str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq))
- except:
- print("No AWACS for faction")
\ No newline at end of file
+ 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")
\ No newline at end of file
diff --git a/gen/armor.py b/gen/armor.py
index 5685a120..79b009ef 100644
--- a/gen/armor.py
+++ b/gen/armor.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
import logging
import random
from dataclasses import dataclass
-from typing import List
+from typing import TYPE_CHECKING, List, Optional, Tuple
from dcs import Mission
from dcs.action import AITaskPush
@@ -10,31 +12,28 @@ from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction
-from dcs.task import (
- AttackGroup,
- ControlledTask,
- EPLRS,
- FireAtPoint,
- GoToWaypoint,
- Hold,
- OrbitAction,
- SetImmortalCommand,
- SetInvisibleCommand,
-)
+from dcs.task import (EPLRS, AttackGroup, ControlledTask, FireAtPoint,
+ GoToWaypoint, Hold, OrbitAction, SetImmortalCommand,
+ SetInvisibleCommand)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
+from dcs.unitgroup import VehicleGroup
from dcs.unittype import VehicleType
-
from game import db
-from .naming import namegen
-from gen.ground_forces.ai_ground_planner import (
- CombatGroupRole,
- DISTANCE_FROM_FRONTLINE,
-)
+from game.unitmap import UnitMap
+from game.utils import heading_sum, opposite_heading
+from game.theater.controlpoint import ControlPoint
+
+from gen.ground_forces.ai_ground_planner import (DISTANCE_FROM_FRONTLINE,
+ CombatGroup, CombatGroupRole)
+
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
-from game.plugins import LuaPluginManager
+from .naming import namegen
+
+if TYPE_CHECKING:
+ from game import Game
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@@ -65,79 +64,87 @@ class JtacInfo:
class GroundConflictGenerator:
- def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
+ def __init__(
+ self,
+ mission: Mission,
+ conflict: Conflict,
+ game: Game,
+ player_planned_combat_groups: List[CombatGroup],
+ enemy_planned_combat_groups: List[CombatGroup],
+ player_stance: CombatStance,
+ unit_map: UnitMap) -> None:
self.mission = mission
self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance)
- self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE])
+ self.enemy_stance = self._enemy_stance()
self.game = game
+ self.unit_map = unit_map
self.jtacs: List[JtacInfo] = []
- def _group_point(self, point) -> Point:
+ def _enemy_stance(self):
+ """Picks the enemy stance according to the number of planned groups on the frontline for each side"""
+ if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups):
+ return random.choice(
+ [
+ CombatStance.AGGRESSIVE,
+ CombatStance.AGGRESSIVE,
+ CombatStance.AGGRESSIVE,
+ CombatStance.ELIMINATION,
+ CombatStance.BREAKTHROUGH
+ ]
+ )
+ else:
+ return random.choice(
+ [
+ CombatStance.DEFENSIVE,
+ CombatStance.DEFENSIVE,
+ CombatStance.DEFENSIVE,
+ CombatStance.AMBUSH,
+ CombatStance.AGGRESSIVE
+ ]
+ )
+
+ @staticmethod
+ def _group_point(point: Point, base_distance) -> Point:
distance = random.randint(
- int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
- int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
+ int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
+ int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
- return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR)
+ return point.random_point_within(distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR)
def generate(self):
-
- player_groups = []
- enemy_groups = []
-
- combat_width = self.conflict.distance/2
- if combat_width > 500000:
- combat_width = 500000
- if combat_width < 35000:
- combat_width = 35000
-
- position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp)
+ position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater)
+ frontline_vector = Conflict.frontline_vector(
+ self.conflict.from_cp,
+ self.conflict.to_cp,
+ self.game.theater
+ )
# Create player groups at random position
- for group in self.player_planned_combat_groups:
- if group.role == CombatGroupRole.ARTILLERY:
- distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
- else:
- distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
- final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline)
-
- if final_position is not None:
- g = self._generate_group(
- side=self.mission.country(self.game.player_country),
- unit=group.units[0],
- heading=self.conflict.heading+90,
- count=len(group.units),
- at=final_position)
- g.set_skill(self.game.settings.player_skill)
- player_groups.append((g,group))
-
- self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90)
+ player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
# Create enemy groups at random position
- for group in self.enemy_planned_combat_groups:
- if group.role == CombatGroupRole.ARTILLERY:
- distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
- else:
- distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
- final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline)
-
- if final_position is not None:
- g = self._generate_group(
- side=self.mission.country(self.game.enemy_country),
- unit=group.units[0],
- heading=self.conflict.heading - 90,
- count=len(group.units),
- at=final_position)
- g.set_skill(self.game.settings.enemy_vehicle_skill)
- enemy_groups.append((g, group))
-
- self.gen_infantry_group_for_group(g, False, self.mission.country(self.game.enemy_country), self.conflict.heading - 90)
+ enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
# Plan combat actions for groups
- self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp)
- self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
+ self.plan_action_for_groups(
+ self.player_stance,
+ player_groups,
+ enemy_groups,
+ self.conflict.heading + 90,
+ self.conflict.from_cp,
+ self.conflict.to_cp
+ )
+ self.plan_action_for_groups(
+ self.enemy_stance,
+ enemy_groups,
+ player_groups,
+ self.conflict.heading - 90,
+ self.conflict.to_cp,
+ self.conflict.from_cp
+ )
# Add JTAC
if self.game.player_faction.has_jtac:
@@ -162,11 +169,13 @@ class GroundConflictGenerator:
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
- def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
-
- # Disable infantry unit gen if disabled
- if not self.game.settings.perf_infantry:
- return
+ def gen_infantry_group_for_group(
+ self,
+ group: VehicleGroup,
+ is_player: bool,
+ side: Country,
+ forward_heading: int
+ ) -> None:
infantry_position = group.points[0].position.random_point_within(250, 50)
@@ -180,7 +189,24 @@ class GroundConflictGenerator:
else:
faction = self.game.enemy_name
- possible_infantry_units = db.find_infantry(faction)
+ # Disable infantry unit gen if disabled
+ if not self.game.settings.perf_infantry:
+ if self.game.settings.manpads:
+ # 50% of armored units protected by manpad
+ if random.choice([True, False]):
+ manpads = db.find_manpad(faction)
+ if len(manpads) > 0:
+ u = random.choice(manpads)
+ self.mission.vehicle_group(
+ side,
+ namegen.next_infantry_name(side, cp, u), u,
+ position=infantry_position,
+ group_size=1,
+ heading=forward_heading,
+ move_formation=PointAction.OffRoad)
+ return
+
+ possible_infantry_units = db.find_infantry(faction, allow_manpad=self.game.settings.manpads)
if len(possible_infantry_units) == 0:
return
@@ -204,125 +230,191 @@ class GroundConflictGenerator:
heading=forward_heading,
move_formation=PointAction.OffRoad)
+ def _plan_artillery_action(
+ self,
+ stance: CombatStance,
+ gen_group: CombatGroup,
+ dcs_group: VehicleGroup,
+ forward_heading: int,
+ target: Point
+ ) -> bool:
+ """
+ Handles adding the DCS tasks for artillery groups for all combat stances.
+ Returns True if tasking was added, returns False if the stance was not a combat stance.
+ """
+ if stance != CombatStance.RETREAT:
+ hold_task = Hold()
+ hold_task.number = 1
+ dcs_group.add_trigger_action(hold_task)
- def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp):
+ # Artillery strike random start
+ artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
+ artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
+ # TODO: Update to fire at group instead of point
+ fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
+ fire_task.number = 2 if stance != CombatStance.RETREAT else 1
+ dcs_group.add_trigger_action(fire_task)
+ artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
+ self.mission.triggerrules.triggers.append(artillery_trigger)
+
+ # Artillery will fall back when under attack
+ if stance != CombatStance.RETREAT:
+
+ # Hold position
+ dcs_group.points[0].tasks.append(Hold())
+ retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
+ dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
+ dcs_group.points[1].tasks.append(Hold())
+ dcs_group.add_waypoint(retreat, PointAction.OffRoad)
+
+ artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
+ for i, u in enumerate(dcs_group.units):
+ artillery_fallback.add_condition(UnitDamaged(u.id))
+ if i < len(dcs_group.units) - 1:
+ artillery_fallback.add_condition(Or())
+
+ hold_2 = Hold()
+ hold_2.number = 3
+ dcs_group.add_trigger_action(hold_2)
+
+ retreat_task = GoToWaypoint(to_index=3)
+ retreat_task.number = 4
+ dcs_group.add_trigger_action(retreat_task)
+
+ artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
+ self.mission.triggerrules.triggers.append(artillery_fallback)
+
+ for u in dcs_group.units:
+ u.initial = True
+ u.heading = forward_heading + random.randint(-5, 5)
+ return True
+ return False
+
+ def _plan_tank_ifv_action(
+ self,
+ stance: CombatStance,
+ enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
+ dcs_group: VehicleGroup,
+ forward_heading: int,
+ to_cp: ControlPoint,
+ ) -> bool:
+ """
+ Handles adding the DCS tasks for tank and IFV groups for all combat stances.
+ Returns True if tasking was added, returns False if the stance was not a combat stance.
+ """
+ if stance == CombatStance.AGGRESSIVE:
+ # Attack nearest enemy if any
+ # Then move forward OR Attack enemy base if it is not too far away
+ target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
+ if target is not None:
+ rand_offset = Point(
+ random.randint(
+ -RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
+ ),
+ random.randint(
+ -RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
+ )
+ )
+ dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
+ dcs_group.points[1].tasks.append(AttackGroup(target.id))
+
+ if (
+ to_cp.position.distance_to_point(dcs_group.points[0].position)
+ <=
+ AGGRESIVE_MOVE_DISTANCE
+ ):
+ attack_point = to_cp.position.random_point_within(500, 0)
+ else:
+ attack_point = self.find_offensive_point(
+ dcs_group,
+ forward_heading,
+ AGGRESIVE_MOVE_DISTANCE
+ )
+ dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
+ elif stance == CombatStance.BREAKTHROUGH:
+ # In breakthrough mode, the units will move forward
+ # If the enemy base is close enough, the units will attack the base
+ if to_cp.position.distance_to_point(
+ dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
+ attack_point = to_cp.position.random_point_within(500, 0)
+ else:
+ attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
+ dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
+ elif stance == CombatStance.ELIMINATION:
+ # In elimination mode, the units focus on destroying as much enemy groups as possible
+ targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
+ for i, target in enumerate(targets, start=1):
+ rand_offset = Point(
+ random.randint(
+ -RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK
+ ),
+ random.randint(
+ -RANDOM_OFFSET_ATTACK,
+ RANDOM_OFFSET_ATTACK
+ )
+ )
+ dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
+ dcs_group.points[i].tasks.append(AttackGroup(target.id))
+ if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
+ attack_point = to_cp.position.random_point_within(500, 0)
+ dcs_group.add_waypoint(attack_point)
+
+ if stance != CombatStance.RETREAT:
+ self.add_morale_trigger(dcs_group, forward_heading)
+ return True
+ return False
+
+ def _plan_apc_atgm_action(
+ self,
+ stance: CombatStance,
+ dcs_group: VehicleGroup,
+ forward_heading: int,
+ to_cp: ControlPoint,
+ ) -> bool:
+ """
+ Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
+ Returns True if tasking was added, returns False if the stance was not a combat stance.
+ """
+ if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
+ # APC & ATGM will never move too much forward, but will follow along any offensive
+ if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
+ attack_point = to_cp.position.random_point_within(500, 0)
+ else:
+ attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
+ dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
+
+ if stance != CombatStance.RETREAT:
+ self.add_morale_trigger(dcs_group, forward_heading)
+ return True
+ return False
+
+ def plan_action_for_groups(
+ self, stance: CombatStance,
+ ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
+ enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
+ forward_heading: int,
+ from_cp: ControlPoint,
+ to_cp: ControlPoint
+ ) -> None:
if not self.game.settings.perf_moving_units:
return
for dcs_group, group in ally_groups:
-
- if hasattr(group.units[0], 'eplrs'):
- if group.units[0].eplrs:
- dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
+ if hasattr(group.units[0], 'eplrs') and group.units[0].eplrs:
+ dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY:
- # Fire on any ennemy in range
if self.game.settings.perf_artillery:
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
if target is not None:
-
- if stance != CombatStance.RETREAT:
- hold_task = Hold()
- hold_task.number = 1
- dcs_group.add_trigger_action(hold_task)
-
- # Artillery strike random start
- artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
- artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45)* 60))
-
- fire_task = FireAtPoint(target, len(group.units) * 10, 100)
- if stance != CombatStance.RETREAT:
- fire_task.number = 2
- else:
- fire_task.number = 1
- dcs_group.add_trigger_action(fire_task)
- artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
- self.mission.triggerrules.triggers.append(artillery_trigger)
-
- # Artillery will fall back when under attack
- if stance != CombatStance.RETREAT:
-
- # Hold position
- dcs_group.points[0].tasks.append(Hold())
- retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
- dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
- dcs_group.points[1].tasks.append(Hold())
- dcs_group.add_waypoint(retreat, PointAction.OffRoad)
-
- artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
- for i, u in enumerate(dcs_group.units):
- artillery_fallback.add_condition(UnitDamaged(u.id))
- if i < len(dcs_group.units) - 1:
- artillery_fallback.add_condition(Or())
-
- hold_2 = Hold()
- hold_2.number = 3
- dcs_group.add_trigger_action(hold_2)
-
- retreat_task = GoToWaypoint(toIndex=3)
- retreat_task.number = 4
- dcs_group.add_trigger_action(retreat_task)
-
- artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
- self.mission.triggerrules.triggers.append(artillery_fallback)
-
- for u in dcs_group.units:
- u.initial = True
- u.heading = forward_heading + random.randint(-5,5)
+ self._plan_artillery_action(stance, group, dcs_group, forward_heading, target)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
- if stance == CombatStance.AGGRESSIVE:
- # Attack nearest enemy if any
- # Then move forward OR Attack enemy base if it is not too far away
- target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
- if target is not None:
- rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
- dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
- dcs_group.points[1].tasks.append(AttackGroup(target.id))
-
- if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
- attack_point = to_cp.position.random_point_within(500, 0)
- else:
- attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
- dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
- elif stance == CombatStance.BREAKTHROUGH:
- # In breakthrough mode, the units will move forward
- # If the enemy base is close enough, the units will attack the base
- if to_cp.position.distance_to_point(
- dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
- attack_point = to_cp.position.random_point_within(500, 0)
- else:
- attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
- dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
- elif stance == CombatStance.ELIMINATION:
- # In elimination mode, the units focus on destroying as much enemy groups as possible
- targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
- i = 1
- for target in targets:
- rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
- dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
- dcs_group.points[i].tasks.append(AttackGroup(target.id))
- i = i + 1
- if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
- attack_point = to_cp.position.random_point_within(500, 0)
- dcs_group.add_waypoint(attack_point)
-
- if stance != CombatStance.RETREAT:
- self.add_morale_trigger(dcs_group, forward_heading)
+ self._plan_tank_ifv_action(stance, enemy_groups, dcs_group, forward_heading, to_cp)
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
-
- if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
- # APC & ATGM will never move too much forward, but will follow along any offensive
- if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
- attack_point = to_cp.position.random_point_within(500, 0)
- else:
- attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
- dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
-
- if stance != CombatStance.RETREAT:
- self.add_morale_trigger(dcs_group, forward_heading)
+ self._plan_apc_atgm_action(stance, dcs_group, forward_heading, to_cp)
if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back
@@ -332,11 +424,10 @@ class GroundConflictGenerator:
else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
- dcs_group.add_waypoint(retreat_point, PointAction.OnRoad)
+ dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
-
- def add_morale_trigger(self, dcs_group, forward_heading):
+ def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None:
"""
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
"""
@@ -353,10 +444,13 @@ class GroundConflictGenerator:
dcs_group.manualHeading = True
# We add a new retreat waypoint
- dcs_group.add_waypoint(self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), PointAction.OffRoad)
+ dcs_group.add_waypoint(
+ self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)),
+ PointAction.OffRoad
+ )
# Fallback task
- fallback = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points)))
+ fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
fallback.enabled = False
dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(fallback)
@@ -372,8 +466,12 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(fallback)
-
- def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE):
+ @staticmethod
+ def find_retreat_point(
+ dcs_group: VehicleGroup,
+ frontline_heading: int,
+ distance: int = RETREAT_DISTANCE
+ ) -> Point:
"""
Find a point to retreat to
:param dcs_group: DCS mission group we are searching a retreat point for
@@ -382,7 +480,12 @@ class GroundConflictGenerator:
"""
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
- def find_offensive_point(self, dcs_group, frontline_heading, distance):
+ @staticmethod
+ def find_offensive_point(
+ dcs_group: VehicleGroup,
+ frontline_heading: int,
+ distance: int
+ ) -> Point:
"""
Find a point to attack
:param dcs_group: DCS mission group we are searching an attack point for
@@ -392,24 +495,36 @@ class GroundConflictGenerator:
"""
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
- def find_n_nearest_enemy_groups(self, player_group, enemy_groups, n):
+ @staticmethod
+ def find_n_nearest_enemy_groups(
+ player_group: VehicleGroup,
+ enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
+ n: int
+ ) -> List[VehicleGroup]:
"""
- Return the neaarest enemy group for the player group
+ Return the nearest enemy group for the player group
@param group Group for which we should find the nearest ennemies
@param enemy_groups Potential enemy groups
@param n number of nearby groups to take
"""
- targets = []
- sorted_list = sorted(enemy_groups, key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position))
+ targets = [] # type: List[Optional[VehicleGroup]]
+ sorted_list = sorted(
+ enemy_groups,
+ key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position)
+ )
for i in range(n):
+ # TODO: Is this supposed to return no groups if enemy_groups is less than n?
if len(sorted_list) <= i:
break
else:
targets.append(sorted_list[i][0])
return targets
-
- def find_nearest_enemy_group(self, player_group, enemy_groups):
+ @staticmethod
+ def find_nearest_enemy_group(
+ player_group: VehicleGroup,
+ enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
+ ) -> Optional[VehicleGroup]:
"""
Search the enemy groups for a potential target suitable to armored assault
@param group Group for which we should find the nearest ennemy
@@ -417,29 +532,33 @@ class GroundConflictGenerator:
"""
min_distance = 99999999
target = None
- for dcs_group, group in enemy_groups:
+ for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
if dist < min_distance:
min_distance = dist
target = dcs_group
return target
-
- def get_artillery_target_in_range(self, dcs_group, group, enemy_groups):
+ @staticmethod
+ def get_artillery_target_in_range(
+ dcs_group: VehicleGroup,
+ group: CombatGroup,
+ enemy_groups: List[Tuple[VehicleGroup, CombatGroup]]
+ ) -> Optional[Point]:
"""
Search the enemy groups for a potential target suitable to an artillery unit
"""
+ # TODO: Update to return a list of groups instead of a single point
rng = group.units[0].threat_range
- if len(enemy_groups) == 0:
+ if not enemy_groups:
return None
- for o in range(10):
+ for _ in range(10):
potential_target = random.choice(enemy_groups)[0]
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
if distance_to_target < rng:
return potential_target.points[0].position
return None
-
def get_artilery_group_distance_from_frontline(self, group):
"""
For artilery group, decide the distance from frontline with the range of the unit
@@ -451,23 +570,85 @@ class GroundConflictGenerator:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
return rg
-
- def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
+ def get_valid_position_for_group(
+ self,
+ conflict_position: Point,
+ combat_width: int,
+ distance_from_frontline: int,
+ heading: int,
+ spawn_heading: int
+ ):
i = 0
- while i < 25: # 25 attempt for valid position
- heading_diff = -90 if isplayer else 90
- shifted = conflict_position[0].point_from_heading(self.conflict.heading,
- random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
- final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
+ while i < 1000:
+ shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
+ final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline)
if self.conflict.theater.is_on_land(final_position):
return final_position
- else:
- i = i + 1
- continue
+ i += 1
+ continue
return None
- def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0):
+ def _generate_groups(
+ self,
+ groups: List[CombatGroup],
+ frontline_vector: Tuple[Point, int, int],
+ is_player: bool
+ ) -> List[Tuple[VehicleGroup, CombatGroup]]:
+ """Finds valid positions for planned groups and generates a pydcs group for them"""
+ positioned_groups = []
+ position, heading, combat_width = frontline_vector
+ spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90))
+ country = self.game.player_country if is_player else self.game.enemy_country
+ for group in groups:
+ if group.role == CombatGroupRole.ARTILLERY:
+ distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
+ else:
+ distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
+
+ final_position = self.get_valid_position_for_group(
+ position,
+ combat_width,
+ distance_from_frontline,
+ heading,
+ spawn_heading
+ )
+
+ if final_position is not None:
+ g = self._generate_group(
+ self.mission.country(country),
+ group.units[0],
+ len(group.units),
+ final_position,
+ distance_from_frontline,
+ heading=opposite_heading(spawn_heading),
+ )
+ if is_player:
+ g.set_skill(self.game.settings.player_skill)
+ else:
+ g.set_skill(self.game.settings.enemy_vehicle_skill)
+ positioned_groups.append((g, group))
+ self.gen_infantry_group_for_group(
+ g,
+ is_player,
+ self.mission.country(country),
+ opposite_heading(spawn_heading)
+ )
+ else:
+ logging.warning(f"Unable to get valid position for {group}")
+
+ return positioned_groups
+
+ def _generate_group(
+ self,
+ side: Country,
+ unit: VehicleType,
+ count: int,
+ at: Point,
+ distance_from_frontline,
+ move_formation: PointAction = PointAction.OffRoad,
+ heading=0,
+ ) -> VehicleGroup:
if side == self.conflict.attackers_country:
cp = self.conflict.from_cp
@@ -478,13 +659,15 @@ class GroundConflictGenerator:
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, cp.id, unit), unit,
- position=self._group_point(at),
+ position=self._group_point(at, distance_from_frontline),
group_size=count,
heading=heading,
move_formation=move_formation)
+ self.unit_map.add_front_line_units(group, cp)
+
for c in range(count):
vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True
- return group
\ No newline at end of file
+ return group
diff --git a/gen/ato.py b/gen/ato.py
index d814e5ee..ab104bab 100644
--- a/gen/ato.py
+++ b/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?
diff --git a/gen/briefinggen.py b/gen/briefinggen.py
index 062ee8b1..14cef8de 100644
--- a/gen/briefinggen.py
+++ b/gen/briefinggen.py
@@ -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:
diff --git a/gen/conflictgen.py b/gen/conflictgen.py
index 3c9eecfe..65ea4058 100644
--- a/gen/conflictgen.py
+++ b/gen/conflictgen.py
@@ -1,58 +1,16 @@
import logging
import random
-from typing import Tuple
+from typing import Tuple, Optional
from dcs.country import Country
from dcs.mapping import Point
-from theater import ConflictTheater, ControlPoint
+from game.theater.conflicttheater import ConflictTheater, FrontLine
+from game.theater.controlpoint import ControlPoint
+from game.utils import heading_sum, opposite_heading
-AIR_DISTANCE = 40000
-
-CAPTURE_AIR_ATTACKERS_DISTANCE = 25000
-CAPTURE_AIR_DEFENDERS_DISTANCE = 60000
-STRIKE_AIR_ATTACKERS_DISTANCE = 45000
-STRIKE_AIR_DEFENDERS_DISTANCE = 25000
-
-CAP_CAS_DISTANCE = 10000, 120000
-
-GROUND_INTERCEPT_SPREAD = 5000
-GROUND_DISTANCE_FACTOR = 1.4
-GROUND_DISTANCE = 2000
-
-GROUND_ATTACK_DISTANCE = 25000, 13000
-
-TRANSPORT_FRONTLINE_DIST = 1800
-
-INTERCEPT_ATTACKERS_HEADING = -45, 45
-INTERCEPT_DEFENDERS_HEADING = -10, 10
-INTERCEPT_CONFLICT_DISTANCE = 50000
-INTERCEPT_ATTACKERS_DISTANCE = 100000
-INTERCEPT_MAX_DISTANCE = 160000
-INTERCEPT_MIN_DISTANCE = 100000
-
-NAVAL_INTERCEPT_DISTANCE_FACTOR = 1
-NAVAL_INTERCEPT_DISTANCE_MAX = 40000
-NAVAL_INTERCEPT_STEP = 5000
FRONTLINE_LENGTH = 80000
-FRONTLINE_MIN_CP_DISTANCE = 5000
-FRONTLINE_DISTANCE_STRENGTH_FACTOR = 0.7
-
-
-def _opposite_heading(h):
- return h+180
-
-
-def _heading_sum(h, a) -> int:
- h += a
- if h > 360:
- return h - 360
- elif h < 0:
- return 360 + h
- else:
- return h
-
class Conflict:
def __init__(self,
@@ -64,12 +22,9 @@ class Conflict:
attackers_country: Country,
defenders_country: Country,
position: Point,
- heading=None,
- distance=None,
- ground_attackers_location: Point = None,
- ground_defenders_location: Point = None,
- air_attackers_location: Point = None,
- air_defenders_location: Point = None):
+ heading: Optional[int] = None,
+ size: Optional[int] = None
+ ):
self.attackers_side = attackers_side
self.defenders_side = defenders_side
@@ -81,307 +36,39 @@ class Conflict:
self.theater = theater
self.position = position
self.heading = heading
- self.distance = distance
- self.size = to_cp.size
- self.radials = to_cp.radials
- self.ground_attackers_location = ground_attackers_location
- self.ground_defenders_location = ground_defenders_location
- self.air_attackers_location = air_attackers_location
- self.air_defenders_location = air_defenders_location
-
- @property
- def center(self) -> Point:
- return self.position.point_from_heading(self.heading, self.distance / 2)
-
- @property
- def tail(self) -> Point:
- return self.position.point_from_heading(self.heading, self.distance)
-
- @property
- def is_vector(self) -> bool:
- return self.heading is not None
-
- @property
- def opposite_heading(self) -> int:
- return _heading_sum(self.heading, 180)
-
- @property
- def to_size(self):
- return self.to_cp.size * GROUND_DISTANCE_FACTOR
-
- def find_insertion_point(self, other_point: Point) -> Point:
- if self.is_vector:
- dx = self.position.x - self.tail.x
- dy = self.position.y - self.tail.y
- dr2 = float(dx ** 2 + dy ** 2)
-
- lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2
- if lerp < 0:
- lerp = 0
- elif lerp > 1:
- lerp = 1
-
- x = lerp * dx + self.tail.x
- y = lerp * dy + self.tail.y
- return Point(x, y)
- else:
- return self.position
-
- def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
- return Conflict._find_ground_position(at, max_distance, heading, self.theater)
+ self.size = size
@classmethod
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
- def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
- attack_heading = from_cp.position.heading_between_point(to_cp.position)
- attack_distance = from_cp.position.distance_to_point(to_cp.position)
- middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
-
- strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0
- position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
- return position, _opposite_heading(attack_heading)
-
+ def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
+ frontline = FrontLine(from_cp, to_cp, theater)
+ attack_heading = frontline.attack_heading
+ position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
+ return position, opposite_heading(attack_heading)
@classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
"""
- probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
- probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
- intersection = probe.intersection(theater.land_poly)
-
- if isinstance(intersection, geometry.LineString):
- intersection = intersection
- elif isinstance(intersection, geometry.MultiLineString):
- intersection = intersection.geoms[0]
- else:
- print(intersection)
- return None
-
- return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
+ Returns a vector for a valid frontline location avoiding exclusion zones.
"""
- frontline = cls.frontline_position(theater, from_cp, to_cp)
- center_position, heading = frontline
- left_position, right_position = None, None
-
- if not theater.is_on_land(center_position):
- pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater)
- if pos:
- right_position = pos
- center_position = pos
- else:
- pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater)
- if pos:
- left_position = pos
- center_position = pos
-
- if left_position is None:
- left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater)
-
- if right_position is None:
- right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater)
-
- return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
-
- @classmethod
- def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
- pos = initial
- for offset in range(0, int(max_distance), 500):
- new_pos = initial.point_from_heading(heading, offset)
- if theater.is_on_land(new_pos):
- pos = new_pos
- else:
- return pos
- return pos
-
- """
- probe_end_point = initial.point_from_heading(heading, max_distance)
- probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)])
-
- intersection = probe.intersection(theater.land_poly)
- if intersection is geometry.LineString:
- return Point(*intersection.xy[1])
- elif intersection is geometry.MultiLineString:
- return Point(*intersection.geoms[0].xy[1])
-
- return None
- """
-
- @classmethod
- def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
- pos = initial
- for _ in range(0, int(max_distance), 500):
- if theater.is_on_land(pos):
- return pos
-
- pos = pos.point_from_heading(heading, 500)
- """
- probe_end_point = initial.point_from_heading(heading, max_distance)
- probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
-
- intersection = probe.intersection(theater.land_poly)
- if isinstance(intersection, geometry.LineString):
- return Point(*intersection.xy[1])
- elif isinstance(intersection, geometry.MultiLineString):
- return Point(*intersection.geoms[0].xy[1])
- """
-
- logging.error("Didn't find ground position ({})!".format(initial))
- return initial
-
- @classmethod
- def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- position = to_cp.position
- attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
- attack_heading = to_cp.find_radial(attack_raw_heading)
- defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
-
- distance = GROUND_DISTANCE
- attackers_location = position.point_from_heading(attack_heading, distance)
- attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater)
-
- defenders_location = position.point_from_heading(defense_heading, 0)
- defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater)
-
- return cls(
- position=position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=attackers_location,
- ground_defenders_location=defenders_location,
- air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE),
- air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), CAPTURE_AIR_DEFENDERS_DISTANCE)
- )
-
- @classmethod
- def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- position = to_cp.position
- attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
- attack_heading = to_cp.find_radial(attack_raw_heading)
- defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
-
- distance = to_cp.size * GROUND_DISTANCE_FACTOR
- attackers_location = position.point_from_heading(attack_heading, distance)
- attackers_location = Conflict._find_ground_position(
- attackers_location, int(distance * 2),
- _heading_sum(attack_heading, 180), theater)
-
- defenders_location = position.point_from_heading(defense_heading, distance)
- defenders_location = Conflict._find_ground_position(
- defenders_location, int(distance * 2),
- _heading_sum(defense_heading, 180), theater)
-
- return cls(
- position=position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=attackers_location,
- ground_defenders_location=defenders_location,
- air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE),
- air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE)
- )
-
- @classmethod
- def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point:
- raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5
- distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE)
- heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100))
- return from_cp.position.point_from_heading(heading, distance)
-
- @classmethod
- def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- heading = from_cp.position.heading_between_point(position)
- return cls(
- position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE),
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=None,
- ground_defenders_location=None,
- air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
- air_defenders_location=position
- )
-
- @classmethod
- def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- heading = random.choice(to_cp.radials)
- initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE)
- position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
- if not position:
- heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
- position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR)
-
- return cls(
- position=position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=position,
- ground_defenders_location=None,
- air_attackers_location=None,
- air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE),
- )
-
- @classmethod
- def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater)
- if not frontline_position:
- assert False
-
- heading = frontline_heading
- starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000),
- GROUND_INTERCEPT_SPREAD,
- _opposite_heading(heading), theater)
- if not starting_position:
- starting_position = frontline_position
- destination_position = frontline_position
- else:
- destination_position = frontline_position
-
- return cls(
- position=destination_position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=None,
- ground_defenders_location=starting_position,
- air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE),
- air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE),
- )
+ center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
+ left_heading = heading_sum(heading, -90)
+ right_heading = heading_sum(heading, 90)
+ left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
+ right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
+ distance = int(left_position.distance_to_point(right_position))
+ return left_position, right_heading, distance
@classmethod
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
-
- return cls(
+ conflict = cls(
position=position,
heading=heading,
- distance=distance,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
@@ -389,114 +76,30 @@ class Conflict:
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
- ground_attackers_location=None,
- ground_defenders_location=None,
- air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
- air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
+ size=distance
)
+ return conflict
@classmethod
- def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- assert cls.has_frontline_between(from_cp, to_cp)
-
- position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
- attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
- attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
- defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
-
- return cls(
- position=position,
- heading=heading,
- distance=distance,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- air_attackers_location=attackers_position,
- air_defenders_location=defenders_position,
- )
+ def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
+ """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
+ pos = initial
+ for distance in range(0, int(max_distance), 100):
+ pos = initial.point_from_heading(heading, distance)
+ if not theater.is_on_land(pos):
+ return initial.point_from_heading(heading, distance - 100)
+ return pos
@classmethod
- def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- position = to_cp.position
- attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
- defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
-
- distance = to_cp.size * GROUND_DISTANCE_FACTOR
- defenders_location = position.point_from_heading(defense_heading, distance)
- defenders_location = Conflict._find_ground_position(
- defenders_location, int(distance * 2),
- _heading_sum(defense_heading, 180), theater)
-
- return cls(
- position=position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=None,
- ground_defenders_location=defenders_location,
- air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE),
- air_defenders_location=position
- )
-
- @classmethod
- def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- radial = random.choice(to_cp.sea_radials)
-
- initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX)
- initial_position = to_cp.position.point_from_heading(radial, initial_distance)
- for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
- position = initial_position.point_from_heading(_opposite_heading(radial), offset)
-
- if not theater.is_on_land(position):
- break
- return position
-
- @classmethod
- def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- attacker_heading = from_cp.position.heading_between_point(to_cp.position)
- return cls(
- position=position,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=None,
- ground_defenders_location=position,
- air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
- air_defenders_location=position.point_from_heading(_opposite_heading(attacker_heading), AIR_DISTANCE)
- )
-
- @classmethod
- def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
- frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp)
- initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST)
- dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
- if not dest:
- radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
- dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR)
-
- return cls(
- position=dest,
- theater=theater,
- from_cp=from_cp,
- to_cp=to_cp,
- attackers_side=attacker_name,
- defenders_side=defender_name,
- attackers_country=attacker,
- defenders_country=defender,
- ground_attackers_location=from_cp.position,
- ground_defenders_location=frontline_position,
- air_attackers_location=from_cp.position.point_from_heading(0, 100),
- air_defenders_location=frontline_position
- )
\ No newline at end of file
+ def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
+ """Finds the nearest valid ground position along a provided heading and it's inverse"""
+ pos = initial
+ if theater.is_on_land(pos):
+ return pos
+ for distance in range(0, int(max_distance), 100):
+ pos = initial.point_from_heading(heading, distance)
+ if theater.is_on_land(pos):
+ return pos
+ pos = initial.point_from_heading(opposite_heading(heading), distance)
+ logging.error("Didn't find ground position ({})!".format(initial))
+ return initial
diff --git a/gen/fleet/cn_dd_group.py b/gen/fleet/cn_dd_group.py
index 020f68c2..c2b51c88 100644
--- a/gen/fleet/cn_dd_group.py
+++ b/gen/fleet/cn_dd_group.py
@@ -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])
diff --git a/gen/fleet/dd_group.py b/gen/fleet/dd_group.py
index b11de653..c6a3e115 100644
--- a/gen/fleet/dd_group.py
+++ b/gen/fleet/dd_group.py
@@ -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
diff --git a/gen/fleet/ru_dd_group.py b/gen/fleet/ru_dd_group.py
index 0948991a..69d0af57 100644
--- a/gen/fleet/ru_dd_group.py
+++ b/gen/fleet/ru_dd_group.py
@@ -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])
diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py
index ce68be2d..5d6ff05f 100644
--- a/gen/flights/ai_flight_planner.py
+++ b/gen/flights/ai_flight_planner.py
@@ -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]
diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py
index 30749e33..2b00f2fe 100644
--- a/gen/flights/ai_flight_planner_db.py
+++ b/gen/flights/ai_flight_planner_db.py
@@ -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 []
diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py
index a6045dde..5bba28db 100644
--- a/gen/flights/closestairfields.py
+++ b/gen/flights/closestairfields.py
@@ -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:
diff --git a/gen/flights/flight.py b/gen/flights/flight.py
index 2462a0a5..b3f5c286 100644
--- a/gen/flights/flight.py
+++ b/gen/flights/flight.py
@@ -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}"
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 430a8c11..8d4e532f 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -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"
diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py
index 0bddfcaf..714fbd25 100644
--- a/gen/flights/traveltime.py
+++ b/gen/flights/traveltime.py
@@ -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:
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
index 3b67e8e0..e8bcce7d 100644
--- a/gen/flights/waypointbuilder.py
+++ b/gen/flights/waypointbuilder.py
@@ -5,17 +5,23 @@ from typing import List, Optional, Tuple, Union
from dcs.mapping import Point
from dcs.unit import Unit
+from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine
+from game.theater import (
+ ControlPoint,
+ MissionTarget,
+ OffMapSpawn,
+ TheaterGroundObject,
+)
from game.weather import Conditions
-from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
- target: Union[TheaterGroundObject, Unit]
+ target: Union[VehicleGroup, TheaterGroundObject, Unit]
class WaypointBuilder:
@@ -31,8 +37,7 @@ class WaypointBuilder:
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
- @staticmethod
- def takeoff(departure: ControlPoint) -> FlightWaypoint:
+ def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
Note that the takeoff waypoint will automatically be created by pydcs
@@ -43,36 +48,93 @@ class WaypointBuilder:
departure: Departure airfield or carrier.
"""
position = departure.position
- waypoint = FlightWaypoint(
- FlightWaypointType.TAKEOFF,
- position.x,
- position.y,
- 0
- )
- waypoint.name = "TAKEOFF"
- waypoint.alt_type = "RADIO"
- waypoint.description = "Takeoff"
- waypoint.pretty_name = "Takeoff"
+ if isinstance(departure, OffMapSpawn):
+ waypoint = FlightWaypoint(
+ FlightWaypointType.NAV,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.rendezvous_altitude
+ )
+ waypoint.name = "NAV"
+ waypoint.alt_type = "BARO"
+ waypoint.description = "Enter theater"
+ waypoint.pretty_name = "Enter theater"
+ else:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.TAKEOFF,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "TAKEOFF"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Takeoff"
+ waypoint.pretty_name = "Takeoff"
return waypoint
- @staticmethod
- def land(arrival: ControlPoint) -> FlightWaypoint:
+ def land(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
+ if isinstance(arrival, OffMapSpawn):
+ waypoint = FlightWaypoint(
+ FlightWaypointType.NAV,
+ position.x,
+ position.y,
+ 500 if self.is_helo else self.doctrine.rendezvous_altitude
+ )
+ waypoint.name = "NAV"
+ waypoint.alt_type = "BARO"
+ waypoint.description = "Exit theater"
+ waypoint.pretty_name = "Exit theater"
+ else:
+ waypoint = FlightWaypoint(
+ FlightWaypointType.LANDING_POINT,
+ position.x,
+ position.y,
+ 0
+ )
+ waypoint.name = "LANDING"
+ waypoint.alt_type = "RADIO"
+ waypoint.description = "Land"
+ waypoint.pretty_name = "Land"
+ return waypoint
+
+ def divert(self,
+ divert: Optional[ControlPoint]) -> Optional[FlightWaypoint]:
+ """Create divert waypoint for the given arrival airfield or carrier.
+
+ Args:
+ divert: Divert airfield or carrier.
+ """
+ if divert is None:
+ return None
+
+ position = divert.position
+ if isinstance(divert, OffMapSpawn):
+ if self.is_helo:
+ altitude = 500
+ else:
+ altitude = self.doctrine.rendezvous_altitude
+ altitude_type = "BARO"
+ else:
+ altitude = 0
+ altitude_type = "RADIO"
+
waypoint = FlightWaypoint(
- FlightWaypointType.LANDING_POINT,
+ FlightWaypointType.DIVERT,
position.x,
position.y,
- 0
+ altitude
)
- waypoint.name = "LANDING"
- waypoint.alt_type = "RADIO"
- waypoint.description = "Land"
- waypoint.pretty_name = "Land"
+ waypoint.alt_type = altitude_type
+ waypoint.name = "DIVERT"
+ waypoint.description = "Divert"
+ waypoint.pretty_name = "Divert"
+ waypoint.only_for_player = True
return waypoint
def hold(self, position: Point) -> FlightWaypoint:
@@ -111,33 +173,8 @@ class WaypointBuilder:
waypoint.name = "SPLIT"
return waypoint
- def ingress_cas(self, position: Point,
- objective: MissionTarget) -> FlightWaypoint:
- return self._ingress(FlightWaypointType.INGRESS_CAS, position,
- objective)
-
- def ingress_escort(self, position: Point,
- objective: MissionTarget) -> FlightWaypoint:
- return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
- objective)
-
- def ingress_dead(self, position:Point,
- objective: MissionTarget) -> FlightWaypoint:
- return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
- objective)
-
- def ingress_sead(self, position: Point,
- objective: MissionTarget) -> FlightWaypoint:
- return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
- objective)
-
- def ingress_strike(self, position: Point,
- objective: MissionTarget) -> FlightWaypoint:
- return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
- objective)
-
- def _ingress(self, ingress_type: FlightWaypointType, position: Point,
- objective: MissionTarget) -> FlightWaypoint:
+ def ingress(self, ingress_type: FlightWaypointType, position: Point,
+ objective: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint(
ingress_type,
position.x,
@@ -163,6 +200,9 @@ class WaypointBuilder:
waypoint.name = "EGRESS"
return waypoint
+ def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
+ return self._target_point(target, f"ATTACK {target.name}")
+
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
@@ -183,6 +223,7 @@ class WaypointBuilder:
waypoint.description = description
waypoint.pretty_name = description
waypoint.name = target.name
+ waypoint.alt_type = "RADIO"
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
@@ -198,8 +239,12 @@ class WaypointBuilder:
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"DEAD on {target.name}", target)
+ def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
+ return self._target_area(f"ATTACK {target.name}", target, flyover=True)
+
@staticmethod
- def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
+ def _target_area(name: str, location: MissionTarget,
+ flyover: bool = False) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
@@ -209,10 +254,19 @@ class WaypointBuilder:
waypoint.description = name
waypoint.pretty_name = name
waypoint.name = name
- # The target waypoints are only for the player's benefit. AI tasks for
+ waypoint.alt_type = "RADIO"
+
+ # Most target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
- waypoint.only_for_player = True
+ #
+ # The exception is for flight plans that require passing over the
+ # target. For example, OCA strikes need to get close enough to detect
+ # the targets in their engagement zone or they will RTB immediately.
+ if flyover:
+ waypoint.flyover = True
+ else:
+ waypoint.only_for_player = True
return waypoint
def cas(self, position: Point) -> FlightWaypoint:
@@ -278,6 +332,56 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
+ @staticmethod
+ def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
+ """Creates a sweep start waypoint.
+
+ Args:
+ position: Position of the waypoint.
+ altitude: Altitude of the sweep in meters.
+ """
+ waypoint = FlightWaypoint(
+ FlightWaypointType.INGRESS_SWEEP,
+ position.x,
+ position.y,
+ altitude
+ )
+ waypoint.name = "SWEEP START"
+ waypoint.description = "Proceed to the target and engage enemy aircraft"
+ waypoint.pretty_name = "Sweep start"
+ return waypoint
+
+ @staticmethod
+ def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
+ """Creates a sweep end waypoint.
+
+ Args:
+ position: Position of the waypoint.
+ altitude: Altitude of the sweep in meters.
+ """
+ waypoint = FlightWaypoint(
+ FlightWaypointType.EGRESS,
+ position.x,
+ position.y,
+ altitude
+ )
+ waypoint.name = "SWEEP END"
+ waypoint.description = "End of sweep"
+ waypoint.pretty_name = "Sweep end"
+ return waypoint
+
+ def sweep(self, start: Point, end: Point,
+ altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
+ """Creates two waypoint for a racetrack orbit.
+
+ Args:
+ start: The beginning of the sweep.
+ end: The end of the sweep.
+ altitude: The sweep altitude.
+ """
+ return (self.sweep_start(start, altitude),
+ self.sweep_end(end, altitude))
+
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package.
@@ -293,8 +397,8 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
- ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
- target)
+ ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
+ target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
diff --git a/gen/forcedoptionsgen.py b/gen/forcedoptionsgen.py
index 8a6684b2..19421942 100644
--- a/gen/forcedoptionsgen.py
+++ b/gen/forcedoptionsgen.py
@@ -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()
-
-
-
\ No newline at end of file
+ self._set_unrestricted_satnav()
diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py
index db1deb03..b0f14df4 100644
--- a/gen/ground_forces/ai_ground_planner.py
+++ b/gen/ground_forces/ai_ground_planner.py
@@ -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,
diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py
index 1989452e..ea18eb46 100644
--- a/gen/groundobjectsgen.py
+++ b/gen/groundobjectsgen.py
@@ -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()
diff --git a/gen/kneeboard.py b/gen/kneeboard.py
index 7a5794ab..61f0af9a 100644
--- a/gen/kneeboard.py
+++ b/gen/kneeboard.py
@@ -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 = []
diff --git a/gen/locations/preset_location_finder.py b/gen/locations/preset_location_finder.py
index 41386d90..4df32466 100644
--- a/gen/locations/preset_location_finder.py
+++ b/gen/locations/preset_location_finder.py
@@ -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:
diff --git a/gen/radios.py b/gen/radios.py
index c2180fe3..87b8661f 100644
--- a/gen/radios.py
+++ b/gen/radios.py
@@ -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.
diff --git a/gen/runways.py b/gen/runways.py
index 5323c37b..ab150720 100644
--- a/gen/runways.py
+++ b/gen/runways.py
@@ -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
diff --git a/gen/sam/aaa_bofors.py b/gen/sam/aaa_bofors.py
index 528edd8b..1d7d18c4 100644
--- a/gen/sam/aaa_bofors.py
+++ b/gen/sam/aaa_bofors.py
@@ -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)
\ No newline at end of file
+ self.position.y + spacing*j, self.heading)
+
+ @classmethod
+ def range(cls) -> AirDefenseRange:
+ return AirDefenseRange.Short
diff --git a/gen/sam/aaa_flak.py b/gen/sam/aaa_flak.py
index 5a0d9121..a6acc45a 100644
--- a/gen/sam/aaa_flak.py
+++ b/gen/sam/aaa_flak.py
@@ -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)
\ No newline at end of file
+ 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
diff --git a/gen/sam/aaa_flak18.py b/gen/sam/aaa_flak18.py
index fea85f70..0716f05a 100644
--- a/gen/sam/aaa_flak18.py
+++ b/gen/sam/aaa_flak18.py
@@ -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
diff --git a/gen/sam/aaa_ww2_ally_flak.py b/gen/sam/aaa_ww2_ally_flak.py
index 7c449dba..6c2fed26 100644
--- a/gen/sam/aaa_ww2_ally_flak.py
+++ b/gen/sam/aaa_ww2_ally_flak.py
@@ -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
diff --git a/gen/sam/aaa_zu23_insurgent.py b/gen/sam/aaa_zu23_insurgent.py
index ec659756..85d65290 100644
--- a/gen/sam/aaa_zu23_insurgent.py
+++ b/gen/sam/aaa_zu23_insurgent.py
@@ -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)
\ No newline at end of file
+ self.position.y + spacing*j, self.heading)
+
+ @classmethod
+ def range(cls) -> AirDefenseRange:
+ return AirDefenseRange.Short
diff --git a/gen/sam/airdefensegroupgenerator.py b/gen/sam/airdefensegroupgenerator.py
new file mode 100644
index 00000000..f58efdf4
--- /dev/null
+++ b/gen/sam/airdefensegroupgenerator.py
@@ -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:
+ ...
diff --git a/gen/sam/cold_war_flak.py b/gen/sam/cold_war_flak.py
index c0b7e81d..ce1b71d9 100644
--- a/gen/sam/cold_war_flak.py
+++ b/gen/sam/cold_war_flak.py
@@ -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
diff --git a/gen/sam/freya_ewr.py b/gen/sam/freya_ewr.py
index 70571e56..f244482b 100644
--- a/gen/sam/freya_ewr.py
+++ b/gen/sam/freya_ewr.py
@@ -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)
\ No newline at end of file
+ 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
diff --git a/gen/sam/genericsam_group_generator.py b/gen/sam/genericsam_group_generator.py
deleted file mode 100644
index 8a35e51b..00000000
--- a/gen/sam/genericsam_group_generator.py
+++ /dev/null
@@ -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)
diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py
index 94738eef..9422e793 100644
--- a/gen/sam/group_generator.py
+++ b/gen/sam/group_generator.py
@@ -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)
diff --git a/gen/sam/sam_avenger.py b/gen/sam/sam_avenger.py
index 44d3aed9..32d1c228 100644
--- a/gen/sam/sam_avenger.py
+++ b/gen/sam/sam_avenger.py
@@ -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
diff --git a/gen/sam/sam_chaparral.py b/gen/sam/sam_chaparral.py
index a8d89181..1e768bf4 100644
--- a/gen/sam/sam_chaparral.py
+++ b/gen/sam/sam_chaparral.py
@@ -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
diff --git a/gen/sam/sam_gepard.py b/gen/sam/sam_gepard.py
index 501ed7b7..7e8ef223 100644
--- a/gen/sam/sam_gepard.py
+++ b/gen/sam/sam_gepard.py
@@ -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
diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py
index 1ff77cde..366c63e7 100644
--- a/gen/sam/sam_group_generator.py
+++ b/gen/sam/sam_group_generator.py
@@ -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)
diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py
index da8c700a..382c4b69 100644
--- a/gen/sam/sam_hawk.py
+++ b/gen/sam/sam_hawk.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py
index adba14b5..76951e9a 100644
--- a/gen/sam/sam_hq7.py
+++ b/gen/sam/sam_hq7.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_linebacker.py b/gen/sam/sam_linebacker.py
index 946d14ed..e2dae5a1 100644
--- a/gen/sam/sam_linebacker.py
+++ b/gen/sam/sam_linebacker.py
@@ -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
diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py
index 490e6f2f..14108083 100644
--- a/gen/sam/sam_patriot.py
+++ b/gen/sam/sam_patriot.py
@@ -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])
\ No newline at end of file
+ self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
+
+ @classmethod
+ def range(cls) -> AirDefenseRange:
+ return AirDefenseRange.Long
diff --git a/gen/sam/sam_rapier.py b/gen/sam/sam_rapier.py
index 981a098e..5b4dbaa9 100644
--- a/gen/sam/sam_rapier.py
+++ b/gen/sam/sam_rapier.py
@@ -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])
\ No newline at end of file
+ self.add_unit(AirDefence.Rapier_FSA_Launcher, "LN#" + str(i), position[0], position[1], position[2])
+
+ @classmethod
+ def range(cls) -> AirDefenseRange:
+ return AirDefenseRange.Short
diff --git a/gen/sam/sam_roland.py b/gen/sam/sam_roland.py
index 1f970517..3c2685c7 100644
--- a/gen/sam/sam_roland.py
+++ b/gen/sam/sam_roland.py
@@ -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
diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py
index d3804a86..371bdb5d 100644
--- a/gen/sam/sam_sa10.py
+++ b/gen/sam/sam_sa10.py
@@ -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])
diff --git a/gen/sam/sam_sa11.py b/gen/sam/sam_sa11.py
index e7634b92..2fd5a08f 100644
--- a/gen/sam/sam_sa11.py
+++ b/gen/sam/sam_sa11.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_sa13.py b/gen/sam/sam_sa13.py
index 8fc069ad..ec7b3693 100644
--- a/gen/sam/sam_sa13.py
+++ b/gen/sam/sam_sa13.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_sa15.py b/gen/sam/sam_sa15.py
index 09fda2ee..30eaabfe 100644
--- a/gen/sam/sam_sa15.py
+++ b/gen/sam/sam_sa15.py
@@ -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)
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/gen/sam/sam_sa19.py b/gen/sam/sam_sa19.py
index c4f710f4..298ae91c 100644
--- a/gen/sam/sam_sa19.py
+++ b/gen/sam/sam_sa19.py
@@ -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
diff --git a/gen/sam/sam_sa2.py b/gen/sam/sam_sa2.py
index ff77265f..c95da151 100644
--- a/gen/sam/sam_sa2.py
+++ b/gen/sam/sam_sa2.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_sa3.py b/gen/sam/sam_sa3.py
index e57f184c..8ab5cad3 100644
--- a/gen/sam/sam_sa3.py
+++ b/gen/sam/sam_sa3.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_sa6.py b/gen/sam/sam_sa6.py
index 1028ed76..fab5f01b 100644
--- a/gen/sam/sam_sa6.py
+++ b/gen/sam/sam_sa6.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_sa8.py b/gen/sam/sam_sa8.py
index 1c09dd2e..2dd104ee 100644
--- a/gen/sam/sam_sa8.py
+++ b/gen/sam/sam_sa8.py
@@ -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
diff --git a/gen/sam/sam_sa9.py b/gen/sam/sam_sa9.py
index d0045bea..f1cfaff7 100644
--- a/gen/sam/sam_sa9.py
+++ b/gen/sam/sam_sa9.py
@@ -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])
\ No newline at end of file
+ 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
diff --git a/gen/sam/sam_vulcan.py b/gen/sam/sam_vulcan.py
index 77cfc0a2..5b67d878 100644
--- a/gen/sam/sam_vulcan.py
+++ b/gen/sam/sam_vulcan.py
@@ -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
+
diff --git a/gen/sam/sam_zsu23.py b/gen/sam/sam_zsu23.py
index 7c90cb4d..c25a9295 100644
--- a/gen/sam/sam_zsu23.py
+++ b/gen/sam/sam_zsu23.py
@@ -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
diff --git a/gen/sam/sam_zu23.py b/gen/sam/sam_zu23.py
index 3134c3a7..494c436d 100644
--- a/gen/sam/sam_zu23.py
+++ b/gen/sam/sam_zu23.py
@@ -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)
\ No newline at end of file
+ self.position.y + spacing*j, self.heading)
+
+ @classmethod
+ def range(cls) -> AirDefenseRange:
+ return AirDefenseRange.Short
diff --git a/gen/sam/sam_zu23_ural.py b/gen/sam/sam_zu23_ural.py
index 1eb31b22..2f26436b 100644
--- a/gen/sam/sam_zu23_ural.py
+++ b/gen/sam/sam_zu23_ural.py
@@ -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
diff --git a/gen/sam/sam_zu23_ural_insurgent.py b/gen/sam/sam_zu23_ural_insurgent.py
index 4512cfc7..d8c26995 100644
--- a/gen/sam/sam_zu23_ural_insurgent.py
+++ b/gen/sam/sam_zu23_ural_insurgent.py
@@ -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
+
diff --git a/gen/triggergen.py b/gen/triggergen.py
index ba87bb3e..a0ccd641 100644
--- a/gen/triggergen.py
+++ b/gen/triggergen.py
@@ -1,12 +1,38 @@
-from dcs.action import MarkToAll
-from dcs.condition import TimeAfter
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from dcs.action import (
+ MarkToAll,
+ SetFlag,
+ DoScript,
+ ClearFlag
+)
+from dcs.condition import (
+ TimeAfter,
+ AllOfCoalitionOutsideZone,
+ PartOfCoalitionInZone,
+ FlagIsFalse,
+ FlagIsTrue
+)
+from dcs.unitgroup import FlyingGroup
from dcs.mission import Mission
from dcs.task import Option
from dcs.translation import String
-from dcs.triggers import Event, TriggerOnce
+from dcs.triggers import (
+ Event,
+ TriggerOnce,
+ TriggerZone,
+ TriggerCondition,
+)
from dcs.unit import Skill
-from .conflictgen import Conflict
+from game.theater import Airfield
+from game.theater.controlpoint import Fob
+
+
+if TYPE_CHECKING:
+ from game.game import Game
PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25
@@ -30,9 +56,11 @@ class Silence(Option):
class TriggersGenerator:
- def __init__(self, mission: Mission, conflict: Conflict, game):
+ capture_zone_types = (Fob, )
+ capture_zone_flag = 600
+
+ def __init__(self, mission: Mission, game: Game):
self.mission = mission
- self.conflict = conflict
self.game = game
def _set_allegiances(self, player_coalition: str, enemy_coalition: str):
@@ -56,9 +84,8 @@ class TriggersGenerator:
airport.operating_level_fuel = 0
for cp in self.game.theater.controlpoints:
- if cp.is_global:
- continue
- self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition)
+ if isinstance(cp, Airfield):
+ self.mission.terrain.airport_by_id(cp.at.id).set_coalition(cp.captured and player_coalition or enemy_coalition)
def _set_skill(self, player_coalition: str, enemy_coalition: str):
"""
@@ -73,8 +100,9 @@ class TriggersGenerator:
continue
for country in coalition.countries.values():
- for plane_group in country.plane_group:
- for plane_unit in plane_group.units:
+ flying_groups = country.plane_group + country.helicopter_group # type: FlyingGroup
+ for flying_group in flying_groups:
+ for plane_unit in flying_group.units:
if plane_unit.skill != Skill.Client and plane_unit.skill != Skill.Player:
plane_unit.skill = Skill(skill_level[0])
@@ -103,16 +131,71 @@ class TriggersGenerator:
added.append(ground_object.obj_name)
self.mission.triggerrules.triggers.append(mark_trigger)
+ def _generate_capture_triggers(self, player_coalition: str, enemy_coalition: str) -> None:
+ """Creates a pair of triggers for each control point of `cls.capture_zone_types`.
+ One for the initial capture of a control point, and one if it is recaptured.
+ Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
+ """
+ for cp in self.game.theater.controlpoints:
+ if isinstance(cp, self.capture_zone_types):
+ if cp.captured:
+ attacking_coalition = enemy_coalition
+ attack_coalition_int = 1 # 1 is the Event int for Red
+ defending_coalition = player_coalition
+ defend_coalition_int = 2 # 2 is the Event int for Blue
+ else:
+ attacking_coalition = player_coalition
+ attack_coalition_int = 2
+ defending_coalition = enemy_coalition
+ defend_coalition_int = 1
+
+ trigger_zone = self.mission.triggers.add_triggerzone(cp.position, radius=3000, hidden=False, name="CAPTURE")
+ flag = self.get_capture_zone_flag()
+ capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
+ capture_trigger.add_condition(AllOfCoalitionOutsideZone(defending_coalition, trigger_zone.id))
+ capture_trigger.add_condition(PartOfCoalitionInZone(attacking_coalition, trigger_zone.id, unit_type="GROUND"))
+ capture_trigger.add_condition(FlagIsFalse(flag=flag))
+ script_string = String(
+ f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
+ )
+ capture_trigger.add_action(DoScript(
+ script_string
+ )
+ )
+ capture_trigger.add_action(SetFlag(flag=flag))
+ self.mission.triggerrules.triggers.append(capture_trigger)
+
+ recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
+ recapture_trigger.add_condition(AllOfCoalitionOutsideZone(attacking_coalition, trigger_zone.id))
+ recapture_trigger.add_condition(PartOfCoalitionInZone(defending_coalition, trigger_zone.id, unit_type="GROUND"))
+ recapture_trigger.add_condition(FlagIsTrue(flag=flag))
+ script_string = String(
+ f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
+ )
+ recapture_trigger.add_action(DoScript(
+ script_string
+ )
+ )
+ recapture_trigger.add_action(ClearFlag(flag=flag))
+ self.mission.triggerrules.triggers.append(recapture_trigger)
+
def generate(self):
player_coalition = "blue"
enemy_coalition = "red"
- self.mission.coalition["blue"].bullseye = {"x": self.conflict.position.x,
- "y": self.conflict.position.y}
- self.mission.coalition["red"].bullseye = {"x": self.conflict.position.x,
- "y": self.conflict.position.y}
+ player_cp, enemy_cp = self.game.theater.closest_opposing_control_points()
+ self.mission.coalition["blue"].bullseye = {"x": enemy_cp.position.x,
+ "y": enemy_cp.position.y}
+ self.mission.coalition["red"].bullseye = {"x": player_cp.position.x,
+ "y": player_cp.position.y}
self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers()
+ self._generate_capture_triggers(player_coalition, enemy_coalition)
+ @classmethod
+ def get_capture_zone_flag(cls):
+ flag = cls.capture_zone_flag
+ cls.capture_zone_flag += 1
+ return flag
diff --git a/gen/visualgen.py b/gen/visualgen.py
index efd0c1f9..e03da467 100644
--- a/gen/visualgen.py
+++ b/gen/visualgen.py
@@ -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:
diff --git a/mypy.ini b/mypy.ini
index 045a50e6..e397c985 100644
--- a/mypy.ini
+++ b/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
\ No newline at end of file
diff --git a/pydcs b/pydcs
index 2883be31..f924289c 160000
--- a/pydcs
+++ b/pydcs
@@ -1 +1 @@
-Subproject commit 2883be31c2eb80834b93efd8d20ca17913986e9b
+Subproject commit f924289c9cbe6e21a01906bdf11c1933110a32de
diff --git a/pydcs_extensions/f22a/f22a.py b/pydcs_extensions/f22a/f22a.py
new file mode 100644
index 00000000..f4230f91
--- /dev/null
+++ b/pydcs_extensions/f22a/f22a.py
@@ -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
diff --git a/pydcs_extensions/hercules/hercules.py b/pydcs_extensions/hercules/hercules.py
new file mode 100644
index 00000000..2a8023c7
--- /dev/null
+++ b/pydcs_extensions/hercules/hercules.py
@@ -0,0 +1,739 @@
+from enum import Enum
+
+from dcs import task
+from dcs.planes import PlaneType
+from dcs.weapons_data import Weapons
+
+
+class HerculesWeapons:
+ GAU_23A_Chain_Gun__30mm_ = {"clsid": "{Herc_GAU_23A_Chain_Gun}", "name": "GAU 23A Chain Gun (30mm)", "weight": 595.9426}
+ Herc_AAA_GEPARD = {"clsid": "Herc_AAA_GEPARD", "name": "AAA GEPARD [34720lb]", "weight": 15782}
+ Herc_AAA_Vulcan_M163 = {"clsid": "Herc_AAA_Vulcan_M163", "name": "AAA Vulcan M163 [21666lb]", "weight": 9848}
+ Herc_Ammo_AGM_154C_missiles = {"clsid": "Herc_Ammo_AGM_154C_missiles", "name": "Ammo AGM-154C*10 [10648lb]", "weight": 4960}
+ Herc_Ammo_AGM_65D_missiles = {"clsid": "Herc_Ammo_AGM_65D_missiles", "name": "Ammo AGM-65D*10 [4800lb]", "weight": 2300}
+ Herc_Ammo_AGM_65E_missiles = {"clsid": "Herc_Ammo_AGM_65E_missiles", "name": "Ammo AGM-65E*10 [6292lb]", "weight": 2980}
+ Herc_Ammo_AGM_65G_missiles = {"clsid": "Herc_Ammo_AGM_65G_missiles", "name": "Ammo AGM-65G*10 [6622lb]", "weight": 3130}
+ Herc_Ammo_AGM_65H_missiles = {"clsid": "Herc_Ammo_AGM_65H_missiles", "name": "Ammo AGM-65H*10 [4570lb]", "weight": 2200}
+ Herc_Ammo_AGM_65K_missiles = {"clsid": "Herc_Ammo_AGM_65K_missiles", "name": "Ammo AGM-65K*10 [7920lb]", "weight": 3720}
+ Herc_Ammo_AGM_84A_missiles = {"clsid": "Herc_Ammo_AGM_84A_missiles", "name": "Ammo AGM-84A*8 [11651lb]", "weight": 5408}
+ Herc_Ammo_AGM_84E_missiles = {"clsid": "Herc_Ammo_AGM_84E_missiles", "name": "Ammo AGM-84E*8 [11651lb]", "weight": 5408}
+ Herc_Ammo_AGM_88C_missiles = {"clsid": "Herc_Ammo_AGM_88C_missiles", "name": "Ammo AGM-88C*10 [7920lb]", "weight": 3730}
+ Herc_Ammo_AIM120B_missiles = {"clsid": "Herc_Ammo_AIM120B_missiles", "name": "Ammo AIM-120B*24 [11193lb]", "weight": 5208}
+ Herc_Ammo_AIM120C_missiles = {"clsid": "Herc_Ammo_AIM120C_missiles", "name": "Ammo AIM-120C*24 [10665lb]", "weight": 5208}
+ Herc_Ammo_AIM54C_missiles = {"clsid": "Herc_Ammo_AIM54C_missiles", "name": "Ammo AIM-54C*18 [18335lb]", "weight": 8454}
+ Herc_Ammo_AIM7M_missiles = {"clsid": "Herc_Ammo_AIM7M_missiles", "name": "Ammo AIM-7M*24 [14995lb]", "weight": 6936}
+ Herc_Ammo_AIM9M_missiles = {"clsid": "Herc_Ammo_AIM9M_missiles", "name": "Ammo AIM-9M*30 [7128lb]", "weight": 4860}
+ Herc_Ammo_AIM9P5_missiles = {"clsid": "Herc_Ammo_AIM9P5_missiles", "name": "Ammo AIM-9P5*30 [5676lb]", "weight": 2700}
+ Herc_Ammo_AIM9X_missiles = {"clsid": "Herc_Ammo_AIM9X_missiles", "name": "Ammo AIM-9X*30 [5676lb]", "weight": 2700}
+ Herc_Ammo_BETAB500SP_bombs = {"clsid": "Herc_Ammo_BETAB500SP_bombs", "name": "Ammo BetAB-500ShP*10 [9328lb]", "weight": 4360}
+ Herc_Ammo_BETAB500_bombs = {"clsid": "Herc_Ammo_BETAB500_bombs", "name": "Ammo BetAB-500*10 [9460lb]", "weight": 4420}
+ Herc_Ammo_CBU_103_bombs = {"clsid": "Herc_Ammo_CBU_103_bombs", "name": "Ammo CBU-103*10 [10142lb]", "weight": 4730}
+ Herc_Ammo_CBU_105_bombs = {"clsid": "Herc_Ammo_CBU_105_bombs", "name": "Ammo CBU-105*10 [11022lb]", "weight": 5130}
+ Herc_Ammo_CBU_87_bombs = {"clsid": "Herc_Ammo_CBU_87_bombs", "name": "Ammo CBU-87*10 [9460lb]", "weight": 4420}
+ Herc_Ammo_CBU_97_bombs = {"clsid": "Herc_Ammo_CBU_97_bombs", "name": "Ammo CBU-97*10 [10362lb]", "weight": 4830}
+ Herc_Ammo_FAB100_bombs = {"clsid": "Herc_Ammo_FAB100_bombs", "name": "Ammo FAB-100*20 [4400lb", "weight": 2120}
+ Herc_Ammo_FAB250_bombs = {"clsid": "Herc_Ammo_FAB250_bombs", "name": "Ammo FAB-250*20 [11000lb]", "weight": 5120}
+ Herc_Ammo_FAB500_bombs = {"clsid": "Herc_Ammo_FAB500_bombs", "name": "Ammo FAB-500*10 [11000lb]", "weight": 5120}
+ Herc_Ammo_GBU_10_bombs = {"clsid": "Herc_Ammo_GBU_10_bombs", "name": "Ammo GBU-10*6 [15340lb]", "weight": 7092}
+ Herc_Ammo_GBU_12_bombs = {"clsid": "Herc_Ammo_GBU_12_bombs", "name": "Ammo GBU-12*16 [9680lb]", "weight": 4520}
+ Herc_Ammo_GBU_16_bombs = {"clsid": "Herc_Ammo_GBU_16_bombs", "name": "Ammo GBU-16*10 [12408lb]", "weight": 5760}
+ Herc_Ammo_GBU_31_V3B_bombs = {"clsid": "Herc_Ammo_GBU_31_V3B_bombs", "name": "Ammo GBU-31V3B*6 [12949lb]", "weight": 6006}
+ Herc_Ammo_GBU_31_VB_bombs = {"clsid": "Herc_Ammo_GBU_31_VB_bombs", "name": "Ammo GBU-31V/B*6 [12328lb]", "weight": 5724}
+ Herc_Ammo_GBU_38_bombs = {"clsid": "Herc_Ammo_GBU_38_bombs", "name": "Ammo GBU-38*10 [6028lb]", "weight": 2860}
+ Herc_Ammo_hydra_HE_rockets = {"clsid": "Herc_Ammo_hydra_HE_rockets", "name": "Ammo M151 Hydra HE*80 [4752lb]", "weight": 2280}
+ Herc_Ammo_hydra_WP_rockets = {"clsid": "Herc_Ammo_hydra_WP_rockets", "name": "Ammo M156 Hydra WP*80 [4752lb]", "weight": 2280}
+ Herc_Ammo_KAB500KR_bombs = {"clsid": "Herc_Ammo_KAB500KR_bombs", "name": "Ammo KAB-500kr*10 [12320lb]", "weight": 5720}
+ Herc_Ammo_KH25ML_missiles = {"clsid": "Herc_Ammo_KH25ML_missiles", "name": "Ammo Kh-25ML*10 [7920lb]", "weight": 3720}
+ Herc_Ammo_KH25MPU_missiles = {"clsid": "Herc_Ammo_KH25MPU_missiles", "name": "Ammo Kh-25MPU*10 [8140lb]", "weight": 3820}
+ Herc_Ammo_KH29L_missiles = {"clsid": "Herc_Ammo_KH29L_missiles", "name": "Ammo Kh-29L*10 [16434lb]", "weight": 7590}
+ Herc_Ammo_KH29T_missiles = {"clsid": "Herc_Ammo_KH29T_missiles", "name": "Ammo Kh-29T*10 [16720lb]", "weight": 7720}
+ Herc_Ammo_KH58U_missiles = {"clsid": "Herc_Ammo_KH58U_missiles", "name": "Ammo Kh-58U*10 [16060lb]", "weight": 7420}
+ Herc_Ammo_KMGU296AO25KO_bombs = {"clsid": "Herc_Ammo_KMGU296AO25KO_bombs", "name": "Ammo KMGU-2 - 96 PTAB-2.5KO*10 [11440lb]", "weight": 5320}
+ Herc_Ammo_KMGU296AO25RT_bombs = {"clsid": "Herc_Ammo_KMGU296AO25RT_bombs", "name": "Ammo KMGU-2 - 96 AO-2.5RT*10 [11440lb]", "weight": 5320}
+ Herc_Ammo_M117_bombs = {"clsid": "Herc_Ammo_M117_bombs", "name": "Ammo M117*16 [11968lb]", "weight": 5560}
+ Herc_Ammo_MAGIC2_missiles = {"clsid": "Herc_Ammo_MAGIC2_missiles", "name": "Ammo Magic2*30 [5676lb]", "weight": 2700}
+ Herc_Ammo_MK20_bombs = {"clsid": "Herc_Ammo_MK20_bombs", "name": "Ammo MK20*20 [9768lb]", "weight": 4560}
+ Herc_Ammo_Mk_82AIR_bombs = {"clsid": "Herc_Ammo_Mk_82AIR_bombs", "name": "Ammo Mk-82AIR*20 [11044lb]", "weight": 4940}
+ Herc_Ammo_Mk_82Snake_bombs = {"clsid": "Herc_Ammo_Mk_82Snake_bombs", "name": "Ammo Mk-82Snakeye*20 [11880lb]", "weight": 4940}
+ Herc_Ammo_Mk_82_bombs = {"clsid": "Herc_Ammo_Mk_82_bombs", "name": "Ammo Mk-82*20 [10560lb]", "weight": 4940}
+ Herc_Ammo_Mk_83_bombs = {"clsid": "Herc_Ammo_Mk_83_bombs", "name": "Ammo Mk-83*10 [9834lb]", "weight": 4590}
+ Herc_Ammo_Mk_84_bombs = {"clsid": "Herc_Ammo_Mk_84_bombs", "name": "Ammo Mk-84*8 [15735b]", "weight": 7272}
+ Herc_Ammo_R27ER_missiles = {"clsid": "Herc_Ammo_R27ER_missiles", "name": "Ammo R-27ER*24 [18480lb]", "weight": 8520}
+ Herc_Ammo_R27ET_missiles = {"clsid": "Herc_Ammo_R27ET_missiles", "name": "Ammo R-27ET*24 [18480lb", "weight": 8496}
+ Herc_Ammo_R27R_missiles = {"clsid": "Herc_Ammo_R27R_missiles", "name": "Ammo R-27R*24 [13359lb]", "weight": 6192}
+ Herc_Ammo_R27T_missiles = {"clsid": "Herc_Ammo_R27T_missiles", "name": "Ammo R-27T*24 [13359lb]", "weight": 6192}
+ Herc_Ammo_R60M_missiles = {"clsid": "Herc_Ammo_R60M_missiles", "name": "Ammo R-60M*30 [2904lb]", "weight": 1440}
+ Herc_Ammo_R77_missiles = {"clsid": "Herc_Ammo_R77_missiles", "name": "Ammo R-77*24 [9240lb]", "weight": 4320}
+ Herc_Ammo_RBK250PTAB25M_bombs = {"clsid": "Herc_Ammo_RBK250PTAB25M_bombs", "name": "Ammo RBK-250 PTAB-2.5M*20 [12012lb]", "weight": 5580}
+ Herc_Ammo_RBK500255PTAB105_bombs = {"clsid": "Herc_Ammo_RBK500255PTAB105_bombs", "name": "Ammo RBK-500-255 PTAB-10-5*10 [9394lb]", "weight": 4390}
+ Herc_Ammo_RBK500PTAB1M_bombs = {"clsid": "Herc_Ammo_RBK500PTAB1M_bombs", "name": "Ammo RBK-500 PTAB-1M*10 [9394lb]", "weight": 4390}
+ Herc_Ammo_S24B_missiles = {"clsid": "Herc_Ammo_S24B_missiles", "name": "Ammo S-24B*20 [10340lb]", "weight": 4820}
+ Herc_Ammo_S25L_missiles = {"clsid": "Herc_Ammo_S25L_missiles", "name": "Ammo S-25L*10 [11000b]", "weight": 5120}
+ Herc_Ammo_S25OFM_missiles = {"clsid": "Herc_Ammo_S25OFM_missiles", "name": "Ammo S-25OFM*10 [10890lb]", "weight": 5070}
+ Herc_Ammo_S530D_missiles = {"clsid": "Herc_Ammo_S530D_missiles", "name": "Ammo Super 530D*24 [6480lb]", "weight": 6600}
+ Herc_Ammo_SAB100_bombs = {"clsid": "Herc_Ammo_SAB100_bombs", "name": "Ammo SAB-100*20 [11000lb]", "weight": 2120}
+ Herc_Ammo_Vikhr_missiles = {"clsid": "Herc_Ammo_Vikhr_missiles", "name": "Ammo Vikhr*48 [5808lb]", "weight": 2760}
+ Herc_APC_BTR_80 = {"clsid": "Herc_APC_BTR_80", "name": "APC BTR-80 [23936lb]", "weight": 10880}
+ Herc_APC_COBRA = {"clsid": "Herc_APC_COBRA", "name": "APC Cobra [10912lb]", "weight": 4960}
+ Herc_APC_LAV_25 = {"clsid": "Herc_APC_LAV_25", "name": "APC LAV-25 [22514lb]", "weight": 10234}
+ Herc_APC_M1025_HMMWV = {"clsid": "Herc_APC_M1025_HMMWV", "name": "M1025 HMMWV [6160lb]", "weight": 2800}
+ Herc_APC_M1043_HMMWV_Armament = {"clsid": "Herc_APC_M1043_HMMWV_Armament", "name": "APC M1043 HMMWV Armament [7023lb]", "weight": 3192}
+ Herc_APC_M113 = {"clsid": "Herc_APC_M113", "name": "APC M113 [21624lb]", "weight": 9830}
+ Herc_APC_MTLB = {"clsid": "Herc_APC_MTLB", "name": "APC MTLB [26000lb]", "weight": 12000}
+ Herc_ART_GVOZDIKA = {"clsid": "Herc_ART_GVOZDIKA", "name": "ART GVOZDIKA [34720lb]", "weight": 15782}
+ Herc_ART_NONA = {"clsid": "Herc_ART_NONA", "name": "ART 2S9 NONA [19140lb]", "weight": 8700}
+ Herc_ARV_BRDM_2 = {"clsid": "Herc_ARV_BRDM_2", "name": "ARV BRDM-2 [12320lb]", "weight": 5600}
+ Herc_ATGM_M1045_HMMWV_TOW = {"clsid": "Herc_ATGM_M1045_HMMWV_TOW", "name": "ATGM M1045 HMMWV TOW [7183lb]", "weight": 3265}
+ Herc_ATGM_M1134_Stryker = {"clsid": "Herc_ATGM_M1134_Stryker", "name": "ATGM M1134 Stryker [30337lb]", "weight": 13790}
+ Herc_BattleStation = {"clsid": "Herc_BattleStation", "name": "Battle Station", "weight": 0}
+ Herc_Ext_Fuel_Tank = {"clsid": "Herc_Ext_Fuel_Tank", "name": "External Fuel Tank", "weight": 4131}
+ Herc_GEN_CRATE = {"clsid": "Herc_GEN_CRATE", "name": "Generic Crate [20000lb]", "weight": 9071}
+ Herc_HEMTT_TFFT = {"clsid": "Herc_HEMTT_TFFT", "name": "HEMTT TFFT [34400lb]", "weight": 15634}
+ Herc_IFV_BMD1 = {"clsid": "Herc_IFV_BMD1", "name": "IFV BMD-1 [18040lb]", "weight": 8200}
+ Herc_IFV_BMP_1 = {"clsid": "Herc_IFV_BMP_1", "name": "IFV BMP-1 [23232lb]", "weight": 10560}
+ Herc_IFV_BMP_2 = {"clsid": "Herc_IFV_BMP_2", "name": "IFV BMP-2 [25168lb]", "weight": 11440}
+ Herc_IFV_BMP_3 = {"clsid": "Herc_IFV_BMP_3", "name": "IFV BMP-3 [32912lb]", "weight": 14960}
+ Herc_IFV_BTRD = {"clsid": "Herc_IFV_BTRD", "name": "IFV BTR-D [18040lb]", "weight": 8200}
+ Herc_IFV_M2A2_Bradley = {"clsid": "Herc_IFV_M2A2_Bradley", "name": "IFV M2A2 Bradley [34720lb]", "weight": 15782}
+ Herc_IFV_MARDER = {"clsid": "Herc_IFV_MARDER", "name": "IFV MARDER [34720lb]", "weight": 15782}
+ Herc_IFV_MCV80_Warrior = {"clsid": "Herc_IFV_MCV80_Warrior", "name": "IFV MCV-80 [34720lb]", "weight": 15782}
+ Herc_IFV_TPZ = {"clsid": "Herc_IFV_TPZ", "name": "IFV TPZ FUCH [33440lb]", "weight": 15200}
+ Herc_JATO = {"clsid": "Herc_JATO", "name": "JATO", "weight": 0}
+ Herc_M_818 = {"clsid": "Herc_M_818", "name": "Transport M818 [16000lb]", "weight": 7272}
+ Herc_SAM_13 = {"clsid": "Herc_SAM_13", "name": "SAM SA-13 STRELA [21624lb]", "weight": 9830}
+ Herc_SAM_19 = {"clsid": "Herc_SAM_19", "name": "SAM SA-19 Tunguska 2S6 [34720lb]", "weight": 15782}
+ Herc_SAM_CHAPARRAL = {"clsid": "Herc_SAM_CHAPARRAL", "name": "SAM CHAPARRAL [21624lb]", "weight": 9830}
+ Herc_SAM_LINEBACKER = {"clsid": "Herc_SAM_LINEBACKER", "name": "SAM LINEBACKER [34720lb]", "weight": 15782}
+ Herc_SAM_M1097_HMMWV = {"clsid": "Herc_SAM_M1097_HMMWV", "name": "SAM Avenger M1097 [7200lb]", "weight": 3273}
+ Herc_SAM_ROLAND_ADS = {"clsid": "Herc_SAM_ROLAND_ADS", "name": "SAM ROLAND ADS [34720lb]", "weight": 15782}
+ Herc_SAM_ROLAND_LN = {"clsid": "Herc_SAM_ROLAND_LN", "name": "SAM ROLAND LN [34720b]", "weight": 15782}
+ Herc_Soldier_Squad = {"clsid": "Herc_Soldier_Squad", "name": "Squad 30 x Soldier [7950lb]", "weight": 120}
+ Herc_SPG_M1126_Stryker_ICV = {"clsid": "Herc_SPG_M1126_Stryker_ICV", "name": "APC M1126 Stryker ICV [29542lb]", "weight": 13429}
+ Herc_SPG_M1128_Stryker_MGS = {"clsid": "Herc_SPG_M1128_Stryker_MGS", "name": "SPG M1128 Stryker MGS [33036lb]", "weight": 15016}
+ Herc_Tanker_HEMTT = {"clsid": "Herc_Tanker_HEMTT", "name": "Tanker M978 HEMTT [34000lb]", "weight": 15455}
+ Herc_TIGR_233036 = {"clsid": "Herc_TIGR_233036", "name": "Transport Tigr [15900lb]", "weight": 7200}
+ Herc_UAZ_469 = {"clsid": "Herc_UAZ_469", "name": "Transport UAZ-469 [3747lb]", "weight": 1700}
+ Herc_URAL_375 = {"clsid": "Herc_URAL_375", "name": "Transport URAL-375 [14815lb]", "weight": 6734}
+ Herc_ZSU_23_4 = {"clsid": "Herc_ZSU_23_4", "name": "AAA ZSU-23-4 Shilka [32912lb]", "weight": 14960}
+ M61_Vulcan_Rotary_Cannon__20mm_ = {"clsid": "{Herc_M61_Vulcan_Rotary_Cannon}", "name": "M61 Vulcan Rotary Cannon (20mm)", "weight": 595.9426}
+ _105mm_Howitzer = {"clsid": "{Herc_105mm_Howitzer}", "name": "105mm Howitzer", "weight": 595.9426}
+
+
+class Hercules(PlaneType):
+ id = "Hercules"
+ flyable = True
+ height = 11.84
+ width = 40.41
+ length = 34.36
+ fuel_max = 19759
+ max_speed = 669.6
+ chaff = 840
+ flare = 840
+ charge_total = 1680
+ chaff_charge_size = 1
+ flare_charge_size = 1
+ category = "Air" #{C168A850-3C0B-436a-95B5-C4A015552560}
+ radio_frequency = 305
+
+ panel_radio = {
+ 1: {
+ "channels": {
+ 1: 305,
+ 2: 264,
+ 4: 256,
+ 8: 257,
+ 16: 261,
+ 17: 261,
+ 9: 255,
+ 18: 251,
+ 5: 254,
+ 10: 262,
+ 20: 266,
+ 11: 259,
+ 3: 265,
+ 6: 250,
+ 12: 268,
+ 13: 269,
+ 7: 270,
+ 14: 260,
+ 19: 253,
+ 15: 263
+ },
+ },
+ }
+
+ class Liveries:
+
+ class USSR(Enum):
+ default = "default"
+
+ class Georgia(Enum):
+ default = "default"
+
+ class Venezuela(Enum):
+ default = "default"
+
+ class Australia(Enum):
+ default = "default"
+
+ class Israel(Enum):
+ default = "default"
+
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ default = "default"
+
+ class Sudan(Enum):
+ default = "default"
+
+ class Norway(Enum):
+ default = "default"
+
+ class Romania(Enum):
+ default = "default"
+
+ class Iran(Enum):
+ default = "default"
+
+ class Ukraine(Enum):
+ default = "default"
+
+ class Libya(Enum):
+ default = "default"
+
+ class Belgium(Enum):
+ default = "default"
+
+ class Slovakia(Enum):
+ default = "default"
+
+ class Greece(Enum):
+ default = "default"
+
+ class UK(Enum):
+ default = "default"
+
+ class Third_Reich(Enum):
+ default = "default"
+
+ class Hungary(Enum):
+ default = "default"
+
+ class Abkhazia(Enum):
+ default = "default"
+
+ class Morocco(Enum):
+ default = "default"
+
+ class United_Nations_Peacekeepers(Enum):
+ default = "default"
+
+ class Switzerland(Enum):
+ default = "default"
+
+ class SouthOssetia(Enum):
+ default = "default"
+
+ class Vietnam(Enum):
+ default = "default"
+
+ class China(Enum):
+ default = "default"
+
+ class Yemen(Enum):
+ default = "default"
+
+ class Kuwait(Enum):
+ default = "default"
+
+ class Serbia(Enum):
+ default = "default"
+
+ class Oman(Enum):
+ default = "default"
+
+ class India(Enum):
+ default = "default"
+
+ class Egypt(Enum):
+ default = "default"
+
+ class TheNetherlands(Enum):
+ default = "default"
+
+ class Poland(Enum):
+ default = "default"
+
+ class Syria(Enum):
+ default = "default"
+
+ class Finland(Enum):
+ default = "default"
+
+ class Kazakhstan(Enum):
+ default = "default"
+
+ class Denmark(Enum):
+ default = "default"
+
+ class Sweden(Enum):
+ default = "default"
+
+ class Croatia(Enum):
+ default = "default"
+
+ class CzechRepublic(Enum):
+ default = "default"
+
+ class GDR(Enum):
+ default = "default"
+
+ class Yugoslavia(Enum):
+ default = "default"
+
+ class Bulgaria(Enum):
+ default = "default"
+
+ class SouthKorea(Enum):
+ default = "default"
+
+ class Tunisia(Enum):
+ default = "default"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ default = "default"
+
+ class Lebanon(Enum):
+ default = "default"
+
+ class Portugal(Enum):
+ default = "default"
+
+ class Cuba(Enum):
+ default = "default"
+
+ class Insurgents(Enum):
+ default = "default"
+
+ class SaudiArabia(Enum):
+ default = "default"
+
+ class France(Enum):
+ default = "default"
+
+ class USA(Enum):
+ default = "default"
+
+ class Honduras(Enum):
+ default = "default"
+
+ class Qatar(Enum):
+ default = "default"
+
+ class Russia(Enum):
+ default = "default"
+
+ class United_Arab_Emirates(Enum):
+ default = "default"
+
+ class Italian_Social_Republi(Enum):
+ default = "default"
+
+ class Austria(Enum):
+ default = "default"
+
+ class Bahrain(Enum):
+ default = "default"
+
+ class Italy(Enum):
+ default = "default"
+
+ class Chile(Enum):
+ default = "default"
+
+ class Turkey(Enum):
+ default = "default"
+
+ class Philippines(Enum):
+ default = "default"
+
+ class Algeria(Enum):
+ default = "default"
+
+ class Pakistan(Enum):
+ default = "default"
+
+ class Malaysia(Enum):
+ default = "default"
+
+ class Indonesia(Enum):
+ default = "default"
+
+ class Iraq(Enum):
+ default = "default"
+
+ class Germany(Enum):
+ default = "default"
+
+ class South_Africa(Enum):
+ default = "default"
+
+ class Jordan(Enum):
+ default = "default"
+
+ class Mexico(Enum):
+ default = "default"
+
+ class USAFAggressors(Enum):
+ default = "default"
+
+ class Brazil(Enum):
+ default = "default"
+
+ class Spain(Enum):
+ default = "default"
+
+ class Belarus(Enum):
+ default = "default"
+
+ class Canada(Enum):
+ default = "default"
+
+ class NorthKorea(Enum):
+ default = "default"
+
+ class Ethiopia(Enum):
+ default = "default"
+
+ class Japan(Enum):
+ default = "default"
+
+ class Thailand(Enum):
+ default = "default"
+
+ class Pylon1:
+ Herc_JATO = (1, HerculesWeapons.Herc_JATO)
+
+ class Pylon2:
+ LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (2, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
+ Smokewinder___red = (2, Weapons.Smokewinder___red)
+ Smokewinder___green = (2, Weapons.Smokewinder___green)
+ Smokewinder___blue = (2, Weapons.Smokewinder___blue)
+ Smokewinder___white = (2, Weapons.Smokewinder___white)
+ Smokewinder___yellow = (2, Weapons.Smokewinder___yellow)
+ Smokewinder___orange = (2, Weapons.Smokewinder___orange)
+ Herc_Ext_Fuel_Tank = (2, HerculesWeapons.Herc_Ext_Fuel_Tank)
+
+ class Pylon3:
+ LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (3, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
+ Smokewinder___red = (3, Weapons.Smokewinder___red)
+ Smokewinder___green = (3, Weapons.Smokewinder___green)
+ Smokewinder___blue = (3, Weapons.Smokewinder___blue)
+ Smokewinder___white = (3, Weapons.Smokewinder___white)
+ Smokewinder___yellow = (3, Weapons.Smokewinder___yellow)
+ Smokewinder___orange = (3, Weapons.Smokewinder___orange)
+ Herc_Ext_Fuel_Tank = (3, HerculesWeapons.Herc_Ext_Fuel_Tank)
+
+ class Pylon4:
+ LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (4, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
+ Smokewinder___red = (4, Weapons.Smokewinder___red)
+ Smokewinder___green = (4, Weapons.Smokewinder___green)
+ Smokewinder___blue = (4, Weapons.Smokewinder___blue)
+ Smokewinder___white = (4, Weapons.Smokewinder___white)
+ Smokewinder___yellow = (4, Weapons.Smokewinder___yellow)
+ Smokewinder___orange = (4, Weapons.Smokewinder___orange)
+ Herc_Ext_Fuel_Tank = (4, HerculesWeapons.Herc_Ext_Fuel_Tank)
+
+ class Pylon5:
+ LAU_68___7_2_75__rockets_M257__Parachute_illumination_ = (5, Weapons.LAU_68___7_2_75__rockets_M257__Parachute_illumination_)
+ Smokewinder___red = (5, Weapons.Smokewinder___red)
+ Smokewinder___green = (5, Weapons.Smokewinder___green)
+ Smokewinder___blue = (5, Weapons.Smokewinder___blue)
+ Smokewinder___white = (5, Weapons.Smokewinder___white)
+ Smokewinder___yellow = (5, Weapons.Smokewinder___yellow)
+ Smokewinder___orange = (5, Weapons.Smokewinder___orange)
+ Herc_Ext_Fuel_Tank = (5, HerculesWeapons.Herc_Ext_Fuel_Tank)
+
+ class Pylon6:
+ M61_Vulcan_Rotary_Cannon__20mm_ = (6, HerculesWeapons.M61_Vulcan_Rotary_Cannon__20mm_)
+
+ class Pylon7:
+ GAU_23A_Chain_Gun__30mm_ = (7, HerculesWeapons.GAU_23A_Chain_Gun__30mm_)
+
+ class Pylon8:
+ _105mm_Howitzer = (8, HerculesWeapons._105mm_Howitzer)
+
+ class Pylon9:
+ Herc_BattleStation = (9, HerculesWeapons.Herc_BattleStation)
+
+ class Pylon10:
+ Herc_Ammo_AGM_65D_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
+ Herc_Ammo_AGM_65H_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
+ Herc_Ammo_AGM_65G_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
+ Herc_Ammo_AGM_65E_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
+ Herc_Ammo_AGM_88C_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
+ Herc_Ammo_AGM_65K_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
+ Herc_Ammo_Vikhr_missiles = (10, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
+ Herc_Ammo_AGM_84A_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
+ Herc_Ammo_AGM_84E_missiles = (10, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
+ Herc_Ammo_KH25ML_missiles = (10, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
+ Herc_Ammo_KH25MPU_missiles = (10, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
+ Herc_Ammo_KH29T_missiles = (10, HerculesWeapons.Herc_Ammo_KH29T_missiles)
+ Herc_Ammo_KH29L_missiles = (10, HerculesWeapons.Herc_Ammo_KH29L_missiles)
+ Herc_Ammo_KH58U_missiles = (10, HerculesWeapons.Herc_Ammo_KH58U_missiles)
+ Herc_Ammo_S24B_missiles = (10, HerculesWeapons.Herc_Ammo_S24B_missiles)
+ Herc_Ammo_S25OFM_missiles = (10, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
+ Herc_Ammo_S25L_missiles = (10, HerculesWeapons.Herc_Ammo_S25L_missiles)
+ Herc_Ammo_GBU_10_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
+ Herc_Ammo_GBU_12_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
+ Herc_Ammo_GBU_16_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
+ Herc_Ammo_GBU_31_VB_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
+ Herc_Ammo_GBU_31_V3B_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
+ Herc_Ammo_GBU_38_bombs = (10, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
+ Herc_Ammo_CBU_87_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
+ Herc_Ammo_CBU_97_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
+ Herc_Ammo_CBU_103_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
+ Herc_Ammo_CBU_105_bombs = (10, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
+ Herc_Ammo_Mk_82_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
+ Herc_Ammo_Mk_82AIR_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
+ Herc_Ammo_Mk_82Snake_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
+ Herc_Ammo_Mk_83_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
+ Herc_Ammo_Mk_84_bombs = (10, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
+ Herc_Ammo_FAB100_bombs = (10, HerculesWeapons.Herc_Ammo_FAB100_bombs)
+ Herc_Ammo_FAB250_bombs = (10, HerculesWeapons.Herc_Ammo_FAB250_bombs)
+ Herc_Ammo_FAB500_bombs = (10, HerculesWeapons.Herc_Ammo_FAB500_bombs)
+ Herc_Ammo_BETAB500_bombs = (10, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
+ Herc_Ammo_BETAB500SP_bombs = (10, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
+ Herc_Ammo_KAB500KR_bombs = (10, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
+ Herc_Ammo_RBK250PTAB25M_bombs = (10, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
+ Herc_Ammo_RBK500255PTAB105_bombs = (10, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
+ Herc_Ammo_RBK500PTAB1M_bombs = (10, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
+#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
+ Herc_Ammo_KMGU296AO25RT_bombs = (10, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
+ Herc_Ammo_KMGU296AO25KO_bombs = (10, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
+ Herc_Ammo_MK20_bombs = (10, HerculesWeapons.Herc_Ammo_MK20_bombs)
+ Herc_Ammo_SAB100_bombs = (10, HerculesWeapons.Herc_Ammo_SAB100_bombs)
+ Herc_Ammo_hydra_HE_rockets = (10, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
+ Herc_Ammo_hydra_WP_rockets = (10, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
+ Herc_Ammo_AIM9M_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
+ Herc_Ammo_AIM9P5_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
+ Herc_Ammo_AIM9X_missiles = (10, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
+ Herc_Ammo_AIM7M_missiles = (10, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
+ Herc_Ammo_AIM120B_missiles = (10, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
+ Herc_Ammo_AIM120C_missiles = (10, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
+ Herc_Ammo_R60M_missiles = (10, HerculesWeapons.Herc_Ammo_R60M_missiles)
+ Herc_Ammo_MAGIC2_missiles = (10, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
+ Herc_Ammo_R27R_missiles = (10, HerculesWeapons.Herc_Ammo_R27R_missiles)
+ Herc_Ammo_R27ER_missiles = (10, HerculesWeapons.Herc_Ammo_R27ER_missiles)
+ Herc_Ammo_R27T_missiles = (10, HerculesWeapons.Herc_Ammo_R27T_missiles)
+ Herc_Ammo_R27ET_missiles = (10, HerculesWeapons.Herc_Ammo_R27ET_missiles)
+#ERRR Herc_Ammo_R27_missiles
+ Herc_Ammo_S530D_missiles = (10, HerculesWeapons.Herc_Ammo_S530D_missiles)
+ Herc_Ammo_AIM54C_missiles = (10, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
+ Herc_APC_M1043_HMMWV_Armament = (10, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
+ Herc_ATGM_M1045_HMMWV_TOW = (10, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
+ Herc_APC_M1025_HMMWV = (10, HerculesWeapons.Herc_APC_M1025_HMMWV)
+ Herc_SAM_M1097_HMMWV = (10, HerculesWeapons.Herc_SAM_M1097_HMMWV)
+ Herc_APC_COBRA = (10, HerculesWeapons.Herc_APC_COBRA)
+ Herc_ARV_BRDM_2 = (10, HerculesWeapons.Herc_ARV_BRDM_2)
+ Herc_TIGR_233036 = (10, HerculesWeapons.Herc_TIGR_233036)
+ Herc_IFV_BMD1 = (10, HerculesWeapons.Herc_IFV_BMD1)
+ Herc_IFV_BTRD = (10, HerculesWeapons.Herc_IFV_BTRD)
+ Herc_ART_NONA = (10, HerculesWeapons.Herc_ART_NONA)
+ Herc_GEN_CRATE = (10, HerculesWeapons.Herc_GEN_CRATE)
+
+ class Pylon11:
+ Herc_Ammo_AGM_65D_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
+ Herc_Ammo_AGM_65H_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
+ Herc_Ammo_AGM_65G_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
+ Herc_Ammo_AGM_65E_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
+ Herc_Ammo_AGM_88C_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
+ Herc_Ammo_AGM_65K_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
+ Herc_Ammo_Vikhr_missiles = (11, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
+ Herc_Ammo_AGM_84A_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
+ Herc_Ammo_AGM_84E_missiles = (11, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
+ Herc_Ammo_KH25ML_missiles = (11, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
+ Herc_Ammo_KH25MPU_missiles = (11, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
+ Herc_Ammo_KH29T_missiles = (11, HerculesWeapons.Herc_Ammo_KH29T_missiles)
+ Herc_Ammo_KH29L_missiles = (11, HerculesWeapons.Herc_Ammo_KH29L_missiles)
+ Herc_Ammo_KH58U_missiles = (11, HerculesWeapons.Herc_Ammo_KH58U_missiles)
+ Herc_Ammo_S24B_missiles = (11, HerculesWeapons.Herc_Ammo_S24B_missiles)
+ Herc_Ammo_S25OFM_missiles = (11, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
+ Herc_Ammo_S25L_missiles = (11, HerculesWeapons.Herc_Ammo_S25L_missiles)
+ Herc_Ammo_GBU_10_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
+ Herc_Ammo_GBU_12_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
+ Herc_Ammo_GBU_16_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
+ Herc_Ammo_GBU_31_VB_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
+ Herc_Ammo_GBU_31_V3B_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
+ Herc_Ammo_GBU_38_bombs = (11, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
+ Herc_Ammo_CBU_87_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
+ Herc_Ammo_CBU_97_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
+ Herc_Ammo_CBU_103_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
+ Herc_Ammo_CBU_105_bombs = (11, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
+ Herc_Ammo_Mk_82_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
+ Herc_Ammo_Mk_82AIR_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
+ Herc_Ammo_Mk_82Snake_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
+ Herc_Ammo_Mk_83_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
+ Herc_Ammo_Mk_84_bombs = (11, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
+ Herc_Ammo_FAB100_bombs = (11, HerculesWeapons.Herc_Ammo_FAB100_bombs)
+ Herc_Ammo_FAB250_bombs = (11, HerculesWeapons.Herc_Ammo_FAB250_bombs)
+ Herc_Ammo_FAB500_bombs = (11, HerculesWeapons.Herc_Ammo_FAB500_bombs)
+ Herc_Ammo_BETAB500_bombs = (11, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
+ Herc_Ammo_BETAB500SP_bombs = (11, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
+ Herc_Ammo_KAB500KR_bombs = (11, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
+ Herc_Ammo_RBK250PTAB25M_bombs = (11, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
+ Herc_Ammo_RBK500255PTAB105_bombs = (11, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
+ Herc_Ammo_RBK500PTAB1M_bombs = (11, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
+#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
+ Herc_Ammo_KMGU296AO25RT_bombs = (11, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
+ Herc_Ammo_KMGU296AO25KO_bombs = (11, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
+ Herc_Ammo_MK20_bombs = (11, HerculesWeapons.Herc_Ammo_MK20_bombs)
+ Herc_Ammo_SAB100_bombs = (11, HerculesWeapons.Herc_Ammo_SAB100_bombs)
+ Herc_Ammo_hydra_HE_rockets = (11, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
+ Herc_Ammo_hydra_WP_rockets = (11, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
+ Herc_Ammo_AIM9M_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
+ Herc_Ammo_AIM9P5_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
+ Herc_Ammo_AIM9X_missiles = (11, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
+ Herc_Ammo_AIM7M_missiles = (11, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
+ Herc_Ammo_AIM120B_missiles = (11, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
+ Herc_Ammo_AIM120C_missiles = (11, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
+ Herc_Ammo_R60M_missiles = (11, HerculesWeapons.Herc_Ammo_R60M_missiles)
+ Herc_Ammo_MAGIC2_missiles = (11, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
+ Herc_Ammo_R27R_missiles = (11, HerculesWeapons.Herc_Ammo_R27R_missiles)
+ Herc_Ammo_R27ER_missiles = (11, HerculesWeapons.Herc_Ammo_R27ER_missiles)
+ Herc_Ammo_R27T_missiles = (11, HerculesWeapons.Herc_Ammo_R27T_missiles)
+ Herc_Ammo_R27ET_missiles = (11, HerculesWeapons.Herc_Ammo_R27ET_missiles)
+#ERRR Herc_Ammo_R27_missiles
+ Herc_Ammo_S530D_missiles = (11, HerculesWeapons.Herc_Ammo_S530D_missiles)
+ Herc_Ammo_AIM54C_missiles = (11, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
+ Herc_APC_M1043_HMMWV_Armament = (11, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
+ Herc_ATGM_M1045_HMMWV_TOW = (11, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
+ Herc_AAA_Vulcan_M163 = (11, HerculesWeapons.Herc_AAA_Vulcan_M163)
+ Herc_SPG_M1126_Stryker_ICV = (11, HerculesWeapons.Herc_SPG_M1126_Stryker_ICV)
+ Herc_SPG_M1128_Stryker_MGS = (11, HerculesWeapons.Herc_SPG_M1128_Stryker_MGS)
+ Herc_ATGM_M1134_Stryker = (11, HerculesWeapons.Herc_ATGM_M1134_Stryker)
+ Herc_APC_LAV_25 = (11, HerculesWeapons.Herc_APC_LAV_25)
+ Herc_APC_M1025_HMMWV = (11, HerculesWeapons.Herc_APC_M1025_HMMWV)
+ Herc_SAM_M1097_HMMWV = (11, HerculesWeapons.Herc_SAM_M1097_HMMWV)
+ Herc_APC_COBRA = (11, HerculesWeapons.Herc_APC_COBRA)
+ Herc_APC_M113 = (11, HerculesWeapons.Herc_APC_M113)
+ Herc_Tanker_HEMTT = (11, HerculesWeapons.Herc_Tanker_HEMTT)
+ Herc_HEMTT_TFFT = (11, HerculesWeapons.Herc_HEMTT_TFFT)
+ Herc_IFV_M2A2_Bradley = (11, HerculesWeapons.Herc_IFV_M2A2_Bradley)
+ Herc_IFV_MCV80_Warrior = (11, HerculesWeapons.Herc_IFV_MCV80_Warrior)
+ Herc_IFV_BMP_1 = (11, HerculesWeapons.Herc_IFV_BMP_1)
+ Herc_IFV_BMP_2 = (11, HerculesWeapons.Herc_IFV_BMP_2)
+ Herc_IFV_BMP_3 = (11, HerculesWeapons.Herc_IFV_BMP_3)
+ Herc_ARV_BRDM_2 = (11, HerculesWeapons.Herc_ARV_BRDM_2)
+ Herc_APC_BTR_80 = (11, HerculesWeapons.Herc_APC_BTR_80)
+ Herc_SAM_ROLAND_ADS = (11, HerculesWeapons.Herc_SAM_ROLAND_ADS)
+ Herc_SAM_ROLAND_LN = (11, HerculesWeapons.Herc_SAM_ROLAND_LN)
+ Herc_SAM_13 = (11, HerculesWeapons.Herc_SAM_13)
+ Herc_ZSU_23_4 = (11, HerculesWeapons.Herc_ZSU_23_4)
+ Herc_SAM_19 = (11, HerculesWeapons.Herc_SAM_19)
+ Herc_UAZ_469 = (11, HerculesWeapons.Herc_UAZ_469)
+ Herc_URAL_375 = (11, HerculesWeapons.Herc_URAL_375)
+ Herc_M_818 = (11, HerculesWeapons.Herc_M_818)
+ Herc_TIGR_233036 = (11, HerculesWeapons.Herc_TIGR_233036)
+ Herc_AAA_GEPARD = (11, HerculesWeapons.Herc_AAA_GEPARD)
+ Herc_SAM_CHAPARRAL = (11, HerculesWeapons.Herc_SAM_CHAPARRAL)
+ Herc_SAM_LINEBACKER = (11, HerculesWeapons.Herc_SAM_LINEBACKER)
+ Herc_IFV_MARDER = (11, HerculesWeapons.Herc_IFV_MARDER)
+ Herc_IFV_TPZ = (11, HerculesWeapons.Herc_IFV_TPZ)
+ Herc_IFV_BMD1 = (11, HerculesWeapons.Herc_IFV_BMD1)
+ Herc_IFV_BTRD = (11, HerculesWeapons.Herc_IFV_BTRD)
+ Herc_ART_NONA = (11, HerculesWeapons.Herc_ART_NONA)
+ Herc_ART_GVOZDIKA = (11, HerculesWeapons.Herc_ART_GVOZDIKA)
+ Herc_APC_MTLB = (11, HerculesWeapons.Herc_APC_MTLB)
+ Herc_GEN_CRATE = (11, HerculesWeapons.Herc_GEN_CRATE)
+
+ class Pylon12:
+ Herc_Soldier_Squad = (12, HerculesWeapons.Herc_Soldier_Squad)
+ Herc_Ammo_AGM_65D_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65D_missiles)
+ Herc_Ammo_AGM_65H_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65H_missiles)
+ Herc_Ammo_AGM_65G_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65G_missiles)
+ Herc_Ammo_AGM_65E_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65E_missiles)
+ Herc_Ammo_AGM_88C_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_88C_missiles)
+ Herc_Ammo_AGM_65K_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_65K_missiles)
+ Herc_Ammo_Vikhr_missiles = (12, HerculesWeapons.Herc_Ammo_Vikhr_missiles)
+ Herc_Ammo_AGM_84A_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_84A_missiles)
+ Herc_Ammo_AGM_84E_missiles = (12, HerculesWeapons.Herc_Ammo_AGM_84E_missiles)
+ Herc_Ammo_KH25ML_missiles = (12, HerculesWeapons.Herc_Ammo_KH25ML_missiles)
+ Herc_Ammo_KH25MPU_missiles = (12, HerculesWeapons.Herc_Ammo_KH25MPU_missiles)
+ Herc_Ammo_KH29T_missiles = (12, HerculesWeapons.Herc_Ammo_KH29T_missiles)
+ Herc_Ammo_KH29L_missiles = (12, HerculesWeapons.Herc_Ammo_KH29L_missiles)
+ Herc_Ammo_KH58U_missiles = (12, HerculesWeapons.Herc_Ammo_KH58U_missiles)
+ Herc_Ammo_S24B_missiles = (12, HerculesWeapons.Herc_Ammo_S24B_missiles)
+ Herc_Ammo_S25OFM_missiles = (12, HerculesWeapons.Herc_Ammo_S25OFM_missiles)
+ Herc_Ammo_S25L_missiles = (12, HerculesWeapons.Herc_Ammo_S25L_missiles)
+ Herc_Ammo_GBU_10_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_10_bombs)
+ Herc_Ammo_GBU_12_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_12_bombs)
+ Herc_Ammo_GBU_16_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_16_bombs)
+ Herc_Ammo_GBU_31_VB_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_31_VB_bombs)
+ Herc_Ammo_GBU_31_V3B_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_31_V3B_bombs)
+ Herc_Ammo_GBU_38_bombs = (12, HerculesWeapons.Herc_Ammo_GBU_38_bombs)
+ Herc_Ammo_CBU_87_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_87_bombs)
+ Herc_Ammo_CBU_97_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_97_bombs)
+ Herc_Ammo_CBU_103_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_103_bombs)
+ Herc_Ammo_CBU_105_bombs = (12, HerculesWeapons.Herc_Ammo_CBU_105_bombs)
+ Herc_Ammo_Mk_82_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82_bombs)
+ Herc_Ammo_Mk_82AIR_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82AIR_bombs)
+ Herc_Ammo_Mk_82Snake_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_82Snake_bombs)
+ Herc_Ammo_Mk_83_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_83_bombs)
+ Herc_Ammo_Mk_84_bombs = (12, HerculesWeapons.Herc_Ammo_Mk_84_bombs)
+ Herc_Ammo_FAB100_bombs = (12, HerculesWeapons.Herc_Ammo_FAB100_bombs)
+ Herc_Ammo_FAB250_bombs = (12, HerculesWeapons.Herc_Ammo_FAB250_bombs)
+ Herc_Ammo_FAB500_bombs = (12, HerculesWeapons.Herc_Ammo_FAB500_bombs)
+ Herc_Ammo_BETAB500_bombs = (12, HerculesWeapons.Herc_Ammo_BETAB500_bombs)
+ Herc_Ammo_BETAB500SP_bombs = (12, HerculesWeapons.Herc_Ammo_BETAB500SP_bombs)
+ Herc_Ammo_KAB500KR_bombs = (12, HerculesWeapons.Herc_Ammo_KAB500KR_bombs)
+ Herc_Ammo_RBK250PTAB25M_bombs = (12, HerculesWeapons.Herc_Ammo_RBK250PTAB25M_bombs)
+ Herc_Ammo_RBK500255PTAB105_bombs = (12, HerculesWeapons.Herc_Ammo_RBK500255PTAB105_bombs)
+ Herc_Ammo_RBK500PTAB1M_bombs = (12, HerculesWeapons.Herc_Ammo_RBK500PTAB1M_bombs)
+#ERRR Herc_Ammo_Herc_Ammo_M117_bombs_bombs
+ Herc_Ammo_KMGU296AO25RT_bombs = (12, HerculesWeapons.Herc_Ammo_KMGU296AO25RT_bombs)
+ Herc_Ammo_KMGU296AO25KO_bombs = (12, HerculesWeapons.Herc_Ammo_KMGU296AO25KO_bombs)
+ Herc_Ammo_MK20_bombs = (12, HerculesWeapons.Herc_Ammo_MK20_bombs)
+ Herc_Ammo_SAB100_bombs = (12, HerculesWeapons.Herc_Ammo_SAB100_bombs)
+ Herc_Ammo_hydra_HE_rockets = (12, HerculesWeapons.Herc_Ammo_hydra_HE_rockets)
+ Herc_Ammo_hydra_WP_rockets = (12, HerculesWeapons.Herc_Ammo_hydra_WP_rockets)
+ Herc_Ammo_AIM9M_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9M_missiles)
+ Herc_Ammo_AIM9P5_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9P5_missiles)
+ Herc_Ammo_AIM9X_missiles = (12, HerculesWeapons.Herc_Ammo_AIM9X_missiles)
+ Herc_Ammo_AIM7M_missiles = (12, HerculesWeapons.Herc_Ammo_AIM7M_missiles)
+ Herc_Ammo_AIM120B_missiles = (12, HerculesWeapons.Herc_Ammo_AIM120B_missiles)
+ Herc_Ammo_AIM120C_missiles = (12, HerculesWeapons.Herc_Ammo_AIM120C_missiles)
+ Herc_Ammo_R60M_missiles = (12, HerculesWeapons.Herc_Ammo_R60M_missiles)
+ Herc_Ammo_MAGIC2_missiles = (12, HerculesWeapons.Herc_Ammo_MAGIC2_missiles)
+ Herc_Ammo_R27R_missiles = (12, HerculesWeapons.Herc_Ammo_R27R_missiles)
+ Herc_Ammo_R27ER_missiles = (12, HerculesWeapons.Herc_Ammo_R27ER_missiles)
+ Herc_Ammo_R27T_missiles = (12, HerculesWeapons.Herc_Ammo_R27T_missiles)
+ Herc_Ammo_R27ET_missiles = (12, HerculesWeapons.Herc_Ammo_R27ET_missiles)
+#ERRR Herc_Ammo_R27_missiles
+ Herc_Ammo_S530D_missiles = (12, HerculesWeapons.Herc_Ammo_S530D_missiles)
+ Herc_Ammo_AIM54C_missiles = (12, HerculesWeapons.Herc_Ammo_AIM54C_missiles)
+ Herc_APC_M1043_HMMWV_Armament = (12, HerculesWeapons.Herc_APC_M1043_HMMWV_Armament)
+ Herc_ATGM_M1045_HMMWV_TOW = (12, HerculesWeapons.Herc_ATGM_M1045_HMMWV_TOW)
+ Herc_AAA_Vulcan_M163 = (12, HerculesWeapons.Herc_AAA_Vulcan_M163)
+ Herc_APC_LAV_25 = (12, HerculesWeapons.Herc_APC_LAV_25)
+ Herc_APC_M1025_HMMWV = (12, HerculesWeapons.Herc_APC_M1025_HMMWV)
+ Herc_SAM_M1097_HMMWV = (12, HerculesWeapons.Herc_SAM_M1097_HMMWV)
+ Herc_APC_COBRA = (12, HerculesWeapons.Herc_APC_COBRA)
+ Herc_APC_M113 = (12, HerculesWeapons.Herc_APC_M113)
+ Herc_IFV_BMP_1 = (12, HerculesWeapons.Herc_IFV_BMP_1)
+ Herc_ARV_BRDM_2 = (12, HerculesWeapons.Herc_ARV_BRDM_2)
+ Herc_APC_BTR_80 = (12, HerculesWeapons.Herc_APC_BTR_80)
+ Herc_SAM_13 = (12, HerculesWeapons.Herc_SAM_13)
+ Herc_UAZ_469 = (12, HerculesWeapons.Herc_UAZ_469)
+ Herc_URAL_375 = (12, HerculesWeapons.Herc_URAL_375)
+ Herc_M_818 = (12, HerculesWeapons.Herc_M_818)
+ Herc_TIGR_233036 = (12, HerculesWeapons.Herc_TIGR_233036)
+ Herc_SAM_CHAPARRAL = (12, HerculesWeapons.Herc_SAM_CHAPARRAL)
+ Herc_IFV_BMD1 = (12, HerculesWeapons.Herc_IFV_BMD1)
+ Herc_IFV_BTRD = (12, HerculesWeapons.Herc_IFV_BTRD)
+ Herc_ART_NONA = (12, HerculesWeapons.Herc_ART_NONA)
+ Herc_GEN_CRATE = (12, HerculesWeapons.Herc_GEN_CRATE)
+
+ pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
+
+ tasks = [task.Transport, task.CAS, task.GroundAttack]
+ task_default = task.Transport
diff --git a/pydcs_extensions/mod_units.py b/pydcs_extensions/mod_units.py
index cfa1b321..bf841e95 100644
--- a/pydcs_extensions/mod_units.py
+++ b/pydcs_extensions/mod_units.py
@@ -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,
diff --git a/pydcs_extensions/rafale/rafale.py b/pydcs_extensions/rafale/rafale.py
index 880192a2..458a6e8e 100644
--- a/pydcs_extensions/rafale/rafale.py
+++ b/pydcs_extensions/rafale/rafale.py
@@ -9,16 +9,37 @@ class RafaleWeapons:
AS_30L = {"clsid": "{AS_30L}", "name": "AS_30L", "weight": 292}
Exocet = {"clsid": "{Exocet}", "name": "Exocet", "weight": 640}
Thales_RBE2 = {"clsid": "{Thales_RBE2}", "name": "Thales_RBE2", "weight": 1.4789}
+ Thales_RBE2_ = {"clsid": "{Thales_RBE2}", "name": "Thales_RBE2", "weight": 1.4789}
DAMOCLES = {"clsid": "{DAMOCLES}", "name": "DAMOCLES", "weight": 265}
DAMOCLES_ = {"clsid": "{DAMOCLES}", "name": "DAMOCLES", "weight": 265}
+ DAMOCLES__ = {"clsid": "{DAMOCLES}", "name": "DAMOCLES", "weight": 265}
_2300_PTB_RAF = {"clsid": "{2300-PTB RAF}", "name": "2300-PTB RAF", "weight": 70}
_2300_PTB_RAF_ = {"clsid": "{2300-PTB RAF}", "name": "2300-PTB RAF", "weight": 70}
PTB_1500 = {"clsid": "{PTB-1500}", "name": "PTB-1500", "weight": 70}
+ RPL_711 = {"clsid": "{RPL 711}", "name": "RPL 711", "weight": 70}
+ RPL_711_ = {"clsid": "{RPL 711}", "name": "RPL 711", "weight": 70}
+ RPL_711__ = {"clsid": "{RPL 711}", "name": "RPL 711", "weight": 70}
+ RPL_711___ = {"clsid": "{PTB-1500}", "name": "RPL 711", "weight": 50}
+ RPL_751 = {"clsid": "{RPL-751}", "name": "RPL-751", "weight": 50}
+ RPL751 = {"clsid": "{RPL751}", "name": "RPL751", "weight": 70}
+ RPL751_ = {"clsid": "{RPL751}", "name": "RPL751", "weight": 70}
+ RPL751__ = {"clsid": "{RPL751}", "name": "RPL751", "weight": 70}
+ METEOR = {"clsid": "{RAFALE_MBDA_METEOR}", "name": "METEOR", "weight": 199}
+ METEOR_x2 = {"clsid": "{LAU-115_2xLAU-127_MBDA_METEOR}", "name": "METEOR x2", "weight": 445}
+ GBU_49 = {"clsid": "{GBU_49}", "name": "GBU_49", "weight": 525}
+ GBU12PII = {"clsid": "{GBU12PII}", "name": "GBU12PII", "weight": 525}
+ AASM_250 = {"clsid": "{AASM_250}", "name": "AASM_250", "weight": 250}
+ AASM_250_L = {"clsid": "{AASM_250_L}", "name": "AASM_250_L", "weight": 500}
+ AASM_250_R = {"clsid": "{AASM_250_R}", "name": "AASM_250_R", "weight": 500}
+ AASM_250_RIGHT = {"clsid": "{AASM_250_RIGHT}", "name": "AASM_250_RIGHT", "weight": 250}
+ _2_GBU_54_V_1_B = {"clsid": "{BRU-70A_2*GBU-54_LEFT}", "name": "2 GBU-54(V)1/B", "weight": 566}
+ _2_GBU_54_V_1_B_ = {"clsid": "{BRU-70A_2*GBU-54_RIGHT}", "name": "2 GBU-54(V)1/B", "weight": 566}
+ _3_GBU_54_V_1_B = {"clsid": "{BRU-70A_3*GBU-54}", "name": "3 GBU-54(V)1/B", "weight": 819}
class Rafale_A_S(PlaneType):
id = "Rafale_A_S"
- flyable = False
+ flyable = True
height = 5.28
width = 10.13
length = 15.96
@@ -34,381 +55,825 @@ class Rafale_A_S(PlaneType):
class Liveries:
+ class USSR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
class Georgia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Syria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Finland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Venezuela(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Australia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Germany(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SaudiArabia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Israel(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Croatia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class CzechRepublic(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Sudan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Norway(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Romania(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Spain(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Ukraine(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Belgium(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Slovakia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Greece(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class UK(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Insurgents(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Hungary(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class France(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Abkhazia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Russia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Sweden(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Austria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Switzerland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Italy(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SouthOssetia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SouthKorea(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Iran(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Ukraine(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Libya(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Belgium(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Slovakia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Greece(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class UK(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Third_Reich(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Hungary(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Abkhazia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Morocco(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class United_Nations_Peacekeepers(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Switzerland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SouthOssetia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Vietnam(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class China(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Pakistan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Yemen(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Belarus(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class NorthKorea(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Iraq(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Kazakhstan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Bulgaria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Kuwait(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Serbia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Oman(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class India(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class USAFAggressors(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class USA(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Denmark(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Egypt(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Canada(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class TheNetherlands(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Turkey(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Japan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Poland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Syria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Finland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Kazakhstan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Denmark(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Sweden(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Croatia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class CzechRepublic(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class GDR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Yugoslavia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Bulgaria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SouthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Tunisia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Lebanon(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Portugal(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Cuba(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Insurgents(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SaudiArabia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class France(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class USA(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Honduras(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Qatar(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Russia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class United_Arab_Emirates(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Italian_Social_Republi(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Austria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Bahrain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Italy(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Chile(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Turkey(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Philippines(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Algeria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Pakistan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Malaysia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Indonesia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Iraq(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Germany(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class South_Africa(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Jordan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Mexico(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class USAFAggressors(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Brazil(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Spain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Belarus(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Canada(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class NorthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Ethiopia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Japan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Thailand(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Pylon1:
Smokewinder___red = (1, Weapons.Smokewinder___red)
@@ -417,96 +882,93 @@ class Rafale_A_S(PlaneType):
Smokewinder___white = (1, Weapons.Smokewinder___white)
Smokewinder___yellow = (1, Weapons.Smokewinder___yellow)
Smokewinder___orange = (1, Weapons.Smokewinder___orange)
- MICA_IR = (1, Weapons.MICA_IR)
AIM_9M_Sidewinder_IR_AAM = (1, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (1, Weapons.AIM_9P_Sidewinder_IR_AAM)
-#ERRR {BRU-42_3*GBU-12}
+ R_550_Magic_2 = (1, Weapons.R_550_Magic_2)
class Pylon2:
- _2xGBU_12 = (2, Weapons._2xGBU_12)
+ AASM_250_L = (2, RafaleWeapons.AASM_250_L)
+ GBU_49 = (2, RafaleWeapons.GBU_49)
MER_2_MK_82 = (2, Weapons.MER_2_MK_82)
_3_Mk_82 = (2, Weapons._3_Mk_82)
- GBU_10 = (2, Weapons.GBU_10)
- GBU_12 = (2, Weapons.GBU_12)
+ GBU12PII = (2, RafaleWeapons.GBU12PII)
Mk_20 = (2, Weapons.Mk_20)
_3_Mk_20_Rockeye = (2, Weapons._3_Mk_20_Rockeye)
Mk_84 = (2, Weapons.Mk_84)
GBU_24 = (2, Weapons.GBU_24)
- AGM_88C_ = (2, Weapons.AGM_88C_)
LAU_131___7_2_75__rockets_M151__HE_ = (2, Weapons.LAU_131___7_2_75__rockets_M151__HE_)
LAU3_HE151 = (2, Weapons.LAU3_HE151)
LAU3_WP156 = (2, Weapons.LAU3_WP156)
LAU3_HE5 = (2, Weapons.LAU3_HE5)
SCALP = (2, RafaleWeapons.SCALP)
AS_30L = (2, RafaleWeapons.AS_30L)
+ AGM_88C_ = (2, Weapons.AGM_88C_)
class Pylon3:
- GBU_10 = (3, Weapons.GBU_10)
+ GBU_49 = (3, RafaleWeapons.GBU_49)
GBU_24 = (3, Weapons.GBU_24)
-#ERRR {BRU-42_3*GBU-12}
- _2xGBU_12 = (3, Weapons._2xGBU_12)
- GBU_12 = (3, Weapons.GBU_12)
+ GBU12PII = (3, RafaleWeapons.GBU12PII)
MER_2_MK_82 = (3, Weapons.MER_2_MK_82)
_3_Mk_82 = (3, Weapons._3_Mk_82)
AGM_88C_ = (3, Weapons.AGM_88C_)
LAU3_HE151 = (3, Weapons.LAU3_HE151)
LAU3_WP156 = (3, Weapons.LAU3_WP156)
LAU_131x3_HYDRA_70_M151 = (3, Weapons.LAU_131x3_HYDRA_70_M151)
- SCALP = (3, RafaleWeapons.SCALP)
AS_30L = (3, RafaleWeapons.AS_30L)
- PTB_1500 = (3, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (3, RafaleWeapons._2300_PTB_RAF)
+ RPL_711__ = (3, RafaleWeapons.RPL_711__)
+ RPL751__ = (3, RafaleWeapons.RPL751__)
class Pylon4:
AIM_9M_Sidewinder_IR_AAM = (4, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (4, Weapons.AIM_9P_Sidewinder_IR_AAM)
MICA_IR = (4, Weapons.MICA_IR)
- LAU3_WP156 = (4, Weapons.LAU3_WP156)
+ LAU_10___4_ZUNI_MK_71 = (4, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (4, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
+ LAU3_HE151 = (4, Weapons.LAU3_HE151)
class Pylon5:
- Mk_84 = (5, Weapons.Mk_84)
- PTB_1500 = (5, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (5, RafaleWeapons._2300_PTB_RAF)
+ GBU12PII = (5, RafaleWeapons.GBU12PII)
+ RPL_711__ = (5, RafaleWeapons.RPL_711__)
+ RPL751__ = (5, RafaleWeapons.RPL751__)
Mercury_LLTV_Pod = (5, Weapons.Mercury_LLTV_Pod)
+ SCALP = (5, RafaleWeapons.SCALP)
Exocet = (5, RafaleWeapons.Exocet)
+ GBU_49 = (5, RafaleWeapons.GBU_49)
class Pylon6:
+ LAU_10___4_ZUNI_MK_71 = (6, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (6, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
AIM_9M_Sidewinder_IR_AAM = (6, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (6, Weapons.AIM_9P_Sidewinder_IR_AAM)
MICA_IR = (6, Weapons.MICA_IR)
- LAU3_WP156 = (6, Weapons.LAU3_WP156)
+ LAU3_HE151 = (6, Weapons.LAU3_HE151)
class Pylon7:
AN_AAQ_28_LITENING = (7, Weapons.AN_AAQ_28_LITENING)
- DAMOCLES_ = (7, RafaleWeapons.DAMOCLES_)
- Thales_RBE2 = (7, RafaleWeapons.Thales_RBE2)
+ DAMOCLES__ = (7, RafaleWeapons.DAMOCLES__)
+ Thales_RBE2_ = (7, RafaleWeapons.Thales_RBE2_)
class Pylon8:
- GBU_10 = (8, Weapons.GBU_10)
+ GBU_49 = (8, RafaleWeapons.GBU_49)
GBU_24 = (8, Weapons.GBU_24)
-#ERRR {BRU-42_3*GBU-12}
- _2xGBU_12 = (8, Weapons._2xGBU_12)
- GBU_12 = (8, Weapons.GBU_12)
+ GBU12PII = (8, RafaleWeapons.GBU12PII)
MER_2_MK_82 = (8, Weapons.MER_2_MK_82)
_3_Mk_20_Rockeye = (8, Weapons._3_Mk_20_Rockeye)
_3_Mk_82 = (8, Weapons._3_Mk_82)
- AGM_88C_ = (8, Weapons.AGM_88C_)
LAU3_HE151 = (8, Weapons.LAU3_HE151)
LAU3_WP156 = (8, Weapons.LAU3_WP156)
LAU_131x3_HYDRA_70_M151 = (8, Weapons.LAU_131x3_HYDRA_70_M151)
- SCALP = (8, RafaleWeapons.SCALP)
AS_30L = (8, RafaleWeapons.AS_30L)
- PTB_1500 = (8, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (8, RafaleWeapons._2300_PTB_RAF)
+ AGM_88C_ = (8, Weapons.AGM_88C_)
+ RPL_711__ = (8, RafaleWeapons.RPL_711__)
+ RPL751__ = (8, RafaleWeapons.RPL751__)
class Pylon9:
+ AASM_250_R = (9, RafaleWeapons.AASM_250_R)
+ GBU_49 = (9, RafaleWeapons.GBU_49)
GBU_24 = (9, Weapons.GBU_24)
-#ERRR {BRU-42_3*GBU-12}
MER_2_MK_82 = (9, Weapons.MER_2_MK_82)
- _2xGBU_12 = (9, Weapons._2xGBU_12)
- GBU_10 = (9, Weapons.GBU_10)
- GBU_12 = (9, Weapons.GBU_12)
- Mk_20 = (9, Weapons.Mk_20)
+ GBU12PII = (9, RafaleWeapons.GBU12PII)
_3_Mk_20_Rockeye = (9, Weapons._3_Mk_20_Rockeye)
Mk_84 = (9, Weapons.Mk_84)
_3_Mk_82 = (9, Weapons._3_Mk_82)
@@ -519,9 +981,9 @@ class Rafale_A_S(PlaneType):
AS_30L = (9, RafaleWeapons.AS_30L)
class Pylon10:
+ R_550_Magic_2 = (10, Weapons.R_550_Magic_2)
AIM_9M_Sidewinder_IR_AAM = (10, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (10, Weapons.AIM_9P_Sidewinder_IR_AAM)
- MICA_IR = (10, Weapons.MICA_IR)
Smokewinder___red = (10, Weapons.Smokewinder___red)
Smokewinder___green = (10, Weapons.Smokewinder___green)
Smokewinder___blue = (10, Weapons.Smokewinder___blue)
@@ -531,13 +993,13 @@ class Rafale_A_S(PlaneType):
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
- tasks = [task.CAP, task.Escort, task.FighterSweep, task.GroundAttack, task.CAS, task.AFAC, task.RunwayAttack, task.AntishipStrike]
+ tasks = [task.CAP, task.Escort, task.FighterSweep, task.GroundAttack, task.CAS, task.AFAC, task.RunwayAttack, task.AntishipStrike, task.SEAD, task.PinpointStrike]
task_default = task.CAP
class Rafale_M(PlaneType):
id = "Rafale_M"
- flyable = False
+ flyable = True
height = 5.28
width = 10.13
length = 15.96
@@ -553,381 +1015,825 @@ class Rafale_M(PlaneType):
class Liveries:
+ class USSR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
class Georgia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Syria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Finland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Venezuela(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Australia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Germany(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SaudiArabia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Israel(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Croatia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class CzechRepublic(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Sudan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Norway(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Romania(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Spain(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Ukraine(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Belgium(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Slovakia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Greece(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class UK(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Insurgents(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Hungary(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class France(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Abkhazia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Russia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Sweden(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Austria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Switzerland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Italy(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SouthOssetia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class SouthKorea(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Iran(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Ukraine(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Libya(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Belgium(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Slovakia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Greece(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class UK(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Third_Reich(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Hungary(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Abkhazia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Morocco(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class United_Nations_Peacekeepers(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Switzerland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SouthOssetia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Vietnam(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class China(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Pakistan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Yemen(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
- class Belarus(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class NorthKorea(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Iraq(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Kazakhstan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Bulgaria(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ class Kuwait(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Serbia(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Oman(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class India(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class USAFAggressors(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class USA(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Denmark(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Egypt(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Canada(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class TheNetherlands(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Turkey(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
-
- class Japan(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Poland(Enum):
- _01_MARINE_12_F = "01 MARINE 12 F"
- _02_MARINE_MAT_17F = "02 MARINE MAT 17F"
- _03_BLACK_DERIVE_11F = "03 BLACK DERIVE 11F"
- _04_MARINE_OLD = "04 MARINE OLD"
- _05_BRAZIL = "05 BRAZIL"
- _06_NEUTRE = "06 NEUTRE"
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Syria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Finland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Kazakhstan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Denmark(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Sweden(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Croatia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class CzechRepublic(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class GDR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Yugoslavia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Bulgaria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SouthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Tunisia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Lebanon(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Portugal(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Cuba(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Insurgents(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class SaudiArabia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class France(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class USA(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Honduras(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Qatar(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Russia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class United_Arab_Emirates(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Italian_Social_Republi(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Austria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Bahrain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Italy(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Chile(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Turkey(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Philippines(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Algeria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Pakistan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Malaysia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Indonesia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Iraq(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Germany(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class South_Africa(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Jordan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Mexico(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class USAFAggressors(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Brazil(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Spain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Belarus(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Canada(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class NorthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Ethiopia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Japan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
+
+ class Thailand(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+ _04_11f_tiger_meet = "04 11f tiger meet"
+ _05_brazil = "05 brazil"
+ _07_marine_tiger_2014 = "07 marine tiger 2014"
+ _08_flottile_12_f_90_ans = "08 flottile 12-f.90 ans"
+ _09_marine_mat_17f = "09 marine mat 17f"
class Pylon1:
Smokewinder___red = (1, Weapons.Smokewinder___red)
@@ -936,7 +1842,7 @@ class Rafale_M(PlaneType):
Smokewinder___white = (1, Weapons.Smokewinder___white)
Smokewinder___yellow = (1, Weapons.Smokewinder___yellow)
Smokewinder___orange = (1, Weapons.Smokewinder___orange)
- MICA_IR = (1, Weapons.MICA_IR)
+ R_550_Magic_2 = (1, Weapons.R_550_Magic_2)
AIM_9M_Sidewinder_IR_AAM = (1, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (1, Weapons.AIM_9P_Sidewinder_IR_AAM)
@@ -955,6 +1861,8 @@ class Rafale_M(PlaneType):
AIM_120C = (2, Weapons.AIM_120C)
LAU_115_2_LAU_127_AIM_120C = (2, Weapons.LAU_115_2_LAU_127_AIM_120C)
Super_530D = (2, Weapons.Super_530D)
+ METEOR = (2, RafaleWeapons.METEOR)
+ AASM_250 = (2, RafaleWeapons.AASM_250)
class Pylon3:
Mk_84 = (3, Weapons.Mk_84)
@@ -969,33 +1877,41 @@ class Rafale_M(PlaneType):
AIM_120B = (3, Weapons.AIM_120B)
AIM_120C = (3, Weapons.AIM_120C)
Super_530D = (3, Weapons.Super_530D)
- PTB_1500 = (3, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (3, RafaleWeapons._2300_PTB_RAF)
+ RPL_711__ = (3, RafaleWeapons.RPL_711__)
+ RPL751__ = (3, RafaleWeapons.RPL751__)
+ METEOR = (3, RafaleWeapons.METEOR)
class Pylon4:
MICA_IR = (4, Weapons.MICA_IR)
AIM_9M_Sidewinder_IR_AAM = (4, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (4, Weapons.AIM_9P_Sidewinder_IR_AAM)
LAU3_WP156 = (4, Weapons.LAU3_WP156)
+ LAU_10___4_ZUNI_MK_71 = (4, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (4, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
+ Mk_82 = (4, Weapons.Mk_82)
class Pylon5:
- PTB_1500 = (5, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (5, RafaleWeapons._2300_PTB_RAF)
+ RPL_711__ = (5, RafaleWeapons.RPL_711__)
+ RPL751__ = (5, RafaleWeapons.RPL751__)
MICA_IR = (5, Weapons.MICA_IR)
AIM_7M = (5, Weapons.AIM_7M)
AIM_120B = (5, Weapons.AIM_120B)
AIM_120C = (5, Weapons.AIM_120C)
Super_530D = (5, Weapons.Super_530D)
+ METEOR = (5, RafaleWeapons.METEOR)
class Pylon6:
MICA_IR = (6, Weapons.MICA_IR)
AIM_9M_Sidewinder_IR_AAM = (6, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (6, Weapons.AIM_9P_Sidewinder_IR_AAM)
LAU3_WP156 = (6, Weapons.LAU3_WP156)
+ LAU_10___4_ZUNI_MK_71 = (6, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (6, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
+ Mk_82 = (6, Weapons.Mk_82)
class Pylon7:
AN_AAQ_28_LITENING = (7, Weapons.AN_AAQ_28_LITENING)
- DAMOCLES_ = (7, RafaleWeapons.DAMOCLES_)
+ DAMOCLES__ = (7, RafaleWeapons.DAMOCLES__)
class Pylon8:
Mk_84 = (8, Weapons.Mk_84)
@@ -1010,10 +1926,12 @@ class Rafale_M(PlaneType):
AIM_120B = (8, Weapons.AIM_120B)
AIM_120C = (8, Weapons.AIM_120C)
Super_530D = (8, Weapons.Super_530D)
- PTB_1500 = (8, RafaleWeapons.PTB_1500)
- _2300_PTB_RAF_ = (8, RafaleWeapons._2300_PTB_RAF)
+ RPL_711__ = (8, RafaleWeapons.RPL_711__)
+ RPL751__ = (8, RafaleWeapons.RPL751__)
+ METEOR = (8, RafaleWeapons.METEOR)
class Pylon9:
+ METEOR = (9, RafaleWeapons.METEOR)
Mk_84 = (9, Weapons.Mk_84)
MER_2_MK_83 = (9, Weapons.MER_2_MK_83)
MER_2_MK_82 = (9, Weapons.MER_2_MK_82)
@@ -1028,11 +1946,12 @@ class Rafale_M(PlaneType):
AIM_120C = (9, Weapons.AIM_120C)
LAU_115_2_LAU_127_AIM_120C = (9, Weapons.LAU_115_2_LAU_127_AIM_120C)
Super_530D = (9, Weapons.Super_530D)
+ AASM_250_RIGHT = (9, RafaleWeapons.AASM_250_RIGHT)
class Pylon10:
+ R_550_Magic_2 = (10, Weapons.R_550_Magic_2)
AIM_9M_Sidewinder_IR_AAM = (10, Weapons.AIM_9M_Sidewinder_IR_AAM)
AIM_9P_Sidewinder_IR_AAM = (10, Weapons.AIM_9P_Sidewinder_IR_AAM)
- MICA_IR = (10, Weapons.MICA_IR)
Smokewinder___red = (10, Weapons.Smokewinder___red)
Smokewinder___green = (10, Weapons.Smokewinder___green)
Smokewinder___blue = (10, Weapons.Smokewinder___blue)
@@ -1044,3 +1963,1018 @@ class Rafale_M(PlaneType):
tasks = [task.CAP, task.Escort, task.FighterSweep, task.GroundAttack, task.CAS, task.AFAC, task.RunwayAttack, task.AntishipStrike, task.Reconnaissance, task.Intercept]
task_default = task.CAP
+
+
+class Rafale_B(PlaneType):
+ id = "Rafale_B"
+ flyable = True
+ height = 5.28
+ width = 10.13
+ length = 15.96
+ fuel_max = 5000
+ max_speed = 2001.996
+ chaff = 48
+ flare = 48
+ charge_total = 96
+ chaff_charge_size = 1
+ flare_charge_size = 1
+ category = "Interceptor" #{78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
+ radio_frequency = 127.5
+
+ class Liveries:
+
+ class USSR(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Georgia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Venezuela(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Australia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Israel(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Sudan(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Norway(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Romania(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Iran(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Ukraine(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Libya(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Belgium(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Slovakia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Greece(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class UK(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Third_Reich(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Hungary(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Abkhazia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Morocco(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class United_Nations_Peacekeepers(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Switzerland(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class SouthOssetia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Vietnam(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class China(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Yemen(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Kuwait(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Serbia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Oman(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class India(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Egypt(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class TheNetherlands(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Poland(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Syria(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Finland(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Kazakhstan(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Denmark(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Sweden(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Croatia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class CzechRepublic(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class GDR(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Yugoslavia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Bulgaria(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class SouthKorea(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Tunisia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Lebanon(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Portugal(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Cuba(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Insurgents(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class SaudiArabia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class France(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class USA(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Honduras(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Qatar(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Russia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class United_Arab_Emirates(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Italian_Social_Republi(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Austria(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Bahrain(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Italy(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Chile(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Turkey(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Philippines(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Algeria(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Pakistan(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Malaysia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Indonesia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Iraq(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Germany(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class South_Africa(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Jordan(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Mexico(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class USAFAggressors(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Brazil(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Spain(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Belarus(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Canada(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class NorthKorea(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Ethiopia(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Japan(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Thailand(Enum):
+ _01_rafale_b_lafayette = "01 rafale b lafayette"
+ _02_rafale_b_mt_de_marsan = "02 rafale b mt de marsan"
+ _03_standard = "03 standard"
+
+ class Pylon1:
+ Smokewinder___red = (1, Weapons.Smokewinder___red)
+ Smokewinder___green = (1, Weapons.Smokewinder___green)
+ Smokewinder___blue = (1, Weapons.Smokewinder___blue)
+ Smokewinder___white = (1, Weapons.Smokewinder___white)
+ Smokewinder___yellow = (1, Weapons.Smokewinder___yellow)
+ Smokewinder___orange = (1, Weapons.Smokewinder___orange)
+ AIM_9M_Sidewinder_IR_AAM = (1, Weapons.AIM_9M_Sidewinder_IR_AAM)
+ AIM_9P_Sidewinder_IR_AAM = (1, Weapons.AIM_9P_Sidewinder_IR_AAM)
+ R_550_Magic_2 = (1, Weapons.R_550_Magic_2)
+
+ class Pylon2:
+ AASM_250_L = (2, RafaleWeapons.AASM_250_L)
+ GBU_49 = (2, RafaleWeapons.GBU_49)
+ MER_2_MK_82 = (2, Weapons.MER_2_MK_82)
+ _3_Mk_82 = (2, Weapons._3_Mk_82)
+ GBU12PII = (2, RafaleWeapons.GBU12PII)
+ Mk_20 = (2, Weapons.Mk_20)
+ _3_Mk_20_Rockeye = (2, Weapons._3_Mk_20_Rockeye)
+ Mk_84 = (2, Weapons.Mk_84)
+ GBU_24 = (2, Weapons.GBU_24)
+ LAU_131___7_2_75__rockets_M151__HE_ = (2, Weapons.LAU_131___7_2_75__rockets_M151__HE_)
+ LAU3_HE151 = (2, Weapons.LAU3_HE151)
+ LAU3_WP156 = (2, Weapons.LAU3_WP156)
+ LAU3_HE5 = (2, Weapons.LAU3_HE5)
+ SCALP = (2, RafaleWeapons.SCALP)
+ AS_30L = (2, RafaleWeapons.AS_30L)
+ AGM_88C_ = (2, Weapons.AGM_88C_)
+
+ class Pylon3:
+ GBU_49 = (3, RafaleWeapons.GBU_49)
+ GBU_24 = (3, Weapons.GBU_24)
+ GBU12PII = (3, RafaleWeapons.GBU12PII)
+ MER_2_MK_82 = (3, Weapons.MER_2_MK_82)
+ _3_Mk_82 = (3, Weapons._3_Mk_82)
+ AGM_88C_ = (3, Weapons.AGM_88C_)
+ LAU3_HE151 = (3, Weapons.LAU3_HE151)
+ LAU3_WP156 = (3, Weapons.LAU3_WP156)
+ LAU_131x3_HYDRA_70_M151 = (3, Weapons.LAU_131x3_HYDRA_70_M151)
+ AS_30L = (3, RafaleWeapons.AS_30L)
+ RPL_711__ = (3, RafaleWeapons.RPL_711__)
+ RPL751__ = (3, RafaleWeapons.RPL751__)
+ Mk_84 = (3, Weapons.Mk_84)
+
+ class Pylon4:
+ AIM_9M_Sidewinder_IR_AAM = (4, Weapons.AIM_9M_Sidewinder_IR_AAM)
+ AIM_9P_Sidewinder_IR_AAM = (4, Weapons.AIM_9P_Sidewinder_IR_AAM)
+ MICA_IR = (4, Weapons.MICA_IR)
+ LAU_10___4_ZUNI_MK_71 = (4, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (4, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
+ LAU3_HE151 = (4, Weapons.LAU3_HE151)
+
+ class Pylon5:
+ GBU12PII = (5, RafaleWeapons.GBU12PII)
+ Mk_84 = (5, Weapons.Mk_84)
+ RPL_711__ = (5, RafaleWeapons.RPL_711__)
+ RPL751__ = (5, RafaleWeapons.RPL751__)
+ Mercury_LLTV_Pod = (5, Weapons.Mercury_LLTV_Pod)
+ SCALP = (5, RafaleWeapons.SCALP)
+ Exocet = (5, RafaleWeapons.Exocet)
+ GBU_49 = (5, RafaleWeapons.GBU_49)
+ MER_2_MK_83 = (5, Weapons.MER_2_MK_83)
+ MER_2_MK_82 = (5, Weapons.MER_2_MK_82)
+
+ class Pylon6:
+ LAU_10___4_ZUNI_MK_71 = (6, Weapons.LAU_10___4_ZUNI_MK_71)
+ LAU_61___19_2_75__rockets_MK151_HE = (6, Weapons.LAU_61___19_2_75__rockets_MK151_HE)
+ AIM_9M_Sidewinder_IR_AAM = (6, Weapons.AIM_9M_Sidewinder_IR_AAM)
+ AIM_9P_Sidewinder_IR_AAM = (6, Weapons.AIM_9P_Sidewinder_IR_AAM)
+ MICA_IR = (6, Weapons.MICA_IR)
+ LAU3_HE151 = (6, Weapons.LAU3_HE151)
+
+ class Pylon7:
+ AN_AAQ_28_LITENING = (7, Weapons.AN_AAQ_28_LITENING)
+ DAMOCLES__ = (7, RafaleWeapons.DAMOCLES__)
+ Thales_RBE2_ = (7, RafaleWeapons.Thales_RBE2_)
+
+ class Pylon8:
+ GBU_49 = (8, RafaleWeapons.GBU_49)
+ GBU_24 = (8, Weapons.GBU_24)
+ GBU12PII = (8, RafaleWeapons.GBU12PII)
+ MER_2_MK_82 = (8, Weapons.MER_2_MK_82)
+ _3_Mk_20_Rockeye = (8, Weapons._3_Mk_20_Rockeye)
+ _3_Mk_82 = (8, Weapons._3_Mk_82)
+ Mk_84 = (8, Weapons.Mk_84)
+ LAU3_HE151 = (8, Weapons.LAU3_HE151)
+ LAU3_WP156 = (8, Weapons.LAU3_WP156)
+ LAU_131x3_HYDRA_70_M151 = (8, Weapons.LAU_131x3_HYDRA_70_M151)
+ AS_30L = (8, RafaleWeapons.AS_30L)
+ AGM_88C_ = (8, Weapons.AGM_88C_)
+ RPL_711__ = (8, RafaleWeapons.RPL_711__)
+ RPL751__ = (8, RafaleWeapons.RPL751__)
+
+ class Pylon9:
+ AASM_250_R = (9, RafaleWeapons.AASM_250_R)
+ GBU_49 = (9, RafaleWeapons.GBU_49)
+ GBU_24 = (9, Weapons.GBU_24)
+ MER_2_MK_82 = (9, Weapons.MER_2_MK_82)
+ GBU12PII = (9, RafaleWeapons.GBU12PII)
+ _3_Mk_20_Rockeye = (9, Weapons._3_Mk_20_Rockeye)
+ Mk_84 = (9, Weapons.Mk_84)
+ _3_Mk_82 = (9, Weapons._3_Mk_82)
+ AGM_88C_ = (9, Weapons.AGM_88C_)
+ LAU_131___7_2_75__rockets_M151__HE_ = (9, Weapons.LAU_131___7_2_75__rockets_M151__HE_)
+ LAU3_HE151 = (9, Weapons.LAU3_HE151)
+ LAU3_WP156 = (9, Weapons.LAU3_WP156)
+ LAU3_HE5 = (9, Weapons.LAU3_HE5)
+ SCALP = (9, RafaleWeapons.SCALP)
+ AS_30L = (9, RafaleWeapons.AS_30L)
+
+ class Pylon10:
+ R_550_Magic_2 = (10, Weapons.R_550_Magic_2)
+ AIM_9M_Sidewinder_IR_AAM = (10, Weapons.AIM_9M_Sidewinder_IR_AAM)
+ AIM_9P_Sidewinder_IR_AAM = (10, Weapons.AIM_9P_Sidewinder_IR_AAM)
+ 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)
+ Smokewinder___orange = (10, Weapons.Smokewinder___orange)
+
+ pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
+
+ tasks = [task.CAP, task.Escort, task.FighterSweep, task.GroundAttack, task.CAS, task.AFAC, task.RunwayAttack, task.AntishipStrike, task.SEAD, task.PinpointStrike]
+ task_default = task.GroundAttack
+
+
+class Rafale_M_NOUNOU(PlaneType):
+ id = "Rafale_M_NOUNOU"
+ group_size_max = 1
+ height = 5.28
+ width = 10.13
+ length = 15.96
+ fuel_max = 4500
+ max_speed = 2001.996
+ chaff = 48
+ flare = 48
+ charge_total = 96
+ chaff_charge_size = 1
+ flare_charge_size = 1
+ tacan = True
+ category = "Tankers" #{8A302789-A55D-4897-B647-66493FA6826F}
+ radio_frequency = 127.5
+
+ class Liveries:
+
+ class USSR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Georgia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Venezuela(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Australia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Israel(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Combined_Joint_Task_Forces_Blue(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Sudan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Norway(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Romania(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Iran(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Ukraine(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Libya(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Belgium(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Slovakia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Greece(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class UK(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Third_Reich(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Hungary(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Abkhazia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Morocco(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class United_Nations_Peacekeepers(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Switzerland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class SouthOssetia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Vietnam(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class China(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Yemen(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Kuwait(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Serbia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Oman(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class India(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Egypt(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class TheNetherlands(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Poland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Syria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Finland(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Kazakhstan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Denmark(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Sweden(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Croatia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class CzechRepublic(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class GDR(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Yugoslavia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Bulgaria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class SouthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Tunisia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Combined_Joint_Task_Forces_Red(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Lebanon(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Portugal(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Cuba(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Insurgents(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class SaudiArabia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class France(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class USA(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Honduras(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Qatar(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Russia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class United_Arab_Emirates(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Italian_Social_Republi(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Austria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Bahrain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Italy(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Chile(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Turkey(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Philippines(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Algeria(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Pakistan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Malaysia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Indonesia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Iraq(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Germany(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class South_Africa(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Jordan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Mexico(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class USAFAggressors(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Brazil(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Spain(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Belarus(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Canada(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class NorthKorea(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Ethiopia(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Japan(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Thailand(Enum):
+ _01_marine_12_f = "01 marine 12 f"
+ _02_rafale_export = "02 rafale export"
+ _03_black_derive_11f = "03 black derive 11f"
+
+ class Pylon1:
+ MICA_IR = (1, Weapons.MICA_IR)
+ R_550_Magic_2 = (1, Weapons.R_550_Magic_2)
+
+ class Pylon3:
+ RPL_751 = (3, RafaleWeapons.RPL_751)
+ RPL_711___ = (3, RafaleWeapons.RPL_711___)
+
+ class Pylon8:
+ RPL_751 = (8, RafaleWeapons.RPL_751)
+ RPL_711___ = (8, RafaleWeapons.RPL_711___)
+
+ class Pylon10:
+ MICA_IR = (10, Weapons.MICA_IR)
+ R_550_Magic_2 = (10, Weapons.R_550_Magic_2)
+
+ class Pylon11:
+ Smokewinder___green = (11, Weapons.Smokewinder___green)
+ Smokewinder___blue = (11, Weapons.Smokewinder___blue)
+ Smokewinder___orange = (11, Weapons.Smokewinder___orange)
+ Smoke_Generator___red_ = (11, Weapons.Smoke_Generator___red_)
+ Smoke_Generator___blue_ = (11, Weapons.Smoke_Generator___blue_)
+ Smoke_Generator___white_ = (11, Weapons.Smoke_Generator___white_)
+
+ pylons = {1, 3, 8, 10, 11}
+
+ tasks = [task.Refueling]
+ task_default = task.Refueling
+
diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py
index 36ca6890..263bfb62 100644
--- a/qt_ui/dialogs.py
+++ b/qt_ui/dialogs.py
@@ -2,7 +2,7 @@
from typing import Optional
from gen.flights.flight import Flight
-from theater.missiontarget import MissionTarget
+from game.theater.missiontarget import MissionTarget
from .models import GameModel, PackageModel
from .windows.mission.QEditFlightDialog import QEditFlightDialog
from .windows.mission.QPackageDialog import (
diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py
index bec194fb..55dcb10b 100644
--- a/qt_ui/displayoptions.py
+++ b/qt_ui/displayoptions.py
@@ -20,8 +20,9 @@ class DisplayRule:
def value(self, value: bool) -> None:
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
self._value = value
- QLiberationMap.instance.reload_scene()
- QLiberationMap.instance.update()
+ if QLiberationMap.instance is not None:
+ QLiberationMap.instance.reload_scene()
+ QLiberationMap.instance.update()
def __bool__(self) -> bool:
return self.value
@@ -50,7 +51,6 @@ class DisplayOptions:
ground_objects = DisplayRule("Ground Objects", True)
control_points = DisplayRule("Control Points", True)
lines = DisplayRule("Lines", True)
- events = DisplayRule("Events", True)
sam_ranges = DisplayRule("Ally SAM Threat Range", False)
enemy_sam_ranges = DisplayRule("Enemy SAM Threat Range", True)
detection_range = DisplayRule("SAM Detection Range", False)
@@ -58,6 +58,7 @@ class DisplayOptions:
waypoint_info = DisplayRule("Waypoint Information", True)
culling = DisplayRule("Display Culling Zones", False)
flight_paths = FlightPathOptions()
+ actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
@classmethod
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
diff --git a/qt_ui/liberation_install.py b/qt_ui/liberation_install.py
index 0440043d..a8044363 100644
--- a/qt_ui/liberation_install.py
+++ b/qt_ui/liberation_install.py
@@ -8,6 +8,7 @@ from game import persistency
global __dcs_saved_game_directory
global __dcs_installation_directory
+global __last_save_file
PREFERENCES_FILE_PATH = "liberation_preferences.json"
@@ -15,6 +16,7 @@ PREFERENCES_FILE_PATH = "liberation_preferences.json"
def init():
global __dcs_saved_game_directory
global __dcs_installation_directory
+ global __last_save_file
if os.path.isfile(PREFERENCES_FILE_PATH):
try:
@@ -22,12 +24,18 @@ def init():
pref_data = json.loads(prefs.read())
__dcs_saved_game_directory = pref_data["saved_game_dir"]
__dcs_installation_directory = pref_data["dcs_install_dir"]
+ if "last_save_file" in pref_data:
+ __last_save_file = pref_data["last_save_file"]
+ else:
+ __last_save_file = ""
is_first_start = False
except:
__dcs_saved_game_directory = ""
__dcs_installation_directory = ""
+ __last_save_file = ""
is_first_start = True
else:
+ __last_save_file = ""
try:
__dcs_saved_game_directory = dcs.installation.get_dcs_saved_games_directory()
if os.path.exists(__dcs_saved_game_directory + ".openbeta"):
@@ -52,11 +60,18 @@ def setup(saved_game_dir, install_dir):
persistency.setup(__dcs_saved_game_directory)
+def setup_last_save_file(last_save_file):
+ global __last_save_file
+ __last_save_file = last_save_file
+
+
def save_config():
global __dcs_saved_game_directory
global __dcs_installation_directory
+ global __last_save_file
pref_data = {"saved_game_dir": __dcs_saved_game_directory,
- "dcs_install_dir": __dcs_installation_directory}
+ "dcs_install_dir": __dcs_installation_directory,
+ "last_save_file": __last_save_file}
with(open(PREFERENCES_FILE_PATH, "w")) as prefs:
prefs.write(json.dumps(pref_data))
@@ -71,6 +86,15 @@ def get_saved_game_dir():
return __dcs_saved_game_directory
+def get_last_save_file():
+ global __last_save_file
+ print(__last_save_file)
+ if os.path.exists(__last_save_file):
+ return __last_save_file
+ else:
+ return None
+
+
def replace_mission_scripting_file():
install_dir = get_dcs_install_directory()
mission_scripting_path = os.path.join(install_dir, "Scripts", "MissionScripting.lua")
diff --git a/qt_ui/main.py b/qt_ui/main.py
index de179f6b..09620098 100644
--- a/qt_ui/main.py
+++ b/qt_ui/main.py
@@ -1,13 +1,19 @@
+import argparse
import logging
import os
import sys
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
import dcs
from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
-from game import db, persistency, VERSION
+from game import Game, db, persistency, VERSION
+from game.settings import Settings
+from game.theater.start_generator import GameGenerator, GeneratorSettings
from qt_ui import (
liberation_install,
liberation_theme,
@@ -16,36 +22,32 @@ from qt_ui import (
)
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
+from qt_ui.windows.newgame.QCampaignList import Campaign
+from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
QLiberationFirstStartWindow
-# Logging setup
-logging_config.init_logging(VERSION)
-
-if __name__ == "__main__":
- # Load eagerly to catch errors early.
- db.FACTIONS.initialize()
+def run_ui(game: Optional[Game] = None) -> None:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
app = QApplication(sys.argv)
# init the theme and load the stylesheet based on the theme index
liberation_theme.init()
- css = ""
with open("./resources/stylesheets/"+liberation_theme.get_theme_css_file()) as stylesheet:
+ logging.info('Loading stylesheet: %s', liberation_theme.get_theme_css_file())
app.setStyleSheet(stylesheet.read())
# Inject custom payload in pydcs framework
custom_payloads = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..\\resources\\customized_payloads")
if os.path.exists(custom_payloads):
- dcs.planes.FlyingType.payload_dirs.append(custom_payloads)
+ dcs.unittype.FlyingType.payload_dirs.append(custom_payloads)
else:
# For release version the path is different.
custom_payloads = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"resources\\customized_payloads")
if os.path.exists(custom_payloads):
- dcs.planes.FlyingType.payload_dirs.append(custom_payloads)
-
+ dcs.unittype.FlyingType.payload_dirs.append(custom_payloads)
first_start = liberation_install.init()
if first_start:
@@ -79,7 +81,7 @@ if __name__ == "__main__":
GameUpdateSignal()
# Start window
- window = QLiberationWindow()
+ window = QLiberationWindow(game)
window.showMaximized()
splash.finish(window)
qt_execution_code = app.exec_()
@@ -91,3 +93,92 @@ if __name__ == "__main__":
logging.info("QT process exited with code : " + str(qt_execution_code))
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ subparsers = parser.add_subparsers(dest="subcommand")
+
+ def path_arg(arg: str) -> Path:
+ path = Path(arg)
+ if not path.exists():
+ raise argparse.ArgumentTypeError("path does not exist")
+ return path
+
+ new_game = subparsers.add_parser("new-game")
+
+ new_game.add_argument(
+ "campaign", type=path_arg,
+ help="Path to the campaign to start."
+ )
+
+ new_game.add_argument(
+ "--blue", default="USA 2005", help="Name of the blue faction."
+ )
+
+ new_game.add_argument(
+ "--red", default="Russia 1990", help="Name of the red faction."
+ )
+
+ new_game.add_argument(
+ "--supercarrier", action="store_true",
+ help="Use the supercarrier module."
+ )
+
+ new_game.add_argument(
+ "--auto-procurement", action="store_true",
+ help="Automate bluefor procurement."
+ )
+
+ new_game.add_argument(
+ "--inverted", action="store_true",
+ help="Invert the campaign."
+ )
+
+ return parser.parse_args()
+
+
+def create_game(campaign_path: Path, blue: str, red: str,
+ supercarrier: bool, auto_procurement: bool,
+ inverted: bool) -> Game:
+ campaign = Campaign.from_json(campaign_path)
+ generator = GameGenerator(
+ blue, red, campaign.load_theater(),
+ Settings(
+ supercarrier=supercarrier,
+ automate_runway_repair=auto_procurement,
+ automate_front_line_reinforcements=auto_procurement,
+ automate_aircraft_reinforcements=auto_procurement
+ ),
+ GeneratorSettings(
+ start_date=datetime.today(),
+ player_budget=DEFAULT_BUDGET,
+ enemy_budget=DEFAULT_BUDGET,
+ midgame=False,
+ inverted=inverted,
+ no_carrier=False,
+ no_lha=False,
+ no_player_navy=False,
+ no_enemy_navy=False
+ )
+ )
+ return generator.generate()
+
+
+def main():
+ logging_config.init_logging(VERSION)
+
+ # Load eagerly to catch errors early.
+ db.FACTIONS.initialize()
+
+ game: Optional[Game] = None
+
+ args = parse_args()
+ if args.subcommand == "new-game":
+ game = create_game(args.campaign, args.blue, args.red,
+ args.supercarrier, args.auto_procurement,
+ args.inverted)
+
+ run_ui(game)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/qt_ui/models.py b/qt_ui/models.py
index 07b990d6..bd0d6397 100644
--- a/qt_ui/models.py
+++ b/qt_ui/models.py
@@ -16,7 +16,7 @@ from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS
-from theater.missiontarget import MissionTarget
+from game.theater.missiontarget import MissionTarget
class DeletableChildModelManager:
@@ -121,14 +121,11 @@ class PackageModel(QAbstractListModel):
def text_for_flight(self, flight: Flight) -> str:
"""Returns the text that should be displayed for the flight."""
- task = flight.flight_type.name
- count = flight.count
- name = db.unit_type_name(flight.unit_type)
estimator = TotEstimator(self.package)
delay = datetime.timedelta(
seconds=int(estimator.mission_start_time(flight).total_seconds()))
origin = flight.from_cp.name
- return f"[{task}] {count} x {name} from {origin} in {delay}"
+ return f"{flight} from {origin} in {delay}"
@staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
@@ -277,10 +274,14 @@ class GameModel:
This isn't a real Qt data model, but simplifies management of the game and
its ATO objects.
"""
- def __init__(self) -> None:
- self.game: Optional[Game] = None
- self.ato_model = AtoModel(self.game, AirTaskingOrder())
- self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
+ def __init__(self, game: Optional[Game]) -> None:
+ self.game: Optional[Game] = game
+ if self.game is None:
+ self.ato_model = AtoModel(self.game, AirTaskingOrder())
+ self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
+ else:
+ self.ato_model = AtoModel(self.game, self.game.blue_ato)
+ self.red_ato_model = AtoModel(self.game, self.game.red_ato)
def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object.
diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py
index b256705d..0b5b4725 100644
--- a/qt_ui/uiconstants.py
+++ b/qt_ui/uiconstants.py
@@ -1,10 +1,9 @@
import os
from typing import Dict
-from pathlib import Path
from PySide2.QtGui import QColor, QFont, QPixmap
-from theater.theatergroundobject import CATEGORY_MAP
+from game.theater.theatergroundobject import CATEGORY_MAP
from .liberation_theme import get_theme_icons
@@ -29,7 +28,6 @@ FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False)
COLORS: Dict[str, QColor] = {
"white": QColor(255, 255, 255),
"white_transparent": QColor(255, 255, 255, 35),
- "grey_transparent": QColor(150, 150, 150, 30),
"light_red": QColor(231, 92, 83, 90),
"red": QColor(200, 80, 80),
@@ -41,6 +39,7 @@ COLORS: Dict[str, QColor] = {
"blue": QColor(0, 132, 255),
"dark_blue": QColor(45, 62, 80),
"sea_blue": QColor(52, 68, 85),
+ "sea_blue_transparent": QColor(52, 68, 85, 150),
"blue_transparent": QColor(0, 132, 255, 20),
"purple": QColor(187, 137, 255),
@@ -63,8 +62,11 @@ COLORS: Dict[str, QColor] = {
"dawn_dust_overlay": QColor(46, 38, 85),
"grey": QColor(150, 150, 150),
+ "grey_transparent": QColor(150, 150, 150, 150),
"dark_grey": QColor(75, 75, 75),
+ "dark_grey_transparent": QColor(75, 75, 75, 150),
"dark_dark_grey": QColor(48, 48, 48),
+ "dark_dark_grey_transparent": QColor(48, 48, 48, 150),
}
@@ -79,6 +81,24 @@ def load_icons():
ICONS["New"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/new.png")
ICONS["Open"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/open.png")
ICONS["Save"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/save.png")
+ ICONS["Discord"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/discord.png")
+ ICONS["Github"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/github.png")
+
+
+ ICONS["Control Points"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/circle.png")
+ ICONS["Ground Objects"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/industry.png")
+ ICONS["Lines"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/arrows-h.png")
+ ICONS["Waypoint Information"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/info.png")
+ ICONS["Map Polygon Debug Mode"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/map.png")
+ ICONS["Ally SAM Threat Range"] = QPixmap("./resources/ui/misc/blue-sam.png")
+ ICONS["Enemy SAM Threat Range"] = QPixmap("./resources/ui/misc/red-sam.png")
+ ICONS["SAM Detection Range"] = QPixmap("./resources/ui/misc/detection-sam.png")
+ ICONS["Display Culling Zones"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/eraser.png")
+ ICONS["Hide Flight Paths"] = QPixmap("./resources/ui/misc/hide-flight-path.png")
+ ICONS["Show Selected Flight Path"] = QPixmap("./resources/ui/misc/flight-path.png")
+ ICONS["Show All Flight Paths"] = QPixmap("./resources/ui/misc/all-flight-paths.png")
+
+
ICONS["Hangar"] = QPixmap("./resources/ui/misc/hangar.png")
ICONS["Terrain_Caucasus"] = QPixmap("./resources/ui/terrain_caucasus.gif")
@@ -88,10 +108,10 @@ def load_icons():
ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif")
ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif")
- ICONS["Dawn"] = QPixmap("./resources/ui/daytime/dawn.png")
- ICONS["Day"] = QPixmap("./resources/ui/daytime/day.png")
- ICONS["Dusk"] = QPixmap("./resources/ui/daytime/dusk.png")
- ICONS["Night"] = QPixmap("./resources/ui/daytime/night.png")
+ ICONS["Dawn"] = QPixmap("./resources/ui/conditions/timeofday/dawn.png")
+ ICONS["Day"] = QPixmap("./resources/ui/conditions/timeofday/day.png")
+ ICONS["Dusk"] = QPixmap("./resources/ui/conditions/timeofday/dusk.png")
+ ICONS["Night"] = QPixmap("./resources/ui/conditions/timeofday/night.png")
ICONS["Money"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/money_icon.png")
ICONS["PassTurn"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/hourglass.png")
@@ -108,6 +128,8 @@ def load_icons():
ICONS["destroyed"] = QPixmap("./resources/ui/ground_assets/destroyed.png")
ICONS["ship"] = QPixmap("./resources/ui/ground_assets/ship.png")
ICONS["ship_blue"] = QPixmap("./resources/ui/ground_assets/ship_blue.png")
+ ICONS["missile"] = QPixmap("./resources/ui/ground_assets/missile.png")
+ ICONS["missile_blue"] = QPixmap("./resources/ui/ground_assets/missile_blue.png")
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
@@ -120,6 +142,25 @@ def load_icons():
ICONS["TaskSEAD"] = QPixmap("./resources/ui/tasks/sead.png")
ICONS["TaskEmpty"] = QPixmap("./resources/ui/tasks/empty.png")
+ """
+ Weather Icons
+ """
+ ICONS["Weather_winds"] = QPixmap("./resources/ui/conditions/weather/winds.png")
+ ICONS["Weather_day-clear"] = QPixmap("./resources/ui/conditions/weather/day-clear.png")
+ ICONS["Weather_day-cloudy-fog"] = QPixmap("./resources/ui/conditions/weather/day-cloudy-fog.png")
+ ICONS["Weather_day-fog"] = QPixmap("./resources/ui/conditions/weather/day-fog.png")
+ ICONS["Weather_day-partly-cloudy"] = QPixmap("./resources/ui/conditions/weather/day-partly-cloudy.png")
+ ICONS["Weather_day-rain"] = QPixmap("./resources/ui/conditions/weather/day-rain.png")
+ ICONS["Weather_day-thunderstorm"] = QPixmap("./resources/ui/conditions/weather/day-thunderstorm.png")
+ ICONS["Weather_day-totally-cloud"] = QPixmap("./resources/ui/conditions/weather/day-totally-cloud.png")
+ ICONS["Weather_night-clear"] = QPixmap("./resources/ui/conditions/weather/night-clear.png")
+ ICONS["Weather_night-cloudy-fog"] = QPixmap("./resources/ui/conditions/weather/night-cloudy-fog.png")
+ ICONS["Weather_night-fog"] = QPixmap("./resources/ui/conditions/weather/night-fog.png")
+ ICONS["Weather_night-partly-cloudy"] = QPixmap("./resources/ui/conditions/weather/night-partly-cloudy.png")
+ ICONS["Weather_night-rain"] = QPixmap("./resources/ui/conditions/weather/night-rain.png")
+ ICONS["Weather_night-thunderstorm"] = QPixmap("./resources/ui/conditions/weather/night-thunderstorm.png")
+ ICONS["Weather_night-totally-cloud"] = QPixmap("./resources/ui/conditions/weather/night-totally-cloud.png")
+
EVENT_ICONS: Dict[str, QPixmap] = {}
diff --git a/qt_ui/widgets/QBudgetBox.py b/qt_ui/widgets/QBudgetBox.py
index 27233559..ad1d66a4 100644
--- a/qt_ui/widgets/QBudgetBox.py
+++ b/qt_ui/widgets/QBudgetBox.py
@@ -18,6 +18,7 @@ class QBudgetBox(QGroupBox):
self.money_amount = QLabel()
self.finances = QPushButton("Details")
+ self.finances.setDisabled(True)
self.finances.setProperty("style", "btn-primary")
self.finances.clicked.connect(self.openFinances)
@@ -36,8 +37,12 @@ class QBudgetBox(QGroupBox):
self.money_amount.setText(str(budget) + "M (+" + str(reward) + "M)")
def setGame(self, game):
+ if game is None:
+ return
+
self.game = game
self.setBudget(self.game.budget, self.game.budget_reward_amount)
+ self.finances.setEnabled(True)
def openFinances(self):
self.subwindow = QFinancesMenu(self.game)
diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py
new file mode 100644
index 00000000..6116586b
--- /dev/null
+++ b/qt_ui/widgets/QConditionsWidget.py
@@ -0,0 +1,269 @@
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout, QFrame, QGridLayout
+from PySide2.QtGui import QPixmap
+
+from game.weather import Conditions, TimeOfDay, Weather
+from game.utils import meter_to_nm, mps_to_knots
+from dcs.weather import Weather as PydcsWeather
+
+import qt_ui.uiconstants as CONST
+
+class QTimeTurnWidget(QGroupBox):
+ """
+ UI Component to display current turn and time info
+ """
+
+ def __init__(self):
+ super(QTimeTurnWidget, self).__init__("Turn")
+ self.setStyleSheet('padding: 0px; margin-left: 5px; margin-right: 0px; margin-top: 1ex; margin-bottom: 5px; border-right: 0px')
+
+ self.icons = {
+ TimeOfDay.Dawn: CONST.ICONS["Dawn"],
+ TimeOfDay.Day: CONST.ICONS["Day"],
+ TimeOfDay.Dusk: CONST.ICONS["Dusk"],
+ TimeOfDay.Night: CONST.ICONS["Night"],
+ }
+
+ # self.setProperty('style', 'conditions__widget--turn')
+ self.layout = QHBoxLayout()
+ self.setLayout(self.layout)
+
+ self.daytime_icon = QLabel()
+ self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
+ self.layout.addWidget(self.daytime_icon)
+
+ self.time_column = QVBoxLayout()
+ self.layout.addLayout(self.time_column)
+
+ self.date_display = QLabel()
+ self.time_column.addWidget(self.date_display)
+
+ self.time_display = QLabel()
+ self.time_column.addWidget(self.time_display)
+
+ def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
+ """Sets the turn information display.
+
+ :arg turn Current turn number.
+ :arg conditions Current time and weather conditions.
+ """
+ self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
+ self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
+ self.time_display.setText(
+ conditions.start_time.strftime("%H:%M:%S Local"))
+ self.setTitle(f"Turn {turn}")
+
+class QWeatherWidget(QGroupBox):
+ """
+ UI Component to display current weather forecast
+ """
+ turn = None
+ conditions = None
+
+ def __init__(self):
+ super(QWeatherWidget, self).__init__("")
+ self.setProperty('style', 'QWeatherWidget')
+
+ self.icons = {
+ TimeOfDay.Dawn: CONST.ICONS["Dawn"],
+ TimeOfDay.Day: CONST.ICONS["Day"],
+ TimeOfDay.Dusk: CONST.ICONS["Dusk"],
+ TimeOfDay.Night: CONST.ICONS["Night"],
+ }
+
+ self.layout = QHBoxLayout()
+ self.setLayout(self.layout)
+
+ self.makeWeatherIcon()
+ self.makeCloudRainFogWidget()
+ self.makeWindsWidget()
+
+ def makeWeatherIcon(self):
+ """Makes the Weather Icon Widget
+ """
+ self.weather_icon = QLabel()
+ self.weather_icon.setPixmap(self.icons[TimeOfDay.Dawn])
+ self.layout.addWidget(self.weather_icon)
+
+ def makeCloudRainFogWidget(self):
+ """Makes the Cloud, Rain, Fog Widget
+ """
+ self.textLayout = QVBoxLayout()
+ self.layout.addLayout(self.textLayout)
+
+ self.forecastClouds = self.makeLabel()
+ self.textLayout.addWidget(self.forecastClouds)
+
+ self.forecastRain = self.makeLabel()
+ self.textLayout.addWidget(self.forecastRain)
+
+ self.forecastFog = self.makeLabel()
+ self.textLayout.addWidget(self.forecastFog)
+
+ def makeWindsWidget(self):
+ """Factory for the winds widget.
+ """
+ windsLayout = QGridLayout()
+ self.layout.addLayout(windsLayout)
+
+ windsLayout.addWidget(self.makeIcon(CONST.ICONS['Weather_winds']), 0, 0, 3, 1)
+
+ windsLayout.addWidget(self.makeLabel('At GL'), 0, 1)
+ windsLayout.addWidget(self.makeLabel('At FL08'), 1, 1)
+ windsLayout.addWidget(self.makeLabel('At FL26'), 2, 1)
+
+ self.windGLSpeedLabel = self.makeLabel('0kts')
+ self.windGLDirLabel = self.makeLabel('0º')
+ windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
+ windsLayout.addWidget(self.windGLDirLabel, 0, 3)
+
+
+ self.windFL08SpeedLabel = self.makeLabel('0kts')
+ self.windFL08DirLabel = self.makeLabel('0º')
+ windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
+ windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
+
+ self.windFL26SpeedLabel = self.makeLabel('0kts')
+ self.windFL26DirLabel = self.makeLabel('0º')
+ windsLayout.addWidget(self.windFL26SpeedLabel, 2, 2)
+ windsLayout.addWidget(self.windFL26DirLabel, 2, 3)
+
+ def makeLabel(self, text: str = '') -> QLabel:
+ """Shorthand to generate a QLabel with widget standard style
+
+ :arg pixmap QPixmap for the icon.
+ """
+ label = QLabel(text)
+ label.setProperty('style', 'text-sm')
+
+ return label
+
+ def makeIcon(self, pixmap: QPixmap) -> QLabel:
+ """Shorthand to generate a QIcon with pixmap.
+
+ :arg pixmap QPixmap for the icon.
+ """
+ icon = QLabel()
+ icon.setPixmap(pixmap)
+
+ return icon
+
+ def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
+ """Sets the turn information display.
+
+ :arg turn Current turn number.
+ :arg conditions Current time and weather conditions.
+ """
+ self.turn = turn
+ self.conditions = conditions
+
+ self.updateForecast()
+ self.updateWinds()
+
+ def updateWinds(self):
+ """Updates the UI with the current conditions wind info.
+ """
+ windGlSpeed = mps_to_knots(self.conditions.weather.wind.at_0m.speed or 0)
+ windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, '0')
+ self.windGLSpeedLabel.setText('{}kts'.format(windGlSpeed))
+ self.windGLDirLabel.setText('{}º'.format(windGlDir))
+
+ windFL08Speed = mps_to_knots(self.conditions.weather.wind.at_2000m.speed or 0)
+ windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(3, '0')
+ self.windFL08SpeedLabel.setText('{}kts'.format(windFL08Speed))
+ self.windFL08DirLabel.setText('{}º'.format(windFL08Dir))
+
+ windFL26Speed = mps_to_knots(self.conditions.weather.wind.at_8000m.speed or 0)
+ windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(3, '0')
+ self.windFL26SpeedLabel.setText('{}kts'.format(windFL26Speed))
+ self.windFL26DirLabel.setText('{}º'.format(windFL26Dir))
+
+ def updateForecast(self):
+ """Updates the Forecast Text and icon with the current conditions wind info.
+ """
+ icon = []
+ if self.conditions.weather.clouds is None:
+ cloudDensity = 0
+ precipitation = None
+ else:
+ cloudDensity = self.conditions.weather.clouds.density
+ precipitation = self.conditions.weather.clouds.precipitation
+
+ fog = self.conditions.weather.fog or None
+ is_night = self.conditions.time_of_day == TimeOfDay.Night
+ time = 'night' if is_night else 'day'
+
+ if cloudDensity <= 0:
+ self.forecastClouds.setText('Sunny')
+ icon = [time, 'clear']
+
+ if cloudDensity > 0 and cloudDensity < 3:
+ self.forecastClouds.setText('Partly Cloudy')
+ icon = [time, 'partly-cloudy']
+
+ if cloudDensity >= 3 and cloudDensity < 5:
+ self.forecastClouds.setText('Mostly Cloudy')
+ icon = [time, 'partly-cloudy']
+
+ if cloudDensity >= 5:
+ self.forecastClouds.setText('Totally Cloudy')
+ icon = [time, 'partly-cloudy']
+
+ if precipitation == PydcsWeather.Preceptions.Rain:
+ self.forecastRain.setText('Rain')
+ icon = [time, 'rain']
+
+ elif precipitation == PydcsWeather.Preceptions.Thunderstorm:
+ self.forecastRain.setText('Thunderstorm')
+ icon = [time, 'thunderstorm']
+
+ else:
+ self.forecastRain.setText('No Rain')
+
+ if not fog:
+ self.forecastFog.setText('No fog')
+ else:
+ visvibilityNm = round(meter_to_nm(fog.visibility), 1)
+ self.forecastFog.setText('Fog vis: {}nm'.format(visvibilityNm))
+ icon = [time, ('cloudy' if cloudDensity > 1 else None), 'fog']
+
+
+ icon_key = "Weather_{}".format('-'.join(filter(None.__ne__, icon)))
+ icon = CONST.ICONS.get(icon_key) or CONST.ICONS['Weather_night-partly-cloudy']
+ self.weather_icon.setPixmap(icon)
+
+
+class QConditionsWidget(QFrame):
+ """
+ UI Component to display Turn Number, Day Time & Hour and weather combined.
+ """
+
+ def __init__(self):
+ super(QConditionsWidget, self).__init__()
+ self.setProperty('style', 'QConditionsWidget')
+
+ self.layout = QGridLayout()
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.setHorizontalSpacing(0)
+ self.layout.setVerticalSpacing(0)
+ self.setLayout(self.layout)
+
+ self.time_turn_widget = QTimeTurnWidget()
+ self.time_turn_widget.setStyleSheet('QGroupBox { margin-right: 0px; }')
+ self.layout.addWidget(self.time_turn_widget, 0, 0)
+
+ self.weather_widget = QWeatherWidget()
+ self.weather_widget.setStyleSheet('QGroupBox { margin-top: 5px; margin-left: 0px; border-left: 0px; }')
+ self.weather_widget.hide()
+ self.layout.addWidget(self.weather_widget, 0, 1)
+
+ def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
+ """Sets the turn information display.
+
+ :arg turn Current turn number.
+ :arg conditions Current time and weather conditions.
+ """
+ self.time_turn_widget.setCurrentTurn(turn, conditions)
+ self.weather_widget.setCurrentTurn(turn, conditions)
+ self.weather_widget.show()
+
diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py
index 79f4e77e..8fe1c347 100644
--- a/qt_ui/widgets/QTopPanel.py
+++ b/qt_ui/widgets/QTopPanel.py
@@ -10,19 +10,19 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST
from game import Game
-from game.event import CAP, CAS, FrontlineAttackEvent
+from game.event.airwar import AirWarEvent
from gen.ato import Package
from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
-from qt_ui.widgets.QTurnCounter import QTurnCounter
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QWaitingForMissionResultWindow import \
QWaitingForMissionResultWindow
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
+from qt_ui.widgets.QConditionsWidget import QConditionsWidget
class QTopPanel(QFrame):
@@ -39,31 +39,34 @@ class QTopPanel(QFrame):
def game(self) -> Optional[Game]:
return self.game_model.game
- def init_ui(self):
-
- self.turnCounter = QTurnCounter()
+ def init_ui(self):
+ self.conditionsWidget = QConditionsWidget()
self.budgetBox = QBudgetBox(self.game)
self.passTurnButton = QPushButton("Pass Turn")
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
self.passTurnButton.setProperty("style", "btn-primary")
self.passTurnButton.clicked.connect(self.passTurn)
+ if not self.game:
+ self.passTurnButton.setEnabled(False)
self.proceedButton = QPushButton("Take off")
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
self.proceedButton.setProperty("style", "start-button")
self.proceedButton.clicked.connect(self.launch_mission)
- if self.game and self.game.turn == 0:
+ if not self.game or self.game.turn == 0:
self.proceedButton.setEnabled(False)
self.factionsInfos = QFactionsInfos(self.game)
self.settings = QPushButton("Settings")
+ self.settings.setDisabled(True)
self.settings.setIcon(CONST.ICONS["Settings"])
self.settings.setProperty("style", "btn-primary")
self.settings.clicked.connect(self.openSettings)
self.statistics = QPushButton("Statistics")
+ self.statistics.setDisabled(True)
self.statistics.setIcon(CONST.ICONS["Statistics"])
self.statistics.setProperty("style", "btn-primary")
self.statistics.clicked.connect(self.openStatisticsWindow)
@@ -83,24 +86,31 @@ class QTopPanel(QFrame):
self.proceedBox.setLayout(self.proceedBoxLayout)
self.layout = QHBoxLayout()
+
self.layout.addWidget(self.factionsInfos)
- self.layout.addWidget(self.turnCounter)
+ self.layout.addWidget(self.conditionsWidget)
self.layout.addWidget(self.budgetBox)
self.layout.addWidget(self.buttonBox)
self.layout.addStretch(1)
self.layout.addWidget(self.proceedBox)
self.layout.setContentsMargins(0,0,0,0)
+
self.setLayout(self.layout)
def setGame(self, game: Optional[Game]):
if game is None:
return
- self.turnCounter.setCurrentTurn(game.turn, game.conditions)
+ self.settings.setEnabled(True)
+ self.statistics.setEnabled(True)
+
+ self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
+ self.passTurnButton.setEnabled(True)
+
if game and game.turn == 0:
self.proceedButton.setEnabled(False)
else:
@@ -167,7 +177,7 @@ class QTopPanel(QFrame):
def confirm_negative_start_time(self,
negative_starts: List[Package]) -> bool:
formatted = '
'.join(
- [f"{p.primary_task.name} {p.target.name}" for p in negative_starts]
+ [f"{p.primary_task} {p.target.name}" for p in negative_starts]
)
mbox = QMessageBox(
QMessageBox.Question,
@@ -210,29 +220,18 @@ class QTopPanel(QFrame):
if negative_starts:
if not self.confirm_negative_start_time(negative_starts):
return
+ closest_cps = self.game.theater.closest_opposing_control_points()
+ game_event = AirWarEvent(
+ self.game,
+ closest_cps[0],
+ closest_cps[1],
+ self.game.theater.controlpoints[0].position,
+ self.game.player_name,
+ self.game.enemy_name)
- # TODO: Refactor this nonsense.
- game_event = None
- for event in self.game.events:
- if isinstance(event,
- FrontlineAttackEvent) and event.is_player_attacking:
- game_event = event
- if game_event is None:
- game_event = FrontlineAttackEvent(
- self.game,
- self.game.theater.controlpoints[0],
- self.game.theater.controlpoints[0],
- self.game.theater.controlpoints[0].position,
- self.game.player_name,
- self.game.enemy_name)
- game_event.is_awacs_enabled = True
- game_event.ca_slots = 1
- game_event.departure_cp = self.game.theater.controlpoints[0]
- game_event.player_attacking({CAS: {}, CAP: {}})
- game_event.depart_from = self.game.theater.controlpoints[0]
-
- self.game.initiate_event(game_event)
- waiting = QWaitingForMissionResultWindow(game_event, self.game)
+ unit_map = self.game.initiate_event(game_event)
+ waiting = QWaitingForMissionResultWindow(game_event, self.game,
+ unit_map)
waiting.show()
def budget_update(self, game:Game):
diff --git a/qt_ui/widgets/QTurnCounter.py b/qt_ui/widgets/QTurnCounter.py
deleted file mode 100644
index a26112e1..00000000
--- a/qt_ui/widgets/QTurnCounter.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import datetime
-
-from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
-
-from game.weather import Conditions, TimeOfDay
-import qt_ui.uiconstants as CONST
-
-
-class QTurnCounter(QGroupBox):
- """
- UI Component to display current turn and time info
- """
-
- def __init__(self):
- super(QTurnCounter, self).__init__("Turn")
-
- self.icons = {
- TimeOfDay.Dawn: CONST.ICONS["Dawn"],
- TimeOfDay.Day: CONST.ICONS["Day"],
- TimeOfDay.Dusk: CONST.ICONS["Dusk"],
- TimeOfDay.Night: CONST.ICONS["Night"],
- }
-
- self.layout = QHBoxLayout()
- self.setLayout(self.layout)
-
- self.daytime_icon = QLabel()
- self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
- self.layout.addWidget(self.daytime_icon)
-
- self.time_column = QVBoxLayout()
- self.layout.addLayout(self.time_column)
-
- self.date_display = QLabel()
- self.time_column.addWidget(self.date_display)
-
- self.time_display = QLabel()
- self.time_column.addWidget(self.time_display)
-
- def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
- """Sets the turn information display.
-
- :arg turn Current turn number.
- :arg conditions Current time and weather conditions.
- """
- self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
- self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
- self.time_display.setText(
- conditions.start_time.strftime("%H:%M:%S Local"))
- self.setTitle("Turn " + str(turn + 1))
diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py
index 295c4ee9..c4c38e22 100644
--- a/qt_ui/widgets/ato.py
+++ b/qt_ui/widgets/ato.py
@@ -60,7 +60,7 @@ class FlightDelegate(QStyledItemDelegate):
def first_row_text(self, index: QModelIndex) -> str:
flight = self.flight(index)
- task = flight.flight_type.name
+ task = flight.flight_type
count = flight.count
name = db.unit_type_name(flight.unit_type)
estimator = TotEstimator(self.package)
diff --git a/qt_ui/widgets/base/QAirportInformation.py b/qt_ui/widgets/base/QAirportInformation.py
deleted file mode 100644
index 4fc1474c..00000000
--- a/qt_ui/widgets/base/QAirportInformation.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QLCDNumber
-
-from theater import ControlPoint, Airport
-
-
-class QAirportInformation(QGroupBox):
-
- def __init__(self, cp:ControlPoint, airport:Airport):
- super(QAirportInformation, self).__init__(airport.name)
- self.cp = cp
- self.airport = airport
- self.init_ui()
-
- def init_ui(self):
- self.layout = QGridLayout()
-
- # Runway information
- self.runways = QGroupBox("Runways")
- self.runwayLayout = QGridLayout()
- for i, runway in enumerate(self.airport.runways):
-
- # Seems like info is missing in pydcs, even if the attribute is there
- lr = ""
- if runway.leftright == 1:
- lr = "L"
- elif runway.leftright == 2:
- lr = "R"
-
- self.runwayLayout.addWidget(QLabel("Runway " + str(runway.heading) + lr), i, 0)
-
- # Seems like info is missing in pydcs, even if the attribute is there
- if runway.ils:
- self.runwayLayout.addWidget(QLabel("ILS "), i, 1)
- self.runwayLayout.addWidget(QLCDNumber(6, runway.ils), i, 1)
- else:
- self.runwayLayout.addWidget(QLabel("NO ILS"), i, 1)
-
-
- self.runways.setLayout(self.runwayLayout)
- self.layout.addWidget(self.runways, 0, 0)
-
- self.layout.addWidget(QLabel("Parking Slots :"), 1, 0)
- self.layout.addWidget(QLabel(str(len(self.airport.parking_slots))), 1, 1)
-
-
- stretch = QVBoxLayout()
- stretch.addStretch()
-
- self.layout.addLayout(stretch, 2, 0)
- self.setLayout(self.layout)
-
-
diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py
index 1f490e4d..f31c611d 100644
--- a/qt_ui/widgets/combos/QAircraftTypeSelector.py
+++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py
@@ -1,15 +1,15 @@
"""Combo box for selecting aircraft types."""
-from typing import Iterable
+from typing import Iterable, Type
from PySide2.QtWidgets import QComboBox
-from dcs.planes import PlaneType
+from dcs.unittype import FlyingType
class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
- def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
+ def __init__(self, aircraft_types: Iterable[Type[FlyingType]]) -> None:
super().__init__()
for aircraft in aircraft_types:
self.addItem(f"{aircraft.id}", userData=aircraft)
diff --git a/qt_ui/widgets/combos/QArrivalAirfieldSelector.py b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py
new file mode 100644
index 00000000..22097b34
--- /dev/null
+++ b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py
@@ -0,0 +1,39 @@
+"""Combo box for selecting a departure airfield."""
+from typing import Iterable, Type
+
+from PySide2.QtWidgets import QComboBox
+from dcs.unittype import FlyingType
+
+from game.theater.controlpoint import ControlPoint
+
+
+class QArrivalAirfieldSelector(QComboBox):
+ """A combo box for selecting a flight's arrival or divert airfield.
+
+ The combo box will automatically be populated with all airfields the given
+ aircraft type is able to land at.
+ """
+
+ def __init__(self, destinations: Iterable[ControlPoint],
+ aircraft: Type[FlyingType], optional_text: str) -> None:
+ super().__init__()
+ self.destinations = list(destinations)
+ self.aircraft = aircraft
+ self.optional_text = optional_text
+ self.rebuild_selector()
+ self.setCurrentIndex(0)
+
+ def change_aircraft(self, aircraft: FlyingType) -> None:
+ if self.aircraft == aircraft:
+ return
+ self.aircraft = aircraft
+ self.rebuild_selector()
+
+ def rebuild_selector(self) -> None:
+ self.clear()
+ for destination in self.destinations:
+ if destination.can_operate(self.aircraft):
+ self.addItem(destination.name, destination)
+ self.model().sort(0)
+ self.insertItem(0, self.optional_text, None)
+ self.setCurrentIndex(0)
diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py
index d1a27382..df47f56d 100644
--- a/qt_ui/widgets/combos/QFlightTypeComboBox.py
+++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py
@@ -1,108 +1,16 @@
"""Combo box for selecting a flight's task type."""
-import logging
-from typing import Iterator
from PySide2.QtWidgets import QComboBox
-from gen.flights.flight import FlightType
-from theater import (
- ConflictTheater,
- ControlPoint,
- FrontLine,
- MissionTarget,
- TheaterGroundObject,
-)
+from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type."""
- COMMON_ENEMY_MISSIONS = [
- FlightType.ESCORT,
- FlightType.SEAD,
- FlightType.DEAD,
- # TODO: FlightType.ELINT,
- # TODO: FlightType.EWAR,
- # TODO: FlightType.RECON,
- ]
-
- COMMON_FRIENDLY_MISSIONS = [
- FlightType.BARCAP,
- ]
-
- FRIENDLY_AIRBASE_MISSIONS = [
- # TODO: FlightType.INTERCEPTION
- # TODO: FlightType.LOGISTICS
- ] + COMMON_FRIENDLY_MISSIONS
-
- FRIENDLY_CARRIER_MISSIONS = [
- # TODO: FlightType.INTERCEPTION
- # TODO: Buddy tanking for the A-4?
- # TODO: Rescue chopper?
- # TODO: Inter-ship logistics?
- ] + COMMON_FRIENDLY_MISSIONS
-
- ENEMY_CARRIER_MISSIONS = [
- FlightType.ESCORT,
- FlightType.BARCAP,
- # TODO: FlightType.ANTISHIP
- ]
-
- ENEMY_AIRBASE_MISSIONS = [
- FlightType.BARCAP,
- # TODO: FlightType.STRIKE
- ] + COMMON_ENEMY_MISSIONS
-
- FRIENDLY_GROUND_OBJECT_MISSIONS = [
- # TODO: FlightType.LOGISTICS
- # TODO: FlightType.TROOP_TRANSPORT
- ] + COMMON_FRIENDLY_MISSIONS
-
- ENEMY_GROUND_OBJECT_MISSIONS = [
- FlightType.BARCAP,
- FlightType.STRIKE,
- ] + COMMON_ENEMY_MISSIONS
-
- FRONT_LINE_MISSIONS = [
- FlightType.CAS,
- FlightType.TARCAP,
- # TODO: FlightType.TROOP_TRANSPORT
- # TODO: FlightType.EVAC
- ] + COMMON_ENEMY_MISSIONS
-
- # TODO: Add BAI missions after we have useful BAI targets.
-
def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None:
super().__init__()
self.theater = theater
self.target = target
- for mission_type in self.mission_types_for_target():
- self.addItem(mission_type.name, userData=mission_type)
-
- def mission_types_for_target(self) -> Iterator[FlightType]:
- if isinstance(self.target, ControlPoint):
- friendly = self.target.captured
- fleet = self.target.is_fleet
- if friendly:
- if fleet:
- yield from self.FRIENDLY_CARRIER_MISSIONS
- else:
- yield from self.FRIENDLY_AIRBASE_MISSIONS
- else:
- if fleet:
- yield from self.ENEMY_CARRIER_MISSIONS
- else:
- yield from self.ENEMY_AIRBASE_MISSIONS
- elif isinstance(self.target, TheaterGroundObject):
- # TODO: Filter more based on the category.
- friendly = self.target.control_point.captured
- if friendly:
- yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
- else:
- yield from self.ENEMY_GROUND_OBJECT_MISSIONS
- elif isinstance(self.target, FrontLine):
- yield from self.FRONT_LINE_MISSIONS
- else:
- logging.error(
- f"Unhandled target type: {self.target.__class__.__name__}"
- )
+ for mission_type in self.target.mission_types(for_player=True):
+ self.addItem(str(mission_type), userData=mission_type)
diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
index eab48e3a..5a91a74d 100644
--- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py
+++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py
@@ -1,12 +1,12 @@
"""Combo box for selecting a departure airfield."""
-from typing import Iterable
+from typing import Iterable, Type
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QComboBox
-from dcs.planes import PlaneType
+from dcs.unittype import FlyingType
from game.inventory import GlobalAircraftInventory
-from theater.controlpoint import ControlPoint
+from game.theater.controlpoint import ControlPoint
class QOriginAirfieldSelector(QComboBox):
@@ -20,7 +20,7 @@ class QOriginAirfieldSelector(QComboBox):
def __init__(self, global_inventory: GlobalAircraftInventory,
origins: Iterable[ControlPoint],
- aircraft: PlaneType) -> None:
+ aircraft: Type[FlyingType]) -> None:
super().__init__()
self.global_inventory = global_inventory
self.origins = list(origins)
@@ -28,7 +28,7 @@ class QOriginAirfieldSelector(QComboBox):
self.rebuild_selector()
self.currentIndexChanged.connect(self.index_changed)
- def change_aircraft(self, aircraft: PlaneType) -> None:
+ def change_aircraft(self, aircraft: FlyingType) -> None:
if self.aircraft == aircraft:
return
self.aircraft = aircraft
@@ -37,12 +37,14 @@ class QOriginAirfieldSelector(QComboBox):
def rebuild_selector(self) -> None:
self.clear()
for origin in self.origins:
+ if not origin.can_operate(self.aircraft):
+ continue
+
inventory = self.global_inventory.for_control_point(origin)
available = inventory.available(self.aircraft)
if available:
self.addItem(f"{origin.name} ({available} available)", origin)
self.model().sort(0)
- self.update()
@property
def available(self) -> int:
diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
index 2a9f8bc9..8f40afde 100644
--- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
+++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
@@ -1,10 +1,10 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
+from game.theater import ControlPointType
from gen import BuildingGroundObject, Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
-from theater import ControlPointType
class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
@@ -54,7 +54,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if cp.captured:
enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured]
for ecp in enemy_cp:
- pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0]
+ pos = Conflict.frontline_position(cp, ecp, self.game.theater)[0]
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
pos.x,
diff --git a/qt_ui/widgets/floatspinners.py b/qt_ui/widgets/floatspinners.py
new file mode 100644
index 00000000..058b3516
--- /dev/null
+++ b/qt_ui/widgets/floatspinners.py
@@ -0,0 +1,20 @@
+from typing import Optional
+
+from PySide2.QtWidgets import QSpinBox
+
+
+class TenthsSpinner(QSpinBox):
+ def __init__(self, minimum: Optional[int] = None,
+ maximum: Optional[int] = None,
+ initial: Optional[int] = None) -> None:
+ super().__init__()
+
+ if minimum is not None:
+ self.setMinimum(minimum)
+ if maximum is not None:
+ self.setMaximum(maximum)
+ if initial is not None:
+ self.setValue(initial)
+
+ def textFromValue(self, val: int) -> str:
+ return f"X {val / 10:.1f}"
diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py
index f1425893..2ca71953 100644
--- a/qt_ui/widgets/map/QFrontLine.py
+++ b/qt_ui/widgets/map/QFrontLine.py
@@ -13,9 +13,11 @@ from PySide2.QtWidgets import (
)
import qt_ui.uiconstants as const
+from game.theater import FrontLine
from qt_ui.dialogs import Dialog
+from qt_ui.models import GameModel
+from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
-from theater.missiontarget import MissionTarget
class QFrontLine(QGraphicsLineItem):
@@ -26,9 +28,10 @@ class QFrontLine(QGraphicsLineItem):
"""
def __init__(self, x1: float, y1: float, x2: float, y2: float,
- mission_target: MissionTarget) -> None:
+ mission_target: FrontLine, game_model: GameModel) -> None:
super().__init__(x1, y1, x2, y2)
self.mission_target = mission_target
+ self.game_model = game_model
self.new_package_dialog: Optional[QNewPackageDialog] = None
self.setAcceptHoverEvents(True)
@@ -55,6 +58,14 @@ class QFrontLine(QGraphicsLineItem):
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
+ cheat_forward = QAction(f"CHEAT: Advance Frontline")
+ cheat_forward.triggered.connect(self.cheat_forward)
+ menu.addAction(cheat_forward)
+
+ cheat_backward = QAction(f"CHEAT: Retreat Frontline")
+ cheat_backward.triggered.connect(self.cheat_backward)
+ menu.addAction(cheat_backward)
+
menu.exec_(event.screenPos())
@property
@@ -80,3 +91,16 @@ class QFrontLine(QGraphicsLineItem):
def open_new_package_dialog(self) -> None:
"""Opens the dialog for planning a new mission package."""
Dialog.open_new_package_dialog(self.mission_target)
+
+ def cheat_forward(self) -> None:
+ self.mission_target.control_point_a.base.affect_strength(0.1)
+ self.mission_target.control_point_b.base.affect_strength(-0.1)
+ self.game_model.game.initialize_turn()
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+
+ def cheat_backward(self) -> None:
+ self.mission_target.control_point_a.base.affect_strength(-0.1)
+ self.mission_target.control_point_b.base.affect_strength(0.1)
+ self.game_model.game.initialize_turn()
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+
\ No newline at end of file
diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py
index a24b609b..8b49077c 100644
--- a/qt_ui/widgets/map/QLiberationMap.py
+++ b/qt_ui/widgets/map/QLiberationMap.py
@@ -2,9 +2,11 @@ from __future__ import annotations
import datetime
import logging
-from typing import List, Optional, Tuple
+import math
+from typing import Iterable, Iterator, List, Optional, Tuple
-from PySide2.QtCore import QPointF, Qt
+from PySide2 import QtWidgets, QtCore
+from PySide2.QtCore import QPointF, Qt, QLineF, QRectF
from PySide2.QtGui import (
QBrush,
QColor,
@@ -12,21 +14,25 @@ from PySide2.QtGui import (
QPen,
QPixmap,
QPolygonF,
- QWheelEvent,
-)
+ QWheelEvent, )
from PySide2.QtWidgets import (
QFrame,
QGraphicsItem,
QGraphicsOpacityEffect,
QGraphicsScene,
- QGraphicsView,
+ QGraphicsView, QGraphicsSceneMouseEvent,
)
from dcs import Point
from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST
from game import Game, db
-from game.utils import meter_to_feet
+from game.theater import ControlPoint, Enum
+from game.theater.conflicttheater import FrontLine, ReferencePoint
+from game.theater.theatergroundobject import (
+ TheaterGroundObject,
+)
+from game.utils import meter_to_feet, nm_to_meter, meter_to_nm
from game.weather import TimeOfDay
from gen import Conflict
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
@@ -38,17 +44,47 @@ from qt_ui.widgets.map.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
-from theater import ControlPoint, FrontLine
-from theater.theatergroundobject import (
- EwrGroundObject,
- MissileSiteGroundObject,
- TheaterGroundObject,
-)
+
+MAX_SHIP_DISTANCE = 80
+
+def binomial(i: int, n: int) -> float:
+ """Binomial coefficient"""
+ return math.factorial(n) / float(
+ math.factorial(i) * math.factorial(n - i))
+
+
+def bernstein(t: float, i: int, n: int) -> float:
+ """Bernstein polynom"""
+ return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i))
+
+
+def bezier(t: float, points: Iterable[Tuple[float, float]]) -> Tuple[float, float]:
+ """Calculate coordinate of a point in the bezier curve"""
+ n = len(points) - 1
+ x = y = 0
+ for i, pos in enumerate(points):
+ bern = bernstein(t, i, n)
+ x += pos[0] * bern
+ y += pos[1] * bern
+ return x, y
+
+
+def bezier_curve_range(n: int, points: Iterable[Tuple[float, float]]) -> Iterator[Tuple[float, float]]:
+ """Range of points in a curve bezier"""
+ for i in range(n):
+ t = i / float(n - 1)
+ yield bezier(t, points)
+
+
+class QLiberationMapState(Enum):
+ NORMAL = 0
+ MOVING_UNIT = 1
class QLiberationMap(QGraphicsView):
- WAYPOINT_SIZE = 4
+ WAYPOINT_SIZE = 4
+ reference_point_setup_mode = False
instance: Optional[QLiberationMap] = None
def __init__(self, game_model: GameModel):
@@ -56,6 +92,7 @@ class QLiberationMap(QGraphicsView):
QLiberationMap.instance = self
self.game_model = game_model
self.game: Optional[Game] = game_model.game
+ self.state = QLiberationMapState.NORMAL
self.waypoint_info_font = QFont()
self.waypoint_info_font.setPointSize(12)
@@ -72,6 +109,11 @@ class QLiberationMap(QGraphicsView):
self.init_scene()
self.setGame(game_model.game)
+ # Object displayed when unit is selected
+ self.movement_line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(QPointF(0,0),QPointF(0,0)))
+ self.movement_line.setPen(QPen(CONST.COLORS["orange"], width=10.0))
+ self.selected_cp: QMapControlPoint = None
+
GameUpdateSignal.get_instance().flight_paths_changed.connect(
lambda: self.draw_flight_plans(self.scene())
)
@@ -113,6 +155,9 @@ class QLiberationMap(QGraphicsView):
update_flight_selection
)
+ self.nm_to_pixel_ratio: int = 0
+
+
def init_scene(self):
scene = QLiberationScene(self)
self.setScene(scene)
@@ -126,45 +171,73 @@ class QLiberationMap(QGraphicsView):
self.game = game
if self.game is not None:
logging.debug("Reloading Map Canvas")
+ self.nm_to_pixel_ratio = self.km_to_pixel(float(nm_to_meter(1)) / 1000.0)
self.reload_scene()
"""
- Uncomment to set up theather reference points
+ Uncomment to set up theather reference points"""
def keyPressEvent(self, event):
- #super(QLiberationMap, self).keyPressEvent(event)
-
- numpad_mod = int(event.modifiers()) & QtCore.Qt.KeypadModifier
- i = 0
- for k,v in self.game.theater.reference_points.items():
- if i == 0:
- point_0 = k
+ modifiers = QtWidgets.QApplication.keyboardModifiers()
+ if not self.reference_point_setup_mode:
+ if modifiers == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_R:
+ self.reference_point_setup_mode = True
+ self.reload_scene()
else:
- point_1 = k
- i = i + 1
+ super(QLiberationMap, self).keyPressEvent(event)
+ else:
+ if modifiers == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_R:
+ self.reference_point_setup_mode = False
+ self.reload_scene()
+ else:
+ distance = 1
+ modifiers = int(event.modifiers())
+ if modifiers & QtCore.Qt.ShiftModifier:
+ distance *= 10
+ elif modifiers & QtCore.Qt.ControlModifier:
+ distance *= 100
- if event.key() == QtCore.Qt.Key_Down:
- self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0] + 10, self.game.theater.reference_points[point_0][1]
- if event.key() == QtCore.Qt.Key_Up:
- self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0] - 10, self.game.theater.reference_points[point_0][1]
- if event.key() == QtCore.Qt.Key_Left:
- self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0], self.game.theater.reference_points[point_0][1] + 10
- if event.key() == QtCore.Qt.Key_Right:
- self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0], self.game.theater.reference_points[point_0][1] - 10
+ if event.key() == QtCore.Qt.Key_Down:
+ self.update_reference_point(
+ self.game.theater.reference_points[0],
+ Point(0, distance))
+ if event.key() == QtCore.Qt.Key_Up:
+ self.update_reference_point(
+ self.game.theater.reference_points[0],
+ Point(0, -distance))
+ if event.key() == QtCore.Qt.Key_Left:
+ self.update_reference_point(
+ self.game.theater.reference_points[0],
+ Point(-distance, 0))
+ if event.key() == QtCore.Qt.Key_Right:
+ self.update_reference_point(
+ self.game.theater.reference_points[0],
+ Point(distance, 0))
+ if event.key() == QtCore.Qt.Key_S:
+ self.update_reference_point(
+ self.game.theater.reference_points[1],
+ Point(0, distance))
+ if event.key() == QtCore.Qt.Key_W:
+ self.update_reference_point(
+ self.game.theater.reference_points[1],
+ Point(0, -distance))
+ if event.key() == QtCore.Qt.Key_A:
+ self.update_reference_point(
+ self.game.theater.reference_points[1],
+ Point(-distance, 0))
+ if event.key() == QtCore.Qt.Key_D:
+ self.update_reference_point(
+ self.game.theater.reference_points[1],
+ Point(distance, 0))
- if event.key() == QtCore.Qt.Key_2 and numpad_mod:
- self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0] + 10, self.game.theater.reference_points[point_1][1]
- if event.key() == QtCore.Qt.Key_8 and numpad_mod:
- self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0] - 10, self.game.theater.reference_points[point_1][1]
- if event.key() == QtCore.Qt.Key_4 and numpad_mod:
- self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0], self.game.theater.reference_points[point_1][1] + 10
- if event.key() == QtCore.Qt.Key_6 and numpad_mod:
- self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0], self.game.theater.reference_points[point_1][1] - 10
+ logging.debug(
+ f"Reference points: {self.game.theater.reference_points}")
+ self.reload_scene()
- print(self.game.theater.reference_points)
- self.reload_scene()
- """
+ @staticmethod
+ def update_reference_point(point: ReferencePoint, change: Point) -> None:
+ point.image_coordinates += change
@staticmethod
def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
@@ -189,6 +262,60 @@ class QLiberationMap(QGraphicsView):
return detection_range, threat_range
+ def display_culling(self, scene: QGraphicsScene) -> None:
+ """Draws the culling distance rings on the map"""
+ culling_points = self.game_model.game.get_culling_points()
+ culling_distance = self.game_model.game.settings.perf_culling_distance
+ for point in culling_points:
+ culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000)
+ distance_point = self._transform_point(culling_distance_point)
+ transformed = self._transform_point(point)
+ radius = distance_point[0] - transformed[0]
+ scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
+
+ @staticmethod
+ def should_display_ground_objects_at(cp: ControlPoint) -> bool:
+ return ((DisplayOptions.sam_ranges and cp.captured) or
+ (DisplayOptions.enemy_sam_ranges and not cp.captured))
+
+ def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
+ go_pos = self._transform_point(ground_object.position)
+ detection_range, threat_range = self.aa_ranges(
+ ground_object
+ )
+ if threat_range:
+ threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
+ ground_object.position.y+threat_range))
+ threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
+
+ # Add threat range circle
+ scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
+ threat_radius, threat_radius, self.threat_pen(cp.captured))
+
+ if detection_range and DisplayOptions.detection_range:
+ # Add detection range circle
+ detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
+ ground_object.position.y+detection_range))
+ detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
+ scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
+ detection_radius, detection_radius, self.detection_pen(cp.captured))
+
+ def draw_ground_objects(self, scene: QGraphicsScene, cp: ControlPoint) -> None:
+ added_objects = []
+ for ground_object in cp.ground_objects:
+ if ground_object.obj_name in added_objects:
+ continue
+
+ go_pos = self._transform_point(ground_object.position)
+ if not ground_object.airbase_group:
+ buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
+ scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
+
+ should_display = self.should_display_ground_objects_at(cp)
+ if ground_object.might_have_aa and should_display:
+ self.draw_threat_range(scene, ground_object, cp)
+ added_objects.append(ground_object.obj_name)
+
def reload_scene(self):
scene = self.scene()
scene.clear()
@@ -198,24 +325,12 @@ class QLiberationMap(QGraphicsView):
self.addBackground()
- # Uncomment below to help set up theater reference points
- #for i, r in enumerate(self.game.theater.reference_points.items()):
- # text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False))
- # text.setPos(0, i * 24)
-
# Display Culling
if DisplayOptions.culling and self.game.settings.perf_culling:
- culling_points = self.game_model.game.get_culling_points()
- culling_distance = self.game_model.game.settings.perf_culling_distance
- for point in culling_points:
- culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000)
- distance_point = self._transform_point(culling_distance_point)
- transformed = self._transform_point(point)
- diameter = distance_point[0] - transformed[0]
- scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
+ self.display_culling(scene)
for cp in self.game.theater.controlpoints:
-
+
pos = self._transform_point(cp.position)
scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2,
@@ -230,45 +345,12 @@ class QLiberationMap(QGraphicsView):
pen = QPen(brush=CONST.COLORS[enemyColor])
brush = CONST.COLORS[enemyColor+"_transparent"]
- added_objects = []
- for ground_object in cp.ground_objects:
- if ground_object.obj_name in added_objects:
- continue
+ self.draw_ground_objects(scene, cp)
- go_pos = self._transform_point(ground_object.position)
- if not ground_object.airbase_group:
- buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
- scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
-
- is_missile = isinstance(ground_object, MissileSiteGroundObject)
- is_aa = ground_object.category == "aa" and not is_missile
- is_ewr = isinstance(ground_object, EwrGroundObject)
- is_display_type = is_aa or is_ewr
- should_display = ((DisplayOptions.sam_ranges and cp.captured)
- or
- (DisplayOptions.enemy_sam_ranges and not cp.captured))
-
- if is_display_type and should_display:
- detection_range, threat_range = self.aa_ranges(
- ground_object
- )
- if threat_range:
- threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
- ground_object.position.y+threat_range))
- threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
-
- # Add threat range circle
- scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
- threat_radius, threat_radius, self.threat_pen(cp.captured))
- if detection_range:
- # Add detection range circle
- detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
- ground_object.position.y+detection_range))
- detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
- if DisplayOptions.detection_range:
- scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
- detection_radius, detection_radius, self.detection_pen(cp.captured))
- added_objects.append(ground_object.obj_name)
+ if cp.target_position is not None:
+ proj = self._transform_point(cp.target_position)
+ scene.addLine(QLineF(QPointF(pos[0], pos[1]), QPointF(proj[0], proj[1])),
+ QPen(CONST.COLORS["green"], width=10, s=Qt.DashDotLine))
for cp in self.game.theater.enemy_points():
if DisplayOptions.lines:
@@ -328,6 +410,10 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_SHIP,
)
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
+ if point.waypoint_type == FlightWaypointType.DIVERT:
+ # Don't clutter the map showing divert points.
+ continue
+
new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected)
@@ -341,7 +427,6 @@ class QLiberationMap(QGraphicsView):
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan)
prev_pos = tuple(new_pos)
- self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool, selected: bool) -> None:
@@ -399,39 +484,113 @@ class QLiberationMap(QGraphicsView):
flight_path_pen
))
+ def draw_bezier_frontline(self, scene: QGraphicsScene, pen:QPen, frontline: FrontLine) -> None:
+ """
+ Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from.
+ https://gist.github.com/Alquimista/1274149#file-bezdraw-py
+ """
+ bezier_fixed_points = []
+ for segment in frontline.segments:
+ bezier_fixed_points.append(self._transform_point(segment.point_a))
+ bezier_fixed_points.append(self._transform_point(segment.point_b))
+
+ old_point = bezier_fixed_points[0]
+ for point in bezier_curve_range(int(len(bezier_fixed_points) * 2), bezier_fixed_points):
+ scene.addLine(old_point[0], old_point[1], point[0], point[1], pen=pen)
+ old_point = point
+
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
scene = self.scene()
- pos = self._transform_point(cp.position)
for connected_cp in cp.connected_points:
pos2 = self._transform_point(connected_cp.position)
if not cp.captured:
color = CONST.COLORS["dark_"+enemyColor]
- elif cp.captured:
- color = CONST.COLORS["dark_"+playerColor]
else:
- color = CONST.COLORS["dark_"+enemyColor]
-
+ color = CONST.COLORS["dark_"+playerColor]
pen = QPen(brush=color)
pen.setColor(color)
pen.setWidth(6)
+ frontline = FrontLine(cp, connected_cp, self.game.theater)
if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp):
- if not cp.captured:
- scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
+ if DisplayOptions.actual_frontline_pos:
+ self.draw_actual_frontline(frontline, scene, pen)
else:
- posx, h = Conflict.frontline_position(self.game.theater, cp, connected_cp)
- pos2 = self._transform_point(posx)
- scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
-
- p1 = point_from_heading(pos2[0], pos2[1], h+180, 25)
- p2 = point_from_heading(pos2[0], pos2[1], h, 25)
- scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1],
- FrontLine(cp, connected_cp)))
-
+ self.draw_frontline_approximation(frontline, scene, pen)
else:
- scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
+ self.draw_bezier_frontline(scene, pen, frontline)
+
+ def draw_frontline_approximation(self, frontline: FrontLine, scene: QGraphicsScene, pen: QPen) -> None:
+ posx = frontline.position
+ h = frontline.attack_heading
+ pos2 = self._transform_point(posx)
+ self.draw_bezier_frontline(scene, pen, frontline)
+ p1 = point_from_heading(pos2[0], pos2[1], h+180, 25)
+ p2 = point_from_heading(pos2[0], pos2[1], h, 25)
+ scene.addItem(
+ QFrontLine(
+ p1[0],
+ p1[1],
+ p2[0],
+ p2[1],
+ frontline,
+ self.game_model
+ )
+ )
+
+ def draw_actual_frontline(self, frontline: FrontLine, scene: QGraphicsScene, pen: QPen) -> None:
+ self.draw_bezier_frontline(scene, pen, frontline)
+ vector = Conflict.frontline_vector(
+ frontline.control_point_a,
+ frontline.control_point_b,
+ self.game.theater
+ )
+ left_pos = self._transform_point(vector[0])
+ right_pos = self._transform_point(
+ vector[0].point_from_heading(vector[1], vector[2])
+ )
+ scene.addItem(
+ QFrontLine(
+ left_pos[0],
+ left_pos[1],
+ right_pos[0],
+ right_pos[1],
+ frontline,
+ self.game_model
+ )
+ )
+
+ def draw_scale(self, scale_distance_nm=20, number_of_points=4):
+
+ PADDING = 14
+ POS_X = 0
+ POS_Y = 10
+ BIG_LINE = 5
+ SMALL_LINE = 2
+
+ dist = self.km_to_pixel(nm_to_meter(scale_distance_nm)/1000.0)
+ self.scene().addRect(POS_X, POS_Y-PADDING, PADDING*2 + dist, BIG_LINE*2+3*PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"])
+ l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE*2, POS_X + PADDING + dist, POS_Y + BIG_LINE*2)
+
+ text = self.scene().addText("0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False))
+ text.setPos(POS_X, POS_Y + BIG_LINE*2)
+ text.setDefaultTextColor(Qt.white)
+
+ text2 = self.scene().addText(str(scale_distance_nm) + "nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False))
+ text2.setPos(POS_X + dist, POS_Y + BIG_LINE * 2)
+ text2.setDefaultTextColor(Qt.white)
+
+ l.setPen(CONST.COLORS["white"])
+ for i in range(number_of_points+1):
+ d = float(i)/float(number_of_points)
+ if i == 0 or i == number_of_points:
+ h = BIG_LINE
+ else:
+ h = SMALL_LINE
+
+ l = self.scene().addLine(POS_X + PADDING + d * dist, POS_Y + BIG_LINE*2, POS_X + PADDING + d * dist, POS_Y + BIG_LINE - h)
+ l.setPen(CONST.COLORS["white"])
def wheelEvent(self, event: QWheelEvent):
-
if event.angleDelta().y() > 0:
factor = 1.25
self._zoom += 1
@@ -449,35 +608,67 @@ class QLiberationMap(QGraphicsView):
else:
self._zoom = -4
- #print(self.factorized, factor, self._zoom)
+ @staticmethod
+ def _transpose_point(p: Point) -> Point:
+ return Point(p.y, p.x)
- def _transform_point(self, p: Point, treshold=30) -> (int, int):
- point_a = list(self.game.theater.reference_points.keys())[0]
- point_a_img = self.game.theater.reference_points[point_a]
+ def _scaling_factor(self) -> Point:
+ point_a = self.game.theater.reference_points[0]
+ point_b = self.game.theater.reference_points[1]
- point_b = list(self.game.theater.reference_points.keys())[1]
- point_b_img = self.game.theater.reference_points[point_b]
+ world_distance = self._transpose_point(
+ point_b.world_coordinates - point_a.world_coordinates)
+ image_distance = point_b.image_coordinates - point_a.image_coordinates
- Y_dist = point_a_img[0] - point_b_img[0]
- lon_dist = point_a[1] - point_b[1]
+ x_scale = image_distance.x / world_distance.x
+ y_scale = image_distance.y / world_distance.y
+ return Point(x_scale, y_scale)
- X_dist = point_a_img[1] - point_b_img[1]
- lat_dist = point_b[0] - point_a[0]
+ # TODO: Move this and its inverse into ConflictTheater.
+ def _transform_point(self, world_point: Point) -> Tuple[float, float]:
+ """Transforms world coordinates to image coordinates.
- Y_scale = float(Y_dist) / float(lon_dist)
- X_scale = float(X_dist) / float(lat_dist)
+ World coordinates are transposed. X increases toward the North, Y
+ increases toward the East. The origin point depends on the map.
- # ---
- Y_offset = p.x - point_a[0]
- X_offset = p.y - point_a[1]
+ Image coordinates originate from the top left. X increases to the right,
+ Y increases toward the bottom.
- X = point_b_img[1] + X_offset * X_scale
- Y = point_a_img[0] - Y_offset * Y_scale
+ The two points should be as distant as possible in both latitude and
+ logitude, and tuning the reference points will be simpler if they are in
+ geographically recognizable locations. For example, the Caucasus map is
+ aligned using the first point on Gelendzhik and the second on Batumi.
- #X += 5
- #Y += 5
+ The distances between each point are computed and a scaling factor is
+ determined from that. The given point is then offset from the first
+ point using the scaling factor.
- return X > treshold and X or treshold, Y > treshold and Y or treshold
+ X is latitude, increasing northward.
+ Y is longitude, increasing eastward.
+ """
+ point_a = self.game.theater.reference_points[0]
+ scale = self._scaling_factor()
+
+ offset = self._transpose_point(point_a.world_coordinates - world_point)
+ scaled = Point(offset.x * scale.x, offset.y * scale.y)
+ transformed = point_a.image_coordinates - scaled
+ return transformed.x, transformed.y
+
+ def _scene_to_dcs_coords(self, scene_point: Point) -> Point:
+ point_a = self.game.theater.reference_points[0]
+ scale = self._scaling_factor()
+
+ offset = point_a.image_coordinates - scene_point
+ scaled = self._transpose_point(
+ Point(offset.x / scale.x, offset.y / scale.y))
+ return point_a.world_coordinates - scaled
+
+ def km_to_pixel(self, km):
+ p1 = Point(0, 0)
+ p2 = Point(0, 1000*km)
+ p1a = Point(*self._transform_point(p1))
+ p2a = Point(*self._transform_point(p2))
+ return p1a.distance_to_point(p2a)
def highlight_color(self, transparent: Optional[bool] = False) -> QColor:
return QColor(255, 255, 0, 20 if transparent else 255)
@@ -501,18 +692,11 @@ class QLiberationMap(QGraphicsView):
return CONST.COLORS[f"{name}_transparent"]
def threat_pen(self, player: bool) -> QPen:
- if player:
- color = "blue"
- else:
- color = "red"
- qpen = QPen(CONST.COLORS[color])
- return qpen
+ color = "blue" if player else "red"
+ return QPen(CONST.COLORS[color])
def detection_pen(self, player: bool) -> QPen:
- if player:
- color = "purple"
- else:
- color = "yellow"
+ color = "purple" if player else "yellow"
qpen = QPen(CONST.COLORS[color])
qpen.setStyle(Qt.DotLine)
return qpen
@@ -554,23 +738,126 @@ class QLiberationMap(QGraphicsView):
effect.setOpacity(0.3)
overlay.setGraphicsEffect(effect)
- else:
+ if DisplayOptions.map_poly or self.reference_point_setup_mode:
# Polygon display mode
if self.game.theater.landmap is not None:
for sea_zone in self.game.theater.landmap[2]:
print(sea_zone)
- poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone])
- scene.addPolygon(poly, CONST.COLORS["sea_blue"], CONST.COLORS["sea_blue"])
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone.exterior.coords])
+ if self.reference_point_setup_mode:
+ color = "sea_blue_transparent"
+ else:
+ color = "sea_blue"
+ scene.addPolygon(poly, CONST.COLORS[color], CONST.COLORS[color])
for inclusion_zone in self.game.theater.landmap[0]:
- poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone])
- scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone.exterior.coords])
+ if self.reference_point_setup_mode:
+ scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_grey_transparent"])
+ else:
+ scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
for exclusion_zone in self.game.theater.landmap[1]:
- poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone])
- scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])
+ poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone.exterior.coords])
+ if self.reference_point_setup_mode:
+ scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_dark_grey_transparent"])
+ else:
+ scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])
+ # Uncomment to display plan projection test
+ # self.projection_test()
+ self.draw_scale()
+ if self.reference_point_setup_mode:
+ for i, point in enumerate(self.game.theater.reference_points):
+ self.scene().addRect(
+ QRectF(point.image_coordinates.x, point.image_coordinates.y,
+ 25, 25), pen=CONST.COLORS["red"],
+ brush=CONST.COLORS["red"])
+ text = self.scene().addText(
+ f"P{i} = {point.image_coordinates}",
+ font=QFont("Trebuchet MS", 14, weight=8, italic=False))
+ text.setDefaultTextColor(CONST.COLORS["red"])
+ text.setPos(point.image_coordinates.x + 26,
+ point.image_coordinates.y)
+ # Set to True to visually debug _transform_point.
+ draw_transformed = False
+ if draw_transformed:
+ x, y = self._transform_point(point.world_coordinates)
+ self.scene().addRect(
+ QRectF(x, y, 25, 25),
+ pen=CONST.COLORS["red"],
+ brush=CONST.COLORS["red"])
+ text = self.scene().addText(
+ f"P{i}' = {x}, {y}",
+ font=QFont("Trebuchet MS", 14, weight=8, italic=False))
+ text.setDefaultTextColor(CONST.COLORS["red"])
+ text.setPos(x + 26, y)
+ def projection_test(self):
+ for i in range(100):
+ for j in range(100):
+ x = i * 100.0
+ y = j * 100.0
+ original = Point(x, y)
+ proj = self._scene_to_dcs_coords(original)
+ unproj = self._transform_point(proj)
+ converted = Point(*unproj)
+ assert math.isclose(original.x, converted.x, abs_tol=0.00000001)
+ assert math.isclose(original.y, converted.y, abs_tol=0.00000001)
+
+ def setSelectedUnit(self, selected_cp: QMapControlPoint):
+ self.state = QLiberationMapState.MOVING_UNIT
+ self.selected_cp = selected_cp
+ position = self._transform_point(selected_cp.control_point.position)
+ self.movement_line = QtWidgets.QGraphicsLineItem(QLineF(QPointF(*position), QPointF(*position)))
+ self.scene().addItem(self.movement_line)
+
+ def is_valid_ship_pos(self, scene_position: Point) -> bool:
+ world_destination = self._scene_to_dcs_coords(scene_position)
+ distance = self.selected_cp.control_point.position.distance_to_point(
+ world_destination
+ )
+ if meter_to_nm(distance) > MAX_SHIP_DISTANCE:
+ return False
+ return self.game.theater.is_in_sea(world_destination)
+
+ def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent):
+ if self.state == QLiberationMapState.MOVING_UNIT:
+ self.setCursor(Qt.PointingHandCursor)
+ self.movement_line.setLine(
+ QLineF(self.movement_line.line().p1(), event.scenePos()))
+
+ pos = Point(event.scenePos().x(), event.scenePos().y())
+ if self.is_valid_ship_pos(pos):
+ self.movement_line.setPen(CONST.COLORS["green"])
+ else:
+ self.movement_line.setPen(CONST.COLORS["red"])
+
+ def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent):
+ if self.state == QLiberationMapState.MOVING_UNIT:
+ if event.buttons() == Qt.RightButton:
+ pass
+ elif event.buttons() == Qt.LeftButton:
+ if self.selected_cp is not None:
+ # Set movement position for the cp
+ pos = event.scenePos()
+ point = Point(int(pos.x()), int(pos.y()))
+ proj = self._scene_to_dcs_coords(point)
+
+ if self.is_valid_ship_pos(point):
+ self.selected_cp.control_point.target_position = proj
+ else:
+ self.selected_cp.control_point.target_position = None
+
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+ else:
+ return
+ self.state = QLiberationMapState.NORMAL
+ try:
+ self.scene().removeItem(self.movement_line)
+ except:
+ pass
+ self.selected_cp = None
\ No newline at end of file
diff --git a/qt_ui/widgets/map/QLiberationScene.py b/qt_ui/widgets/map/QLiberationScene.py
index 30f0b56a..5ad08d5e 100644
--- a/qt_ui/widgets/map/QLiberationScene.py
+++ b/qt_ui/widgets/map/QLiberationScene.py
@@ -1,5 +1,4 @@
-from PySide2.QtGui import QFont
-from PySide2.QtWidgets import QGraphicsScene
+from PySide2.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent
import qt_ui.uiconstants as CONST
@@ -11,3 +10,11 @@ class QLiberationScene(QGraphicsScene):
item = self.addText("Go to \"File/New Game\" to setup a new campaign or go to \"File/Open\" to load an existing save game.",
CONST.FONT_PRIMARY)
item.setDefaultTextColor(CONST.COLORS["white"])
+
+ def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
+ super(QLiberationScene, self).mouseMoveEvent(event)
+ self.parent().sceneMouseMovedEvent(event)
+
+ def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
+ super(QLiberationScene, self).mousePressEvent(event)
+ self.parent().sceneMousePressEvent(event)
diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py
index e59cdbfb..8249d33a 100644
--- a/qt_ui/widgets/map/QMapControlPoint.py
+++ b/qt_ui/widgets/map/QMapControlPoint.py
@@ -4,9 +4,9 @@ from PySide2.QtGui import QColor, QPainter
from PySide2.QtWidgets import QAction, QMenu
import qt_ui.uiconstants as const
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
-from theater import ControlPoint
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
from ...windows.GameUpdateSignal import GameUpdateSignal
@@ -26,6 +26,12 @@ class QMapControlPoint(QMapObject):
f"CHEAT: Capture {self.control_point.name}")
self.capture_action.triggered.connect(self.cheat_capture)
+ self.move_action = QAction("Move")
+ self.move_action.triggered.connect(self.move)
+
+ self.cancel_move_action = QAction("Cancel Move")
+ self.cancel_move_action.triggered.connect(self.cancel_move)
+
def paint(self, painter, option, widget=None) -> None:
if DisplayOptions.control_points:
painter.save()
@@ -33,13 +39,12 @@ class QMapControlPoint(QMapObject):
painter.setBrush(self.brush_color)
painter.setPen(self.pen_color)
- if self.control_point.has_runway():
- if self.isUnderMouse():
- painter.setBrush(const.COLORS["white"])
- painter.setPen(self.pen_color)
+ if not self.control_point.runway_is_operational():
+ painter.setBrush(const.COLORS["black"])
+ painter.setPen(self.brush_color)
- r = option.rect
- painter.drawEllipse(r.x(), r.y(), r.width(), r.height())
+ r = option.rect
+ painter.drawEllipse(r.x(), r.y(), r.width(), r.height())
# TODO: Draw sunk carriers differently.
# Either don't draw them at all, or perhaps use a sunk ship icon.
painter.restore()
@@ -71,6 +76,12 @@ class QMapControlPoint(QMapObject):
self.base_details_dialog.show()
def add_context_menu_actions(self, menu: QMenu) -> None:
+
+ if self.control_point.moveable and self.control_point.captured:
+ menu.addAction(self.move_action)
+ if self.control_point.target_position is not None:
+ menu.addAction(self.cancel_move_action)
+
if self.control_point.is_fleet:
return
@@ -79,17 +90,21 @@ class QMapControlPoint(QMapObject):
for connected in self.control_point.connected_points:
if connected.captured:
+ menu.addAction(self.capture_action)
break
- else:
- return
-
- menu.addAction(self.capture_action)
def cheat_capture(self) -> None:
self.control_point.capture(self.game_model.game, for_player=True)
# Reinitialized ground planners and the like.
self.game_model.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+
+ def move(self):
+ self.parent.setSelectedUnit(self)
+
+ def cancel_move(self):
+ self.control_point.target_position = None
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def open_new_package_dialog(self) -> None:
"""Extends the default packagedialog to redirect to base menu for red air base."""
diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py
index a7d857f3..7d8217b5 100644
--- a/qt_ui/widgets/map/QMapGroundObject.py
+++ b/qt_ui/widgets/map/QMapGroundObject.py
@@ -8,8 +8,9 @@ import qt_ui.uiconstants as const
from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS
+from game.theater import ControlPoint, TheaterGroundObject
+from game.theater.theatergroundobject import MissileSiteGroundObject
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
-from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
@@ -72,6 +73,8 @@ class QMapGroundObject(QMapObject):
cat = self.ground_object.category
if cat == "aa" and self.ground_object.sea_object:
cat = "ship"
+ if isinstance(self.ground_object, MissileSiteGroundObject):
+ cat = "missile"
rect = QRect(option.rect.x() + 2, option.rect.y(),
option.rect.width() - 2, option.rect.height())
diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py
index fa28c333..16f07061 100644
--- a/qt_ui/widgets/map/QMapObject.py
+++ b/qt_ui/widgets/map/QMapObject.py
@@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
from qt_ui.dialogs import Dialog
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
-from theater.missiontarget import MissionTarget
+from game.theater.missiontarget import MissionTarget
class QMapObject(QGraphicsRectItem):
@@ -47,9 +47,12 @@ class QMapObject(QGraphicsRectItem):
object_details_action.triggered.connect(self.on_click)
menu.addAction(object_details_action)
- new_package_action = QAction(f"New package")
- new_package_action.triggered.connect(self.open_new_package_dialog)
- menu.addAction(new_package_action)
+ # Not all locations have valid objetives. Off-map spawns, for example,
+ # have no mission types.
+ if list(self.mission_target.mission_types(for_player=True)):
+ new_package_action = QAction(f"New package")
+ new_package_action.triggered.connect(self.open_new_package_dialog)
+ menu.addAction(new_package_action)
self.add_context_menu_actions(menu)
diff --git a/qt_ui/widgets/spinsliders.py b/qt_ui/widgets/spinsliders.py
new file mode 100644
index 00000000..1b63862b
--- /dev/null
+++ b/qt_ui/widgets/spinsliders.py
@@ -0,0 +1,26 @@
+from PySide2.QtCore import Qt
+from PySide2.QtWidgets import QGridLayout, QLabel, QSlider
+
+from qt_ui.widgets.floatspinners import TenthsSpinner
+
+
+class TenthsSpinSlider(QGridLayout):
+ def __init__(self, label: str, minimum: int, maximum: int,
+ initial: int) -> None:
+ super().__init__()
+ self.addWidget(QLabel(label), 0, 0)
+
+ slider = QSlider(Qt.Horizontal)
+ slider.setMinimum(minimum)
+ slider.setMaximum(maximum)
+ slider.setValue(initial)
+ self.spinner = TenthsSpinner(minimum, maximum, initial)
+ slider.valueChanged.connect(lambda x: self.spinner.setValue(x))
+ self.spinner.valueChanged.connect(lambda x: slider.setValue(x))
+
+ self.addWidget(slider, 1, 0)
+ self.addWidget(self.spinner, 1, 1)
+
+ @property
+ def value(self) -> float:
+ return self.spinner.value() / 10
diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py
index 529a7498..f70c0e49 100644
--- a/qt_ui/windows/GameUpdateSignal.py
+++ b/qt_ui/windows/GameUpdateSignal.py
@@ -1,19 +1,11 @@
from __future__ import annotations
-from typing import Optional, Tuple
+from typing import Optional
from PySide2.QtCore import QObject, Signal
from game import Game
-from game.event import Event, Debriefing
-
-
-class DebriefingSignal:
-
- def __init__(self, game, gameEvent, debriefing):
- self.game = game
- self.gameEvent = gameEvent
- self.debriefing = debriefing
+from game.event import Debriefing
class GameUpdateSignal(QObject):
@@ -21,7 +13,7 @@ class GameUpdateSignal(QObject):
instance = None
gameupdated = Signal(Game)
budgetupdated = Signal(Game)
- debriefingReceived = Signal(DebriefingSignal)
+ debriefingReceived = Signal(Debriefing)
flight_paths_changed = Signal()
package_selection_changed = Signal(int) # -1 indicates no selection.
@@ -51,12 +43,9 @@ class GameUpdateSignal(QObject):
# noinspection PyUnresolvedReferences
self.budgetupdated.emit(game)
- def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
- sig = DebriefingSignal(game, gameEvent, debriefing)
+ def sendDebriefing(self, debriefing: Debriefing) -> None:
# noinspection PyUnresolvedReferences
- self.gameupdated.emit(game)
- # noinspection PyUnresolvedReferences
- self.debriefingReceived.emit(sig)
+ self.debriefingReceived.emit(debriefing)
@staticmethod
def get_instance() -> GameUpdateSignal:
diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py
index e0ecce57..a2d4c25c 100644
--- a/qt_ui/windows/QDebriefingWindow.py
+++ b/qt_ui/windows/QDebriefingWindow.py
@@ -1,3 +1,5 @@
+import logging
+
from PySide2.QtGui import QIcon, QPixmap
from PySide2.QtWidgets import (
QDialog,
@@ -8,24 +10,21 @@ from PySide2.QtWidgets import (
QVBoxLayout,
)
+from game import db
from game.debriefing import Debriefing
-from game.game import Event, Game, db
class QDebriefingWindow(QDialog):
- def __init__(self, debriefing: Debriefing, gameEvent: Event, game: Game):
+ def __init__(self, debriefing: Debriefing):
super(QDebriefingWindow, self).__init__()
+ self.debriefing = debriefing
self.setModal(True)
self.setWindowTitle("Debriefing")
self.setMinimumSize(300, 200)
self.setWindowIcon(QIcon("./resources/icon.png"))
- self.game = game
- self.gameEvent = gameEvent
- self.debriefing = debriefing
-
self.initUI()
def initUI(self):
@@ -39,86 +38,93 @@ class QDebriefingWindow(QDialog):
self.layout.addWidget(header)
self.layout.addStretch()
- # Result
- #if self.gameEvent.is_successfull(self.debriefing):
- # title = QLabel("Operation end !")
- # title.setProperty("style", "title-success")
- #else:
- # title = QLabel("Operation end !")
- # title.setProperty("style", "title-danger")
title = QLabel("Casualty report")
self.layout.addWidget(title)
# Player lost units
- lostUnits = QGroupBox(self.game.player_country + "'s lost units :")
+ lostUnits = QGroupBox(
+ f"{self.debriefing.player_country}'s lost units:")
lostUnitsLayout = QGridLayout()
lostUnits.setLayout(lostUnitsLayout)
row = 0
- for unit_type, count in self.debriefing.player_dead_aircraft_dict.items():
+ player_air_losses = self.debriefing.air_losses.by_type(player=True)
+ for unit_type, count in player_air_losses.items():
try:
- lostUnitsLayout.addWidget(QLabel(db.unit_type_name(unit_type)), row, 0)
- lostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
+ lostUnitsLayout.addWidget(
+ QLabel(db.unit_type_name(unit_type)), row, 0)
+ lostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
row += 1
- except:
- print("Issue adding " + str(unit_type) + " to debriefing information")
+ except AttributeError:
+ logging.exception(
+ f"Issue adding {unit_type} to debriefing information")
- for unit_type, count in self.debriefing.player_dead_units_dict.items():
+ front_line_losses = self.debriefing.front_line_losses_by_type(
+ player=True
+ )
+ for unit_type, count in front_line_losses.items():
try:
- lostUnitsLayout.addWidget(QLabel(db.unit_type_name(unit_type)), row, 0)
- lostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
+ lostUnitsLayout.addWidget(
+ QLabel(db.unit_type_name(unit_type)), row, 0)
+ lostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
row += 1
- except:
- print("Issue adding " + str(unit_type) + " to debriefing information")
+ except AttributeError:
+ logging.exception(
+ f"Issue adding {unit_type} to debriefing information")
- for building, count in self.debriefing.player_dead_buildings_dict.items():
+ building_losses = self.debriefing.building_losses_by_type(player=True)
+ for building, count in building_losses.items():
try:
- lostUnitsLayout.addWidget(QLabel(building, row, 0))
- lostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
+ lostUnitsLayout.addWidget(QLabel(building), row, 0)
+ lostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
row += 1
- except:
- print("Issue adding " + str(building) + " to debriefing information")
+ except AttributeError:
+ logging.exception(
+ f"Issue adding {building} to debriefing information")
self.layout.addWidget(lostUnits)
# Enemy lost units
- enemylostUnits = QGroupBox(self.game.enemy_country + "'s lost units :")
+ enemylostUnits = QGroupBox(
+ f"{self.debriefing.enemy_country}'s lost units:")
enemylostUnitsLayout = QGridLayout()
enemylostUnits.setLayout(enemylostUnitsLayout)
- #row = 0
- #if self.debriefing.destroyed_objects:
- # enemylostUnitsLayout.addWidget(QLabel("Ground assets"), row, 0)
- # enemylostUnitsLayout.addWidget(QLabel("{}".format(len(self.debriefing.destroyed_objects))), row, 1)
- # row += 1
-
- for unit_type, count in self.debriefing.enemy_dead_aircraft_dict.items():
- if count == 0:
- continue
+ enemy_air_losses = self.debriefing.air_losses.by_type(player=False)
+ for unit_type, count in enemy_air_losses.items():
try:
- enemylostUnitsLayout.addWidget(QLabel(db.unit_type_name(unit_type)), row, 0)
- enemylostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
+ enemylostUnitsLayout.addWidget(
+ QLabel(db.unit_type_name(unit_type)), row, 0)
+ enemylostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
row += 1
- except:
- print("Issue adding " + str(unit_type) + " to debriefing information")
+ except AttributeError:
+ logging.exception(
+ f"Issue adding {unit_type} to debriefing information")
- for unit_type, count in self.debriefing.enemy_dead_units_dict.items():
+ front_line_losses = self.debriefing.front_line_losses_by_type(
+ player=False
+ )
+ for unit_type, count in front_line_losses.items():
if count == 0:
continue
enemylostUnitsLayout.addWidget(QLabel(db.unit_type_name(unit_type)), row, 0)
enemylostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
row += 1
- for building, count in self.debriefing.enemy_dead_buildings_dict.items():
+ building_losses = self.debriefing.building_losses_by_type(player=False)
+ for building, count in building_losses.items():
try:
enemylostUnitsLayout.addWidget(QLabel(building), row, 0)
enemylostUnitsLayout.addWidget(QLabel("{}".format(count)), row, 1)
row += 1
- except:
- print("Issue adding " + str(building) + " to debriefing information")
+ except AttributeError:
+ logging.exception(
+ f"Issue adding {building} to debriefing information")
self.layout.addWidget(enemylostUnits)
+ # TODO: Display dead ground object units and runways.
+
# confirm button
okay = QPushButton("Okay")
okay.clicked.connect(self.close)
diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py
index 1560fb16..69101818 100644
--- a/qt_ui/windows/QLiberationWindow.py
+++ b/qt_ui/windows/QLiberationWindow.py
@@ -18,6 +18,8 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency
+from game.debriefing import Debriefing
+from qt_ui import liberation_install
from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel
@@ -25,7 +27,7 @@ from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
-from qt_ui.windows.GameUpdateSignal import DebriefingSignal, GameUpdateSignal
+from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
@@ -35,11 +37,11 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import \
class QLiberationWindow(QMainWindow):
- def __init__(self):
+ def __init__(self, game: Optional[Game]) -> None:
super(QLiberationWindow, self).__init__()
- self.game: Optional[Game] = None
- self.game_model = GameModel()
+ self.game = game
+ self.game_model = GameModel(game)
Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game)
@@ -52,15 +54,27 @@ class QLiberationWindow(QMainWindow):
self.initUi()
self.initActions()
- self.initMenuBar()
self.initToolbar()
+ self.initMenuBar()
self.connectSignals()
screen = QDesktopWidget().screenGeometry()
self.setGeometry(0, 0, screen.width(), screen.height())
self.setWindowState(Qt.WindowMaximized)
- self.onGameGenerated(persistency.restore_game())
+ if self.game is None:
+ last_save_file = liberation_install.get_last_save_file()
+ if last_save_file:
+ try:
+ logging.info("Loading last saved game : " + str(last_save_file))
+ game = persistency.load_game(last_save_file)
+ self.onGameGenerated(game)
+ except:
+ logging.info("Error loading latest save game")
+ else:
+ logging.info("No existing save game")
+ else:
+ self.onGameGenerated(self.game)
def initUi(self):
hbox = QSplitter(Qt.Horizontal)
@@ -117,12 +131,27 @@ class QLiberationWindow(QMainWindow):
self.showLiberationPrefDialogAction.setIcon(QIcon.fromTheme("help-about"))
self.showLiberationPrefDialogAction.triggered.connect(self.showLiberationDialog)
+ self.openDiscordAction = QAction("&Discord Server", self)
+ self.openDiscordAction.setIcon(CONST.ICONS["Discord"])
+ self.openDiscordAction.triggered.connect(lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
+
+ self.openGithubAction = QAction("&Github Repo", self)
+ self.openGithubAction.setIcon(CONST.ICONS["Github"])
+ self.openGithubAction.triggered.connect(lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
+
def initToolbar(self):
self.tool_bar = self.addToolBar("File")
self.tool_bar.addAction(self.newGameAction)
self.tool_bar.addAction(self.openAction)
self.tool_bar.addAction(self.saveGameAction)
+ self.links_bar = self.addToolBar("Links")
+ self.links_bar.addAction(self.openDiscordAction)
+ self.links_bar.addAction(self.openGithubAction)
+
+ self.display_bar = self.addToolBar("Display")
+
+
def initMenuBar(self):
self.menu = self.menuBar()
@@ -142,20 +171,26 @@ class QLiberationWindow(QMainWindow):
last_was_group = True
for item in DisplayOptions.menu_items():
if isinstance(item, DisplayRule):
- displayMenu.addAction(self.make_display_rule_action(item))
+ action = self.make_display_rule_action(item)
+ displayMenu.addAction(action)
+ if action.icon():
+ self.display_bar.addAction(action)
last_was_group = False
elif isinstance(item, DisplayGroup):
if not last_was_group:
displayMenu.addSeparator()
+ self.display_bar.addSeparator()
group = QActionGroup(displayMenu)
for display_rule in item:
- displayMenu.addAction(
- self.make_display_rule_action(display_rule, group))
+ action = self.make_display_rule_action(display_rule, group)
+ displayMenu.addAction(action)
+ if action.icon():
+ self.display_bar.addAction(action)
last_was_group = True
help_menu = self.menu.addMenu("&Help")
- help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
- help_menu.addAction("&Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
+ help_menu.addAction(self.openDiscordAction)
+ help_menu.addAction(self.openGithubAction)
help_menu.addAction("&Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases"))
help_menu.addAction("&Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"]))
help_menu.addAction("&ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
@@ -174,6 +209,10 @@ class QLiberationWindow(QMainWindow):
return closure
action = QAction(f"&{display_rule.menu_text}", group)
+
+ if display_rule.menu_text in CONST.ICONS.keys():
+ action.setIcon(CONST.ICONS[display_rule.menu_text])
+
action.setCheckable(True)
action.setChecked(display_rule.value)
action.toggled.connect(make_check_closure())
@@ -198,6 +237,8 @@ class QLiberationWindow(QMainWindow):
if self.game.savepath:
persistency.save_game(self.game)
GameUpdateSignal.get_instance().updateGame(self.game)
+ liberation_install.setup_last_save_file(self.game.savepath)
+ liberation_install.save_config()
else:
self.saveGameAs()
@@ -206,6 +247,8 @@ class QLiberationWindow(QMainWindow):
if file is not None:
self.game.savepath = file[0]
persistency.save_game(self.game)
+ liberation_install.setup_last_save_file(self.game.savepath)
+ liberation_install.save_config()
def onGameGenerated(self, game: Game):
logging.info("On Game generated")
@@ -258,9 +301,9 @@ class QLiberationWindow(QMainWindow):
self.subwindow = QLiberationPreferencesWindow()
self.subwindow.show()
- def onDebriefing(self, debrief: DebriefingSignal):
+ def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing")
- self.debriefing = QDebriefingWindow(debrief.debriefing, debrief.gameEvent, debrief.game)
+ self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show()
def closeEvent(self, event: QCloseEvent) -> None:
diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py
index cf186829..e101389c 100644
--- a/qt_ui/windows/QWaitingForMissionResultWindow.py
+++ b/qt_ui/windows/QWaitingForMissionResultWindow.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import json
import os
@@ -20,6 +22,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.debriefing import Debriefing, wait_for_debriefing
from game.game import Event, Game, logging
from game.persistency import base_path
+from game.unitmap import UnitMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -36,25 +39,30 @@ class DebriefingFileWrittenSignal(QObject):
self.debriefingReceived.emit(debriefing)
@staticmethod
- def get_instance():
+ def get_instance() -> DebriefingFileWrittenSignal:
return DebriefingFileWrittenSignal.instance
+
DebriefingFileWrittenSignal()
+
class QWaitingForMissionResultWindow(QDialog):
- def __init__(self, gameEvent: Event, game: Game):
+ def __init__(self, gameEvent: Event, game: Game, unit_map: UnitMap) -> None:
super(QWaitingForMissionResultWindow, self).__init__()
self.setModal(True)
self.gameEvent = gameEvent
self.game = game
+ self.unit_map = unit_map
self.setWindowTitle("Waiting for mission completion.")
self.setWindowIcon(QIcon("./resources/icon.png"))
self.setMinimumHeight(570)
self.initUi()
DebriefingFileWrittenSignal.get_instance().debriefingReceived.connect(self.updateLayout)
- self.wait_thread = wait_for_debriefing(lambda debriefing: self.on_debriefing_udpate(debriefing), self.game)
+ self.wait_thread = wait_for_debriefing(
+ lambda debriefing: self.on_debriefing_update(debriefing), self.game,
+ self.unit_map)
def initUi(self):
self.layout = QGridLayout()
@@ -119,57 +127,68 @@ class QWaitingForMissionResultWindow(QDialog):
self.layout.addLayout(self.gridLayout, 1, 0)
self.setLayout(self.layout)
- def updateLayout(self, debriefing):
+ def updateLayout(self, debriefing: Debriefing) -> None:
updateBox = QGroupBox("Mission status")
updateLayout = QGridLayout()
updateBox.setLayout(updateLayout)
self.debriefing = debriefing
updateLayout.addWidget(QLabel("Aircraft destroyed"), 0, 0)
- updateLayout.addWidget(QLabel(str(len(debriefing.killed_aircrafts))), 0, 1)
+ updateLayout.addWidget(
+ QLabel(str(len(list(debriefing.air_losses.losses)))), 0, 1)
- updateLayout.addWidget(QLabel("Ground units destroyed"), 1, 0)
- updateLayout.addWidget(QLabel(str(len(debriefing.killed_ground_units))), 1, 1)
+ updateLayout.addWidget(
+ QLabel("Front line units destroyed"), 1, 0)
+ updateLayout.addWidget(
+ QLabel(str(len(list(debriefing.front_line_losses)))), 1, 1)
- #updateLayout.addWidget(QLabel("Weapons fired"), 2, 0)
- #updateLayout.addWidget(QLabel(str(len(debriefing.weapons_fired))), 2, 1)
+ updateLayout.addWidget(
+ QLabel("Other ground units destroyed"), 2, 0)
+ updateLayout.addWidget(
+ QLabel(str(len(list(debriefing.ground_object_losses)))), 2, 1)
- updateLayout.addWidget(QLabel("Base Capture Events"), 2, 0)
- updateLayout.addWidget(QLabel(str(len(debriefing.base_capture_events))), 2, 1)
+ updateLayout.addWidget(
+ QLabel("Buildings destroyed"), 3, 0)
+ updateLayout.addWidget(
+ QLabel(str(len(list(debriefing.building_losses)))), 3, 1)
+
+ updateLayout.addWidget(QLabel("Base Capture Events"), 4, 0)
+ updateLayout.addWidget(
+ QLabel(str(len(debriefing.base_capture_events))), 4, 1)
# Clear previous content of the window
for i in reversed(range(self.gridLayout.count())):
try:
self.gridLayout.itemAt(i).widget().setParent(None)
except:
- pass
+ logging.exception("Failed to clear window")
# Set new window content
self.gridLayout.addWidget(updateBox, 0, 0)
- if not debriefing.mission_ended:
+ if not debriefing.state_data.mission_ended:
self.gridLayout.addWidget(QLabel("Mission is being played"), 1, 0)
self.gridLayout.addWidget(self.actions, 2, 0)
else:
self.gridLayout.addWidget(QLabel("Mission is over"), 1, 0)
self.gridLayout.addWidget(self.actions2, 2, 0)
-
- def on_debriefing_udpate(self, debriefing):
+ def on_debriefing_update(self, debriefing: Debriefing) -> None:
try:
logging.info("On Debriefing update")
- print(debriefing)
+ logging.debug(debriefing)
DebriefingFileWrittenSignal.get_instance().sendDebriefing(debriefing)
- except Exception as e:
- logging.error("Got an error while sending debriefing")
- logging.error(e)
- self.wait_thread = wait_for_debriefing(lambda debriefing: self.on_debriefing_udpate(debriefing), self.game)
+ except Exception:
+ logging.exception("Got an error while sending debriefing")
+ self.wait_thread = wait_for_debriefing(
+ lambda d: self.on_debriefing_update(d), self.game, self.unit_map)
def process_debriefing(self):
self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing)
self.game.pass_turn()
- GameUpdateSignal.get_instance().sendDebriefing(self.game, self.gameEvent, self.debriefing)
+ GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
+ GameUpdateSignal.get_instance().updateGame(self.game)
self.close()
def debriefing_directory_location(self) -> str:
@@ -187,8 +206,8 @@ class QWaitingForMissionResultWindow(QDialog):
with open(file[0], "r") as json_file:
json_data = json.load(json_file)
json_data["mission_ended"] = True
- debriefing = Debriefing(json_data, self.game)
- self.on_debriefing_udpate(debriefing)
+ debriefing = Debriefing(json_data, self.game, self.unit_map)
+ self.on_debriefing_update(debriefing)
except Exception as e:
logging.error(e)
msg = QMessageBox()
diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py
index cf5e1a34..838da2ca 100644
--- a/qt_ui/windows/basemenu/QBaseMenu2.py
+++ b/qt_ui/windows/basemenu/QBaseMenu2.py
@@ -1,13 +1,24 @@
from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QPixmap
-from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget
+from PySide2.QtWidgets import (
+ QDialog,
+ QHBoxLayout,
+ QLabel,
+ QMessageBox,
+ QPushButton,
+ QVBoxLayout,
+ QWidget,
+)
+from game import db
+from game.theater import ControlPoint, ControlPointType
+from gen.flights.flight import FlightType
+from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import ControlPoint, ControlPointType
class QBaseMenu2(QDialog):
@@ -18,12 +29,8 @@ class QBaseMenu2(QDialog):
# Attrs
self.cp = cp
self.game_model = game_model
- self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]
self.objectName = "menuDialogue"
- # Widgets
- self.qbase_menu_tab = QBaseMenuTabs(cp, self.game_model)
-
try:
game = self.game_model.game
self.airport = game.theater.terrain.airport_by_id(self.cp.id)
@@ -40,15 +47,11 @@ class QBaseMenu2(QDialog):
self.setMinimumWidth(800)
self.setMaximumWidth(800)
self.setModal(True)
- self.initUi()
- def initUi(self):
self.setWindowTitle(self.cp.name)
- self.topLayoutWidget = QWidget()
- self.topLayout = QHBoxLayout()
- self.topLayoutWidget = QWidget()
- self.topLayout = QHBoxLayout()
+ base_menu_header = QWidget()
+ top_layout = QHBoxLayout()
header = QLabel(self)
header.setGeometry(0, 0, 655, 106)
@@ -58,28 +61,103 @@ class QBaseMenu2(QDialog):
title = QLabel("" + self.cp.name + "")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
- unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_planes, self.cp.base.total_armor,
- "Available" if self.cp.has_runway() else "Unavailable"))
- self.topLayout.addWidget(title)
- self.topLayout.addWidget(unitsPower)
- self.topLayout.setAlignment(Qt.AlignTop)
- self.topLayoutWidget.setProperty("style", "baseMenuHeader")
- self.topLayoutWidget.setLayout(self.topLayout)
+ self.intel_summary = QLabel()
+ self.update_intel_summary()
+ top_layout.addWidget(title)
+ top_layout.addWidget(self.intel_summary)
+ top_layout.setAlignment(Qt.AlignTop)
- self.mainLayout = QGridLayout()
- self.mainLayout.addWidget(header, 0, 0)
- self.mainLayout.addWidget(self.topLayoutWidget, 1, 0)
- self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0)
- totalBudget = QLabel(
+ self.repair_button = QPushButton()
+ self.repair_button.clicked.connect(self.begin_runway_repair)
+ self.update_repair_button()
+ top_layout.addWidget(self.repair_button)
+
+ base_menu_header.setProperty("style", "baseMenuHeader")
+ base_menu_header.setLayout(top_layout)
+
+ main_layout = QVBoxLayout()
+ main_layout.addWidget(header)
+ main_layout.addWidget(base_menu_header)
+ main_layout.addWidget(QBaseMenuTabs(cp, self.game_model))
+ bottom_row = QHBoxLayout()
+ main_layout.addLayout(bottom_row)
+
+ if FlightType.OCA_RUNWAY in self.cp.mission_types(for_player=True):
+ runway_attack_button = QPushButton("Attack airfield")
+ bottom_row.addWidget(runway_attack_button)
+
+ runway_attack_button.setProperty("style", "btn-danger")
+ runway_attack_button.clicked.connect(self.new_package)
+
+ budget_display = QLabel(
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
)
- totalBudget.setObjectName("budgetField")
- totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom)
- totalBudget.setProperty("style", "budget-label")
- self.mainLayout.addWidget(totalBudget)
- self.setLayout(self.mainLayout)
+ budget_display.setObjectName("budgetField")
+ budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
+ budget_display.setProperty("style", "budget-label")
+ bottom_row.addWidget(budget_display)
+ self.setLayout(main_layout)
- def closeEvent(self, closeEvent:QCloseEvent):
+ @property
+ def can_repair_runway(self) -> bool:
+ return self.cp.captured and self.cp.runway_can_be_repaired
+
+ @property
+ def can_afford_runway_repair(self) -> bool:
+ return self.game_model.game.budget >= db.RUNWAY_REPAIR_COST
+
+ def begin_runway_repair(self) -> None:
+ if not self.can_afford_runway_repair:
+ QMessageBox.critical(
+ self,
+ "Cannot repair runway",
+ f"Runway repair costs ${db.RUNWAY_REPAIR_COST}M but you have "
+ f"only ${self.game_model.game.budget}M available.",
+ QMessageBox.Ok)
+ return
+ if not self.can_repair_runway:
+ QMessageBox.critical(
+ self,
+ "Cannot repair runway",
+ f"Cannot repair this runway.", QMessageBox.Ok)
+ return
+
+ self.cp.begin_runway_repair()
+ self.game_model.game.budget -= db.RUNWAY_REPAIR_COST
+ self.update_repair_button()
+ self.update_intel_summary()
+ GameUpdateSignal.get_instance().updateGame(self.game_model.game)
+
+ def update_repair_button(self) -> None:
+ self.repair_button.setVisible(True)
+ turns_remaining = self.cp.runway_status.repair_turns_remaining
+ if self.cp.captured and turns_remaining is not None:
+ self.repair_button.setText("Repairing...")
+ self.repair_button.setDisabled(True)
+ return
+
+ if self.can_repair_runway:
+ if self.can_afford_runway_repair:
+ self.repair_button.setText(f"Repair ${db.RUNWAY_REPAIR_COST}M")
+ self.repair_button.setDisabled(False)
+ return
+ else:
+ self.repair_button.setText(
+ f"Cannot afford repair ${db.RUNWAY_REPAIR_COST}M")
+ self.repair_button.setDisabled(True)
+ return
+
+ self.repair_button.setVisible(False)
+ self.repair_button.setDisabled(True)
+
+ def update_intel_summary(self) -> None:
+ self.intel_summary.setText("\n".join([
+ f"{self.cp.base.total_aircraft} aircraft",
+ f"{self.cp.base.total_armor} ground units",
+ str(self.cp.runway_status)
+ ]))
+
+ def closeEvent(self, close_event: QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def get_base_image(self):
@@ -89,3 +167,13 @@ class QBaseMenu2(QDialog):
return "./resources/ui/lha.png"
else:
return "./resources/ui/airbase.png"
+
+ def new_package(self) -> None:
+ Dialog.open_new_package_dialog(self.cp, parent=self.window())
+
+ def update_dialogue_budget(self, budget: int):
+ GameUpdateSignal.get_instance().updateBudget(self.game_model.game)
+ for child in self.children():
+ if child.objectName() == "budgetField":
+ child.setText(
+ QRecruitBehaviour.BUDGET_FORMAT.format(budget))
diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py
index 0c82c86e..b7bb551a 100644
--- a/qt_ui/windows/basemenu/QBaseMenuTabs.py
+++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py
@@ -1,43 +1,33 @@
-from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget
+from PySide2.QtWidgets import QTabWidget
+from game.theater import ControlPoint, OffMapSpawn
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
-from theater import ControlPoint
class QBaseMenuTabs(QTabWidget):
def __init__(self, cp: ControlPoint, game_model: GameModel):
super(QBaseMenuTabs, self).__init__()
- self.cp = cp
- if cp:
-
- if not cp.captured:
- if not cp.is_carrier:
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
- self.intel = QIntelInfo(cp, game_model.game)
- self.addTab(self.intel, "Intel")
- else:
- if cp.has_runway():
- self.airfield_command = QAirfieldCommand(cp, game_model)
- self.addTab(self.airfield_command, "Airfield Command")
-
- if not cp.is_carrier:
- self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
- self.addTab(self.ground_forces_hq, "Ground Forces HQ")
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Base Defenses")
- else:
- self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
- self.addTab(self.base_defenses_hq, "Fleet")
+ if not cp.captured:
+ if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")
+ self.intel = QIntelInfo(cp, game_model.game)
+ self.addTab(self.intel, "Intel")
else:
- tabError = QFrame()
- l = QGridLayout()
- l.addWidget(QLabel("No Control Point"))
- tabError.setLayout(l)
- self.addTab(tabError, "No Control Point")
\ No newline at end of file
+ self.airfield_command = QAirfieldCommand(cp, game_model)
+ self.addTab(self.airfield_command, "Airfield Command")
+
+ if cp.is_carrier:
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Fleet")
+ elif not isinstance(cp, OffMapSpawn):
+ self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
+ self.addTab(self.ground_forces_hq, "Ground Forces HQ")
+ self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
+ self.addTab(self.base_defenses_hq, "Base Defenses")
\ No newline at end of file
diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py
index b41ac68a..8b5d84a1 100644
--- a/qt_ui/windows/basemenu/QRecruitBehaviour.py
+++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py
@@ -1,22 +1,26 @@
+import logging
+from typing import Type
+
from PySide2.QtWidgets import (
QGroupBox,
QHBoxLayout,
QLabel,
+ QLayout,
QPushButton,
QSizePolicy,
QSpacerItem,
)
-import logging
from dcs.unittype import UnitType
-from theater import db
-
+from game import db
+from game.event import UnitsDeliveryEvent
+from game.theater import ControlPoint
+from qt_ui.models import GameModel
class QRecruitBehaviour:
- game = None
- cp = None
- deliveryEvent = None
+ game_model: GameModel
+ cp: ControlPoint
existing_units_labels = None
bought_amount_labels = None
maximum_units = -1
@@ -24,12 +28,16 @@ class QRecruitBehaviour:
BUDGET_FORMAT = "Available Budget: ${}M"
def __init__(self) -> None:
- self.deliveryEvent = None
self.bought_amount_labels = {}
self.existing_units_labels = {}
self.recruitable_types = []
self.update_available_budget()
+ @property
+ def pending_deliveries(self) -> UnitsDeliveryEvent:
+ assert self.cp.pending_unit_deliveries
+ return self.cp.pending_unit_deliveries
+
@property
def budget(self) -> int:
return self.game_model.game.budget
@@ -38,7 +46,8 @@ class QRecruitBehaviour:
def budget(self, value: int) -> None:
self.game_model.game.budget = value
- def add_purchase_row(self, unit_type, layout, row):
+ def add_purchase_row(self, unit_type: Type[UnitType], layout: QLayout,
+ row: int, disabled: bool = False) -> int:
exist = QGroupBox()
exist.setProperty("style", "buy-box")
exist.setMaximumHeight(36)
@@ -47,7 +56,7 @@ class QRecruitBehaviour:
exist.setLayout(existLayout)
existing_units = self.cp.base.total_units_of_type(unit_type)
- scheduled_units = self.deliveryEvent.units.get(unit_type, 0)
+ scheduled_units = self.pending_deliveries.units.get(unit_type, 0)
unitName = QLabel("" + db.unit_type_name_2(unit_type) + "")
unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
@@ -73,6 +82,7 @@ class QRecruitBehaviour:
buy = QPushButton("+")
buy.setProperty("style", "btn-buy")
+ buy.setDisabled(disabled)
buy.setMinimumSize(16, 16)
buy.setMaximumSize(16, 16)
buy.clicked.connect(lambda: self.buy(unit_type))
@@ -80,6 +90,7 @@ class QRecruitBehaviour:
sell = QPushButton("-")
sell.setProperty("style", "btn-sell")
+ sell.setDisabled(disabled)
sell.setMinimumSize(16, 16)
sell.setMaximumSize(16, 16)
sell.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
@@ -100,10 +111,10 @@ class QRecruitBehaviour:
return row + 1
- def _update_count_label(self, unit_type: UnitType):
+ def _update_count_label(self, unit_type: Type[UnitType]):
self.bought_amount_labels[unit_type].setText("{}".format(
- unit_type in self.deliveryEvent.units and "{}".format(self.deliveryEvent.units[unit_type]) or "0"
+ unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0"
))
self.existing_units_labels[unit_type].setText("{}".format(
@@ -114,22 +125,12 @@ class QRecruitBehaviour:
parent = self.parent()
while parent.objectName != "menuDialogue":
parent = parent.parent()
- for child in parent.children():
- if child.objectName() == "budgetField":
- child.setText(
- QRecruitBehaviour.BUDGET_FORMAT.format(self.budget))
-
- def buy(self, unit_type):
-
- if self.maximum_units > 0:
- if self.total_units + 1 > self.maximum_units:
- logging.info("Not enough space left !")
- # TODO : display modal warning
- return
+ parent.update_dialogue_budget(self.budget)
+ def buy(self, unit_type: Type[UnitType]):
price = db.PRICES[unit_type]
if self.budget >= price:
- self.deliveryEvent.deliver({unit_type: 1})
+ self.pending_deliveries.deliver({unit_type: 1})
self.budget -= price
else:
# TODO : display modal warning
@@ -138,12 +139,12 @@ class QRecruitBehaviour:
self.update_available_budget()
def sell(self, unit_type):
- if self.deliveryEvent.units.get(unit_type, 0) > 0:
+ if self.pending_deliveries.units.get(unit_type, 0) > 0:
price = db.PRICES[unit_type]
self.budget += price
- self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1
- if self.deliveryEvent.units[unit_type] == 0:
- del self.deliveryEvent.units[unit_type]
+ self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1
+ if self.pending_deliveries.units[unit_type] == 0:
+ del self.pending_deliveries.units[unit_type]
elif self.cp.base.total_units_of_type(unit_type) > 0:
price = db.PRICES[unit_type]
self.budget += price
@@ -152,25 +153,6 @@ class QRecruitBehaviour:
self._update_count_label(unit_type)
self.update_available_budget()
- @property
- def total_units(self):
-
- total = 0
- for unit_type in self.recruitables_types:
- total += self.cp.base.total_units(unit_type)
- print(unit_type, total, self.cp.base.total_units(unit_type))
- print("--------------------------------")
-
- if self.deliveryEvent:
- for unit_bought in self.deliveryEvent.units:
- if db.unit_task(unit_bought) in self.recruitables_types:
- total += self.deliveryEvent.units[unit_bought]
- print(unit_bought, total, self.deliveryEvent.units[unit_bought])
-
- print("=============================")
-
- return total
-
def set_maximum_units(self, maximum_units):
"""
Set the maximum number of units that can be bought
@@ -181,4 +163,4 @@ class QRecruitBehaviour:
"""
Set the maximum number of units that can be bought
"""
- self.recruitables_types = recruitables_types
\ No newline at end of file
+ self.recruitables_types = recruitables_types
diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
index a01aaaa9..82c7033d 100644
--- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py
@@ -1,4 +1,5 @@
-from typing import Optional, Set
+import logging
+from typing import Optional, Set, Type
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (
@@ -11,13 +12,14 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
-from dcs.unittype import UnitType
+from dcs.task import CAP, CAS
+from dcs.unittype import FlyingType, UnitType
-from game.event.event import UnitsDeliveryEvent
+from game import db
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.uiconstants import ICONS
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import CAP, CAS, ControlPoint, db
class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
@@ -25,25 +27,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
QFrame.__init__(self)
self.cp = cp
self.game_model = game_model
- self.deliveryEvent: Optional[UnitsDeliveryEvent] = None
self.bought_amount_labels = {}
self.existing_units_labels = {}
- for event in self.game_model.game.events:
- if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
- self.deliveryEvent = event
- if not self.deliveryEvent:
- self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
-
# Determine maximum number of aircrafts that can be bought
- self.set_maximum_units(self.cp.available_aircraft_slots)
+ self.set_maximum_units(self.cp.total_aircraft_parking)
self.set_recruitable_types([CAP, CAS])
self.bought_amount_labels = {}
self.existing_units_labels = {}
- self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status = QHangarStatus(game_model, self.cp)
self.init_ui()
@@ -56,12 +51,14 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
task_box_layout = QGridLayout()
row = 0
- unit_types: Set[UnitType] = set()
+ unit_types: Set[Type[FlyingType]] = set()
for task in tasks:
units = db.find_unittype(task, self.game_model.game.player_name)
if not units:
continue
for unit in units:
+ if not issubclass(unit, FlyingType):
+ continue
if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE:
continue
if self.cp.is_lha and unit not in db.LHA_CAPABLE:
@@ -70,7 +67,9 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
for unit_type in sorted_units:
- row = self.add_purchase_row(unit_type, task_box_layout, row)
+ row = self.add_purchase_row(
+ unit_type, task_box_layout, row,
+ disabled=not self.cp.can_operate(unit_type))
stretch = QVBoxLayout()
stretch.addStretch()
task_box_layout.addLayout(stretch, row, 0)
@@ -86,13 +85,18 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
self.setLayout(main_layout)
def buy(self, unit_type):
+ if self.maximum_units > 0:
+ if self.cp.unclaimed_parking(self.game_model.game) <= 0:
+ logging.debug(f"No space for additional aircraft at {self.cp}.")
+ return
+
super().buy(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
# orders.
- if self.deliveryEvent.units.get(unit_type, 0) <= 0:
+ if self.pending_deliveries.units.get(unit_type, 0) <= 0:
global_inventory = self.game_model.game.aircraft_inventory
inventory = global_inventory.for_control_point(self.cp)
try:
@@ -105,22 +109,44 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
"assigned to a mission?", QMessageBox.Ok)
return
super().sell(unit_type)
- self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
+ self.hangar_status.update_label()
class QHangarStatus(QHBoxLayout):
- def __init__(self, current_amount: int, max_amount: int):
- super(QHangarStatus, self).__init__()
+ def __init__(self, game_model: GameModel,
+ control_point: ControlPoint) -> None:
+ super().__init__()
+ self.game_model = game_model
+ self.control_point = control_point
+
self.icon = QLabel()
self.icon.setPixmap(ICONS["Hangar"])
self.text = QLabel("")
- self.update_label(current_amount, max_amount)
+ self.update_label()
self.addWidget(self.icon, Qt.AlignLeft)
self.addWidget(self.text, Qt.AlignLeft)
self.addStretch(50)
self.setAlignment(Qt.AlignLeft)
- def update_label(self, current_amount: int, max_amount: int):
- self.text.setText("{}/{}".format(current_amount, max_amount))
+ def update_label(self) -> None:
+ next_turn = self.control_point.expected_aircraft_next_turn(
+ self.game_model.game)
+ max_amount = self.control_point.total_aircraft_parking
+
+ components = [f"{next_turn.present} present"]
+ if next_turn.ordered > 0:
+ components.append(f"{next_turn.ordered} purchased")
+ elif next_turn.ordered < 0:
+ components.append(f"{-next_turn.ordered} sold")
+
+ transferring = next_turn.transferring
+ if transferring > 0:
+ components.append(f"{transferring} transferring in")
+ if transferring < 0:
+ components.append(f"{-transferring} transferring out")
+
+ details = ", ".join(components)
+ self.text.setText(
+ f"{next_turn.total}/{max_amount} ({details})")
diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
index 9965115a..97c804bf 100644
--- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
+++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py
@@ -1,10 +1,10 @@
from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \
QAircraftRecruitmentMenu
from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView
-from theater import ControlPoint
class QAirfieldCommand(QFrame):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
index 350cf5e8..6d46b35b 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefenseGroupInfo.py
@@ -1,10 +1,16 @@
from PySide2.QtCore import Qt
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
+from PySide2.QtWidgets import (
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QPushButton,
+ QVBoxLayout,
+)
+from game.theater import ControlPoint, TheaterGroundObject
from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
-from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
index 5ad1f6c9..75a45eb0 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseDefensesHQ.py
@@ -1,7 +1,9 @@
from PySide2.QtWidgets import QFrame, QGridLayout
+
from game import Game
-from qt_ui.windows.basemenu.base_defenses.QBaseInformation import QBaseInformation
-from theater import ControlPoint
+from game.theater import ControlPoint
+from qt_ui.windows.basemenu.base_defenses.QBaseInformation import \
+ QBaseInformation
class QBaseDefensesHQ(QFrame):
diff --git a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
index f5325887..3c920080 100644
--- a/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
+++ b/qt_ui/windows/basemenu/base_defenses/QBaseInformation.py
@@ -1,10 +1,15 @@
from PySide2.QtGui import Qt
-from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QVBoxLayout, QFrame, QWidget, QScrollArea
+from PySide2.QtWidgets import (
+ QFrame,
+ QGridLayout,
+ QScrollArea,
+ QVBoxLayout,
+ QWidget,
+)
-from game import db
-from qt_ui.uiconstants import AIRCRAFT_ICONS, VEHICLES_ICONS
-from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import QBaseDefenseGroupInfo
-from theater import ControlPoint, Airport
+from game.theater import Airport, ControlPoint
+from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import \
+ QBaseDefenseGroupInfo
class QBaseInformation(QFrame):
@@ -25,7 +30,7 @@ class QBaseInformation(QFrame):
scroll_content.setLayout(task_box_layout)
for g in self.cp.ground_objects:
- if g.airbase_group:
+ if g.airbase_group and len(g.groups) > 0:
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
task_box_layout.addWidget(group_info)
diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
index ec1cabf6..c359eaaf 100644
--- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
+++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py
@@ -6,11 +6,12 @@ from PySide2.QtWidgets import (
QVBoxLayout,
QWidget,
)
+from dcs.task import PinpointStrike
-from game.event import UnitsDeliveryEvent
+from game import db
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour
-from theater import ControlPoint, PinpointStrike, db
class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
@@ -23,12 +24,6 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
self.bought_amount_labels = {}
self.existing_units_labels = {}
- for event in self.game_model.game.events:
- if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp:
- self.deliveryEvent = event
- if not self.deliveryEvent:
- self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp)
-
self.init_ui()
def init_ui(self):
@@ -61,4 +56,4 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
main_layout.addWidget(scroll)
- self.setLayout(main_layout)
\ No newline at end of file
+ self.setLayout(main_layout)
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
index bb18594f..39cba843 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py
@@ -1,11 +1,11 @@
from PySide2.QtWidgets import QFrame, QGridLayout
+from game.theater import ControlPoint
from qt_ui.models import GameModel
from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \
QArmorRecruitmentMenu
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \
QGroundForcesStrategy
-from theater import ControlPoint
class QGroundForcesHQ(QFrame):
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
index 0b7b4db6..3aee8c50 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py
@@ -1,8 +1,9 @@
-from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout
+from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout
from game import Game
-from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
-from theater import ControlPoint
+from game.theater import ControlPoint
+from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import \
+ QGroundForcesStrategySelector
class QGroundForcesStrategy(QGroupBox):
diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
index 09c3fa5b..4acd8731 100644
--- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
+++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py
@@ -1,6 +1,6 @@
from PySide2.QtWidgets import QComboBox
-from theater import ControlPoint, CombatStance
+from game.theater import CombatStance, ControlPoint
class QGroundForcesStrategySelector(QComboBox):
diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py
index bc7cb13b..e422ef3a 100644
--- a/qt_ui/windows/basemenu/intel/QIntelInfo.py
+++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py
@@ -1,11 +1,14 @@
+from PySide2.QtWidgets import (
+ QFrame,
+ QGridLayout,
+ QGroupBox,
+ QLabel,
+ QVBoxLayout,
+)
+from dcs.task import CAP, CAS, Embarking, PinpointStrike
-
-from PySide2.QtWidgets import QLabel, QGroupBox, QVBoxLayout, QFrame, QGridLayout
-from dcs.task import Embarking, CAS, PinpointStrike, CAP
-
-from game import Game
-from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import QGroundForcesStrategySelector
-from theater import ControlPoint, db
+from game import Game, db
+from game.theater import ControlPoint
class QIntelInfo(QFrame):
diff --git a/qt_ui/windows/finances/QFinancesMenu.py b/qt_ui/windows/finances/QFinancesMenu.py
index 362f642c..67cd27c9 100644
--- a/qt_ui/windows/finances/QFinancesMenu.py
+++ b/qt_ui/windows/finances/QFinancesMenu.py
@@ -64,7 +64,11 @@ class QFinancesMenu(QDialog):
self.setLayout(layout)
- layout.addWidget(QHorizontalSeparationLine(), i+1, 0, 1, 3)
-
- layout.addWidget(QLabel("" + str(self.game.budget_reward_amount) + "M "), i+2, 2)
-
+ layout.addWidget(QHorizontalSeparationLine(), i + 1, 0, 1, 3)
+ layout.addWidget(QLabel(
+ f"Income multiplier: {game.settings.player_income_multiplier:.1f}"),
+ i + 2, 1
+ )
+ layout.addWidget(
+ QLabel("" + str(self.game.budget_reward_amount) + "M "),
+ i + 2, 2)
diff --git a/qt_ui/windows/groundobject/QBuildingInfo.py b/qt_ui/windows/groundobject/QBuildingInfo.py
index e474a59f..fcf6366b 100644
--- a/qt_ui/windows/groundobject/QBuildingInfo.py
+++ b/qt_ui/windows/groundobject/QBuildingInfo.py
@@ -2,7 +2,7 @@ import os
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QLabel
-
+from game.db import REWARDS
class QBuildingInfo(QGroupBox):
@@ -28,6 +28,13 @@ class QBuildingInfo(QGroupBox):
layout = QVBoxLayout()
layout.addWidget(self.header)
layout.addWidget(self.name)
+
+ if self.building.category in REWARDS.keys():
+ income_label_text = 'Value: ' + str(REWARDS[self.building.category]) + "M"
+ if self.building.is_dead:
+ income_label_text = '' + income_label_text + ''
+ self.reward = QLabel(income_label_text)
+ layout.addWidget(self.reward)
+
footer = QHBoxLayout()
self.setLayout(layout)
-
diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py
index dcfed0a3..d40bb720 100644
--- a/qt_ui/windows/groundobject/QGroundObjectMenu.py
+++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py
@@ -2,20 +2,31 @@ import logging
from PySide2 import QtCore
from PySide2.QtGui import Qt
-from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \
- QComboBox, QSpinBox, QMessageBox
+from PySide2.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QGridLayout,
+ QGroupBox,
+ QHBoxLayout,
+ QLabel,
+ QMessageBox,
+ QPushButton,
+ QSpinBox,
+ QVBoxLayout,
+)
from dcs import Point
from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS
-from game.db import PRICES, unit_type_of, PinpointStrike
-from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
+from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
+from game.theater import ControlPoint, TheaterGroundObject
+from gen.defenses.armor_group_generator import \
+ generate_armor_group_of_type_and_size
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
-from theater import ControlPoint, TheaterGroundObject
class QGroundObjectMenu(QDialog):
@@ -51,6 +62,8 @@ class QGroundObjectMenu(QDialog):
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
+ if self.cp.captured:
+ self.mainLayout.addWidget(self.financesBox)
self.actionLayout = QHBoxLayout()
@@ -104,12 +117,28 @@ class QGroundObjectMenu(QDialog):
self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout()
+
j = 0
+ total_income = 0
+ received_income = 0
for i, building in enumerate(self.buildings):
if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
self.buildingsLayout.addWidget(QBuildingInfo(building, self.ground_object), j/3, j%3)
j = j + 1
+ if building.category in REWARDS.keys():
+ total_income = total_income + REWARDS[building.category]
+ if not building.is_dead:
+ received_income = received_income + REWARDS[building.category]
+ else:
+ logging.warning(building.category + " not in REWARDS")
+
+ self.financesBox = QGroupBox("Finances: ")
+ self.financesBoxLayout = QGridLayout()
+ self.financesBoxLayout.addWidget(QLabel("Available: " + str(total_income) + "M"), 2, 1)
+ self.financesBoxLayout.addWidget(QLabel("Receiving: " + str(received_income) + "M"), 2, 2)
+
+ self.financesBox.setLayout(self.financesBoxLayout)
self.buildingBox.setLayout(self.buildingsLayout)
self.intelBox.setLayout(self.intelLayout)
@@ -161,6 +190,7 @@ class QGroundObjectMenu(QDialog):
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
group.units.append(unit)
GameUpdateSignal.get_instance().updateGame(self.game)
+ self.parent().update_dialogue_budget(self.game.budget)
# Remove destroyed units in the vicinity
destroyed_units = self.game.get_destroyed_units()
@@ -180,6 +210,7 @@ class QGroundObjectMenu(QDialog):
self.ground_object.groups = []
self.do_refresh_layout()
GameUpdateSignal.get_instance().updateBudget(self.game)
+ self.parent().update_dialogue_budget(self.game.budget)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(self, self.ground_object, self.cp, self.game, self.total_value)
@@ -219,7 +250,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.init_ui()
def init_ui(self):
- faction = self.game.player_name
+ faction = self.game.player_faction
# Sams
@@ -239,7 +270,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
# Armored units
- armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense
+ armored_units = db.find_unittype(PinpointStrike, faction.name) # Todo : refactor this legacy nonsense
for unit in set(armored_units):
self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
@@ -288,9 +319,9 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]")
def buyArmor(self):
- print("Buy Armor ")
+ logging.info("Buying Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
- print(utype)
+ logging.info(utype)
price = db.PRICES[utype] * self.amount.value() - self.current_group_value
if price > self.game.budget:
self.error_money()
@@ -304,6 +335,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.ground_object.groups = [group]
GameUpdateSignal.get_instance().updateBudget(self.game)
+ self.parent().parent().update_dialogue_budget(self.game.budget)
self.changed.emit()
self.close()
@@ -324,6 +356,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.ground_object.groups = [generated_group]
GameUpdateSignal.get_instance().updateBudget(self.game)
+ self.parent().parent().update_dialogue_budget(self.game.budget)
self.changed.emit()
self.close()
diff --git a/qt_ui/windows/infos/QInfoItem.py b/qt_ui/windows/infos/QInfoItem.py
index be5cf333..f8d63bf2 100644
--- a/qt_ui/windows/infos/QInfoItem.py
+++ b/qt_ui/windows/infos/QInfoItem.py
@@ -8,5 +8,5 @@ class QInfoItem(QStandardItem):
def __init__(self, info: Information):
super(QInfoItem, self).__init__()
self.info = info
- self.setText("[%02d]" % self.info.turn + " " + self.info.title + ' : {:<16}'.format(info.text))
+ self.setText(str(info))
self.setEditable(False)
diff --git a/qt_ui/windows/mission/QFlightItem.py b/qt_ui/windows/mission/QFlightItem.py
index 78966dbf..3ba73a64 100644
--- a/qt_ui/windows/mission/QFlightItem.py
+++ b/qt_ui/windows/mission/QFlightItem.py
@@ -1,5 +1,3 @@
-import datetime
-
from PySide2.QtGui import QStandardItem, QIcon
from game import db
@@ -23,6 +21,4 @@ class QFlightItem(QStandardItem):
self.setEditable(False)
estimator = TotEstimator(self.package)
delay = estimator.mission_start_time(flight)
- self.setText("["+str(self.flight.flight_type.name[:6])+"] "
- + str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
- + " in " + str(delay))
+ self.setText(f"{flight} in {delay}")
diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py
index 6298379f..2cee9ba6 100644
--- a/qt_ui/windows/mission/QPackageDialog.py
+++ b/qt_ui/windows/mission/QPackageDialog.py
@@ -8,6 +8,7 @@ from PySide2.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
+ QMessageBox,
QPushButton,
QTimeEdit,
QVBoxLayout,
@@ -16,14 +17,14 @@ from PySide2.QtWidgets import (
from game.game import Game
from gen.ato import Package
from gen.flights.flight import Flight
-from gen.flights.flightplan import FlightPlanBuilder
+from gen.flights.flightplan import FlightPlanBuilder, PlanningError
from gen.flights.traveltime import TotEstimator
from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.ato import QFlightList
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
-from theater.missiontarget import MissionTarget
+from game.theater.missiontarget import MissionTarget
class QPackageDialog(QDialog):
@@ -167,7 +168,15 @@ class QPackageDialog(QDialog):
self.package_model.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package_model.package,
is_player=True)
- planner.populate_flight_plan(flight)
+ try:
+ planner.populate_flight_plan(flight)
+ except PlanningError as ex:
+ self.game.aircraft_inventory.return_from_flight(flight)
+ self.package_model.delete_flight(flight)
+ logging.exception("Could not create flight")
+ QMessageBox.critical(
+ self, "Could not create flight", str(ex), QMessageBox.Ok
+ )
# noinspection PyUnresolvedReferences
self.package_changed.emit()
diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py
index 2c602d56..1ca6e845 100644
--- a/qt_ui/windows/mission/QPlannedFlightsView.py
+++ b/qt_ui/windows/mission/QPlannedFlightsView.py
@@ -4,7 +4,7 @@ from PySide2.QtWidgets import QAbstractItemView, QListView
from qt_ui.models import GameModel
from qt_ui.windows.mission.QFlightItem import QFlightItem
-from theater.controlpoint import ControlPoint
+from game.theater.controlpoint import ControlPoint
class QPlannedFlightsView(QListView):
diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py
index f4fe6041..0e0bf773 100644
--- a/qt_ui/windows/mission/flight/QFlightCreator.py
+++ b/qt_ui/windows/mission/flight/QFlightCreator.py
@@ -10,15 +10,17 @@ from PySide2.QtWidgets import (
from dcs.planes import PlaneType
from game import Game
+from game.theater import ControlPoint, OffMapSpawn
from gen.ato import Package
from gen.flights.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
+from qt_ui.widgets.combos.QArrivalAirfieldSelector import \
+ QArrivalAirfieldSelector
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
-from theater import ControlPoint
class QFlightCreator(QDialog):
@@ -49,16 +51,30 @@ class QFlightCreator(QDialog):
self.on_aircraft_changed)
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
- self.airfield_selector = QOriginAirfieldSelector(
+ self.departure = QOriginAirfieldSelector(
self.game.aircraft_inventory,
[cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData()
)
- self.airfield_selector.availability_changed.connect(self.update_max_size)
- layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
+ self.departure.availability_changed.connect(self.update_max_size)
+ layout.addLayout(QLabeledWidget("Departure:", self.departure))
+
+ self.arrival = QArrivalAirfieldSelector(
+ [cp for cp in game.theater.controlpoints if cp.captured],
+ self.aircraft_selector.currentData(),
+ "Same as departure"
+ )
+ layout.addLayout(QLabeledWidget("Arrival:", self.arrival))
+
+ self.divert = QArrivalAirfieldSelector(
+ [cp for cp in game.theater.controlpoints if cp.captured],
+ self.aircraft_selector.currentData(),
+ "None"
+ )
+ layout.addLayout(QLabeledWidget("Divert:", self.divert))
self.flight_size_spinner = QFlightSizeSpinner()
- self.update_max_size(self.airfield_selector.available)
+ self.update_max_size(self.departure.available)
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner(
@@ -82,10 +98,16 @@ class QFlightCreator(QDialog):
def verify_form(self) -> Optional[str]:
aircraft: PlaneType = self.aircraft_selector.currentData()
- origin: ControlPoint = self.airfield_selector.currentData()
+ origin: ControlPoint = self.departure.currentData()
+ arrival: ControlPoint = self.arrival.currentData()
+ divert: ControlPoint = self.divert.currentData()
size: int = self.flight_size_spinner.value()
if not origin.captured:
return f"{origin.name} is not owned by your coalition."
+ if arrival is not None and not arrival.captured:
+ return f"{arrival.name} is not owned by your coalition."
+ if divert is not None and not divert.captured:
+ return f"{divert.name} is not owned by your coalition."
available = origin.base.aircraft.get(aircraft, 0)
if not available:
return f"{origin.name} has no {aircraft.id} available."
@@ -104,14 +126,22 @@ class QFlightCreator(QDialog):
task = self.task_selector.currentData()
aircraft = self.aircraft_selector.currentData()
- origin = self.airfield_selector.currentData()
+ origin = self.departure.currentData()
+ arrival = self.arrival.currentData()
+ divert = self.divert.currentData()
size = self.flight_size_spinner.value()
- if self.game.settings.perf_ai_parking_start:
+ if arrival is None:
+ arrival = origin
+
+ if isinstance(origin, OffMapSpawn):
+ start_type = "In Flight"
+ elif self.game.settings.perf_ai_parking_start:
start_type = "Cold"
else:
start_type = "Warm"
- flight = Flight(self.package, aircraft, size, origin, task, start_type)
+ flight = Flight(self.package, aircraft, size, task, start_type, origin,
+ arrival, divert)
flight.client_count = self.client_slots_spinner.value()
# noinspection PyUnresolvedReferences
@@ -120,7 +150,9 @@ class QFlightCreator(QDialog):
def on_aircraft_changed(self, index: int) -> None:
new_aircraft = self.aircraft_selector.itemData(index)
- self.airfield_selector.change_aircraft(new_aircraft)
+ self.departure.change_aircraft(new_aircraft)
+ self.arrival.change_aircraft(new_aircraft)
+ self.divert.change_aircraft(new_aircraft)
def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4))
diff --git a/qt_ui/windows/mission/flight/QFlightWaypointsEditor.py b/qt_ui/windows/mission/flight/QFlightWaypointsEditor.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py b/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
index c1e82ef3..76e7f815 100644
--- a/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
+++ b/qt_ui/windows/mission/flight/settings/QFlightTypeTaskInfo.py
@@ -16,8 +16,8 @@ class QFlightTypeTaskInfo(QGroupBox):
if db.unit_type_name(self.flight.unit_type) in AIRCRAFT_ICONS:
self.aircraft_icon.setPixmap(AIRCRAFT_ICONS[db.unit_type_name(self.flight.unit_type)])
- self.task = QLabel("Task :")
- self.task_type = QLabel(flight.flight_type.name)
+ self.task = QLabel("Task:")
+ self.task_type = QLabel(str(flight.flight_type))
self.task_type.setProperty("style", flight.flight_type.name)
layout.addWidget(self.aircraft_icon, 0, 0)
diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
index 381d8e39..9f5df938 100644
--- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
+++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py
@@ -1,4 +1,3 @@
-import itertools
from datetime import timedelta
from PySide2.QtCore import QItemSelectionModel, QPoint
@@ -7,7 +6,8 @@ from PySide2.QtWidgets import QHeaderView, QTableView
from game.utils import meter_to_feet
from gen.ato import Package
-from gen.flights.flight import Flight, FlightWaypoint
+from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
+from gen.flights.traveltime import TotEstimator
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
QWaypointItem
@@ -32,25 +32,13 @@ class QFlightWaypointList(QTableView):
self.update_list()
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select)
- self.selectionModel().selectionChanged.connect(self.on_waypoint_selected_changed)
-
- def on_waypoint_selected_changed(self):
- index = self.selectionModel().currentIndex().row()
def update_list(self):
self.model.clear()
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
- # The first waypoint is set up by pydcs at mission generation time, so
- # we need to add that waypoint manually.
- takeoff = FlightWaypoint(self.flight.from_cp.position.x,
- self.flight.from_cp.position.y, 0)
- takeoff.description = "Take Off"
- takeoff.name = takeoff.pretty_name = "Take Off from " + self.flight.from_cp.name
- takeoff.alt_type = "RADIO"
-
- waypoints = itertools.chain([takeoff], self.flight.points)
+ waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
@@ -73,8 +61,9 @@ class QFlightWaypointList(QTableView):
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
- @staticmethod
- def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str:
+ def tot_text(self, flight: Flight, waypoint: FlightWaypoint) -> str:
+ if waypoint.waypoint_type == FlightWaypointType.TAKEOFF:
+ return self.takeoff_text(flight)
prefix = ""
time = flight.flight_plan.tot_for_waypoint(waypoint)
if time is None:
@@ -84,3 +73,12 @@ class QFlightWaypointList(QTableView):
return ""
time = timedelta(seconds=int(time.total_seconds()))
return f"{prefix}T+{time}"
+
+ def takeoff_text(self, flight: Flight) -> str:
+ estimator = TotEstimator(self.package)
+ takeoff_time = estimator.takeoff_time_for_flight(flight)
+ # Handle custom flight plans where we can't estimate the takeoff time.
+ if takeoff_time is None:
+ takeoff_time = timedelta()
+ start_time = timedelta(seconds=int(takeoff_time.total_seconds()))
+ return f"T+{start_time}"
diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
index c480da2a..3795bf1e 100644
--- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
+++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
@@ -1,3 +1,4 @@
+import logging
from typing import Iterable, List, Optional
from PySide2.QtCore import Signal
@@ -16,6 +17,7 @@ from gen.flights.flight import Flight, FlightType, FlightWaypoint
from gen.flights.flightplan import (
CustomFlightPlan,
FlightPlanBuilder,
+ PlanningError,
StrikeFlightPlan,
)
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
@@ -23,7 +25,6 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
from qt_ui.windows.mission.flight.waypoints \
.QPredefinedWaypointSelectionWindow import \
QPredefinedWaypointSelectionWindow
-from theater import FrontLine
class QFlightWaypointTab(QFrame):
@@ -57,22 +58,13 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("Generator :"))
rlayout.addWidget(QLabel("AI compatible"))
- # TODO: Filter by objective type.
self.recreate_buttons.clear()
- recreate_types = [
- FlightType.CAS,
- FlightType.CAP,
- FlightType.DEAD,
- FlightType.ESCORT,
- FlightType.SEAD,
- FlightType.STRIKE
- ]
- for task in recreate_types:
+ for task in self.package.target.mission_types(for_player=True):
def make_closure(arg):
def closure():
return self.confirm_recreate(arg)
return closure
- button = QPushButton(f"Recreate as {task.name}")
+ button = QPushButton(f"Recreate as {task}")
button.clicked.connect(make_closure(task))
rlayout.addWidget(button)
self.recreate_buttons.append(button)
@@ -112,7 +104,8 @@ class QFlightWaypointTab(QFrame):
return
self.degrade_to_custom_flight_plan()
- self.flight.flight_plan.waypoints.remove(waypoint)
+ assert isinstance(self.flight.flight_plan, CustomFlightPlan)
+ self.flight.flight_plan.custom_waypoints.remove(waypoint)
def on_fast_waypoint(self):
self.subwindow = QPredefinedWaypointSelectionWindow(self.game, self.flight, self.flight_waypoint_list)
@@ -152,24 +145,20 @@ class QFlightWaypointTab(QFrame):
QMessageBox.No,
QMessageBox.Yes
)
+ original_task = self.flight.flight_type
if result == QMessageBox.Yes:
- # TODO: Should be buttons for both BARCAP and TARCAP.
- # BARCAP and TARCAP behave differently. TARCAP arrives a few minutes
- # ahead of the rest of the package and stays until the package
- # departs, whereas BARCAP usually isn't part of a strike package and
- # has a fixed mission time.
- if task == FlightType.CAP:
- if isinstance(self.package.target, FrontLine):
- task = FlightType.TARCAP
- else:
- task = FlightType.BARCAP
self.flight.flight_type = task
- self.planner.populate_flight_plan(self.flight)
+ try:
+ self.planner.populate_flight_plan(self.flight)
+ except PlanningError as ex:
+ self.flight.flight_type = original_task
+ logging.exception("Could not recreate flight")
+ QMessageBox.critical(
+ self, "Could not recreate flight", str(ex), QMessageBox.Ok
+ )
self.flight_waypoint_list.update_list()
self.on_change()
def on_change(self):
self.flight_waypoint_list.update_list()
self.on_flight_changed.emit()
-
-
diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py
index 617869bc..6f981694 100644
--- a/qt_ui/windows/newgame/QCampaignList.py
+++ b/qt_ui/windows/newgame/QCampaignList.py
@@ -4,7 +4,7 @@ import json
import logging
from dataclasses import dataclass
from pathlib import Path
-from typing import List
+from typing import Any, Dict, List
from PySide2 import QtGui
from PySide2.QtCore import QItemSelectionModel
@@ -12,7 +12,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView
import qt_ui.uiconstants as CONST
-from theater import ConflictTheater
+from game.theater import ConflictTheater
@dataclass(frozen=True)
@@ -21,7 +21,8 @@ class Campaign:
icon_name: str
authors: str
description: str
- theater: ConflictTheater
+ data: Dict[str, Any]
+ path: Path
@classmethod
def from_json(cls, path: Path) -> Campaign:
@@ -29,14 +30,23 @@ class Campaign:
data = json.load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
- return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"),
- data.get("description", ""), ConflictTheater.from_json(data))
+ return cls(
+ data["name"],
+ f"Terrain_{sanitized_theater}",
+ data.get("authors", "???"),
+ data.get("description", ""),
+ data,
+ path
+ )
+
+ def load_theater(self) -> ConflictTheater:
+ return ConflictTheater.from_json(self.path.parent, self.data)
def load_campaigns() -> List[Campaign]:
campaign_dir = Path("resources\\campaigns")
campaigns = []
- for path in campaign_dir.iterdir():
+ for path in campaign_dir.glob("*.json"):
try:
logging.debug(f"Loading campaign from {path}...")
campaign = Campaign.from_json(path)
diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py
index e82357dc..3a14fafb 100644
--- a/qt_ui/windows/newgame/QNewGameWizard.py
+++ b/qt_ui/windows/newgame/QNewGameWizard.py
@@ -10,12 +10,13 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
from game import db
from game.settings import Settings
+from qt_ui.widgets.spinsliders import TenthsSpinSlider
from qt_ui.windows.newgame.QCampaignList import (
Campaign,
QCampaignList,
load_campaigns,
)
-from theater.start_generator import GameGenerator
+from game.theater.start_generator import GameGenerator, GeneratorSettings
jinja_env = Environment(
loader=FileSystemLoader("resources/ui/templates"),
@@ -28,6 +29,10 @@ jinja_env = Environment(
lstrip_blocks=True,
)
+
+DEFAULT_BUDGET = 650
+
+
class NewGameWizard(QtWidgets.QWizard):
def __init__(self, parent=None):
super(NewGameWizard, self).__init__(parent)
@@ -37,7 +42,8 @@ class NewGameWizard(QtWidgets.QWizard):
self.addPage(IntroPage())
self.addPage(FactionSelection())
self.addPage(TheaterConfiguration(self.campaigns))
- self.addPage(MiscOptions())
+ self.addPage(GeneratorOptions())
+ self.addPage(DifficultyAndAutomationOptions())
self.addPage(ConclusionPage())
self.setPixmap(QtWidgets.QWizard.WatermarkPixmap,
@@ -51,40 +57,47 @@ class NewGameWizard(QtWidgets.QWizard):
logging.info("New Game Wizard accept")
logging.info("======================")
- blueFaction = [c for c in db.FACTIONS][self.field("blueFaction")]
- redFaction = [c for c in db.FACTIONS][self.field("redFaction")]
+ campaign = self.field("selectedCampaign")
+ if campaign is None:
+ campaign = self.campaigns[0]
- selectedCampaign = self.field("selectedCampaign")
- if selectedCampaign is None:
- selectedCampaign = self.campaigns[0]
+ settings = Settings(
+ player_income_multiplier=self.field(
+ "player_income_multiplier") / 10,
+ enemy_income_multiplier=self.field("enemy_income_multiplier") / 10,
+ automate_runway_repair=self.field("automate_runway_repairs"),
+ automate_front_line_reinforcements=self.field(
+ "automate_front_line_purchases"
+ ),
+ automate_aircraft_reinforcements=self.field(
+ "automate_aircraft_purchases"
+ ),
+ supercarrier=self.field("supercarrier")
+ )
+ generator_settings = GeneratorSettings(
+ start_date=db.TIME_PERIODS[
+ list(db.TIME_PERIODS.keys())[self.field("timePeriod")]],
+ player_budget=int(self.field("starting_money")),
+ enemy_budget=int(self.field("enemy_starting_money")),
+ # QSlider forces integers, so we use 1 to 50 and divide by 10 to
+ # give 0.1 to 5.0.
+ midgame=self.field("midGame"),
+ inverted=self.field("invertMap"),
+ no_carrier=self.field("no_carrier"),
+ no_lha=self.field("no_lha"),
+ no_player_navy=self.field("no_player_navy"),
+ no_enemy_navy=self.field("no_enemy_navy")
+ )
- conflictTheater = selectedCampaign.theater
-
- timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]]
- midGame = self.field("midGame")
- multiplier = self.field("multiplier")
- no_carrier = self.field("no_carrier")
- no_lha = self.field("no_lha")
- supercarrier = self.field("supercarrier")
- no_player_navy = self.field("no_player_navy")
- no_enemy_navy = self.field("no_enemy_navy")
- invertMap = self.field("invertMap")
- starting_money = int(self.field("starting_money"))
-
- player_name = blueFaction
- enemy_name = redFaction
-
- settings = Settings()
- settings.inverted = invertMap
- settings.supercarrier = supercarrier
- settings.do_not_generate_carrier = no_carrier
- settings.do_not_generate_lha = no_lha
- settings.do_not_generate_player_navy = no_player_navy
- settings.do_not_generate_enemy_navy = no_enemy_navy
-
- generator = GameGenerator(player_name, enemy_name, conflictTheater,
- settings, timePeriod, starting_money,
- multiplier, midGame)
+ blue_faction = [c for c in db.FACTIONS][self.field("blueFaction")]
+ red_faction = [c for c in db.FACTIONS][self.field("redFaction")]
+ generator = GameGenerator(
+ blue_faction,
+ red_faction,
+ campaign.load_theater(),
+ settings,
+ generator_settings
+ )
self.generatedGame = generator.generate()
super(NewGameWizard, self).accept()
@@ -255,8 +268,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
invertMap = QtWidgets.QCheckBox()
self.registerField('invertMap', invertMap)
mapSettingsLayout = QtWidgets.QGridLayout()
- mapSettingsLayout.addWidget(QtWidgets.QLabel("Invert Map"), 1, 0)
- mapSettingsLayout.addWidget(invertMap, 1, 1)
+ mapSettingsLayout.addWidget(QtWidgets.QLabel("Invert Map"), 0, 0)
+ mapSettingsLayout.addWidget(invertMap, 0, 1)
+
+ mapSettingsLayout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
+ midgame = QtWidgets.QCheckBox()
+ self.registerField('midGame', midgame)
+ mapSettingsLayout.addWidget(midgame, 1, 1)
mapSettingsGroup.setLayout(mapSettingsLayout)
# Time Period
@@ -304,13 +322,13 @@ class CurrencySpinner(QtWidgets.QSpinBox):
class BudgetInputs(QtWidgets.QGridLayout):
- def __init__(self) -> None:
+ def __init__(self, label: str) -> None:
super().__init__()
- self.addWidget(QtWidgets.QLabel("Starting money"), 0, 0)
+ self.addWidget(QtWidgets.QLabel(label), 0, 0)
minimum = 0
maximum = 5000
- initial = 650
+ initial = DEFAULT_BUDGET
slider = QtWidgets.QSlider(Qt.Horizontal)
slider.setMinimum(minimum)
@@ -324,24 +342,72 @@ class BudgetInputs(QtWidgets.QGridLayout):
self.addWidget(self.starting_money, 1, 1)
-class MiscOptions(QtWidgets.QWizardPage):
- def __init__(self, parent=None):
- super(MiscOptions, self).__init__(parent)
+class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
+ def __init__(self, parent=None) -> None:
+ super().__init__(parent)
- self.setTitle("Miscellaneous settings")
- self.setSubTitle("\nOthers settings for the game.")
+ self.setTitle("Difficulty and automation options")
+ self.setSubTitle("\nOptions controlling game difficulty and level of "
+ "player involvement.")
self.setPixmap(QtWidgets.QWizard.LogoPixmap,
QtGui.QPixmap('./resources/ui/wizard/logo1.png'))
- midGame = QtWidgets.QCheckBox()
- multiplier = QtWidgets.QSpinBox()
- multiplier.setEnabled(False)
- multiplier.setMinimum(1)
- multiplier.setMaximum(5)
+ layout = QtWidgets.QVBoxLayout()
- miscSettingsGroup = QtWidgets.QGroupBox("Misc Settings")
- self.registerField('midGame', midGame)
- self.registerField('multiplier', multiplier)
+ economy_group = QtWidgets.QGroupBox("Economy options")
+ layout.addWidget(economy_group)
+ economy_layout = QtWidgets.QVBoxLayout()
+ economy_group.setLayout(economy_layout)
+
+ player_income = TenthsSpinSlider("Player income multiplier", 1, 50, 10)
+ self.registerField("player_income_multiplier", player_income.spinner)
+ economy_layout.addLayout(player_income)
+
+ enemy_income = TenthsSpinSlider("Enemy income multiplier", 1, 50, 10)
+ self.registerField("enemy_income_multiplier", enemy_income.spinner)
+ economy_layout.addLayout(enemy_income)
+
+ player_budget = BudgetInputs("Player starting budget")
+ self.registerField('starting_money', player_budget.starting_money)
+ economy_layout.addLayout(player_budget)
+
+ enemy_budget = BudgetInputs("Enemy starting budget")
+ self.registerField("enemy_starting_money", enemy_budget.starting_money)
+ economy_layout.addLayout(enemy_budget)
+
+ assist_group = QtWidgets.QGroupBox("Player assists")
+ layout.addWidget(assist_group)
+ assist_layout = QtWidgets.QGridLayout()
+ assist_group.setLayout(assist_layout)
+
+ assist_layout.addWidget(
+ QtWidgets.QLabel("Automate runway repairs"), 0, 0)
+ runway_repairs = QtWidgets.QCheckBox()
+ self.registerField("automate_runway_repairs", runway_repairs)
+ assist_layout.addWidget(runway_repairs, 0, 1, Qt.AlignRight)
+
+ assist_layout.addWidget(
+ QtWidgets.QLabel("Automate front-line purchases"), 1, 0)
+ front_line = QtWidgets.QCheckBox()
+ self.registerField("automate_front_line_purchases", front_line)
+ assist_layout.addWidget(front_line, 1, 1, Qt.AlignRight)
+
+ assist_layout.addWidget(
+ QtWidgets.QLabel("Automate aircraft purchases"), 2, 0)
+ aircraft = QtWidgets.QCheckBox()
+ self.registerField("automate_aircraft_purchases", aircraft)
+ assist_layout.addWidget(aircraft, 2, 1, Qt.AlignRight)
+
+ self.setLayout(layout)
+
+
+class GeneratorOptions(QtWidgets.QWizardPage):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setTitle("Generator settings")
+ self.setSubTitle("\nOptions affecting the generation of the game.")
+ self.setPixmap(QtWidgets.QWizard.LogoPixmap,
+ QtGui.QPixmap('./resources/ui/wizard/logo1.png'))
# Campaign settings
generatorSettingsGroup = QtWidgets.QGroupBox("Generator Settings")
@@ -356,13 +422,6 @@ class MiscOptions(QtWidgets.QWizardPage):
no_enemy_navy = QtWidgets.QCheckBox()
self.registerField('no_enemy_navy', no_enemy_navy)
- layout = QtWidgets.QGridLayout()
- layout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
- layout.addWidget(midGame, 1, 1)
- layout.addWidget(QtWidgets.QLabel("Ennemy forces multiplier [Disabled for Now]"), 2, 0)
- layout.addWidget(multiplier, 2, 1)
- miscSettingsGroup.setLayout(layout)
-
generatorLayout = QtWidgets.QGridLayout()
generatorLayout.addWidget(QtWidgets.QLabel("No Aircraft Carriers"), 1, 0)
generatorLayout.addWidget(no_carrier, 1, 1)
@@ -376,15 +435,8 @@ class MiscOptions(QtWidgets.QWizardPage):
generatorLayout.addWidget(no_enemy_navy, 5, 1)
generatorSettingsGroup.setLayout(generatorLayout)
- budget_inputs = BudgetInputs()
- economySettingsGroup = QtWidgets.QGroupBox("Economy")
- economySettingsGroup.setLayout(budget_inputs)
- self.registerField('starting_money', budget_inputs.starting_money)
-
mlayout = QVBoxLayout()
- mlayout.addWidget(miscSettingsGroup)
mlayout.addWidget(generatorSettingsGroup)
- mlayout.addWidget(economySettingsGroup)
self.setLayout(mlayout)
diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py
index 486068f5..fa0d49b4 100644
--- a/qt_ui/windows/settings/QSettingsWindow.py
+++ b/qt_ui/windows/settings/QSettingsWindow.py
@@ -24,6 +24,7 @@ import qt_ui.uiconstants as CONST
from game.game import Game
from game.infos.information import Information
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
+from qt_ui.widgets.spinsliders import TenthsSpinSlider
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage
@@ -54,6 +55,7 @@ class QSettingsWindow(QDialog):
self.game = game
self.pluginsPage = None
self.pluginsOptionsPage = None
+ self.campaign_management_page = QWidget()
self.setModal(True)
self.setWindowTitle("Settings")
@@ -82,6 +84,14 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(difficulty)
self.right_layout.addWidget(self.difficultyPage)
+ self.init_campaign_management_layout()
+ campaign_management = QStandardItem("Campaign Management")
+ campaign_management.setIcon(CONST.ICONS["Money"])
+ campaign_management.setEditable(False)
+ campaign_management.setSelectable(True)
+ self.categoryModel.appendRow(campaign_management)
+ self.right_layout.addWidget(self.campaign_management_page)
+
self.initGeneratorLayout()
generator = QStandardItem("Mission Generator")
generator.setIcon(CONST.ICONS["Generator"])
@@ -131,10 +141,13 @@ class QSettingsWindow(QDialog):
def initDifficultyLayout(self):
self.difficultyPage = QWidget()
- self.difficultyLayout = QGridLayout()
+ self.difficultyLayout = QVBoxLayout()
self.difficultyLayout.setAlignment(Qt.AlignTop)
self.difficultyPage.setLayout(self.difficultyLayout)
+ # DCS AI difficulty settings
+ self.aiDifficultySettings = QGroupBox("AI Difficulty")
+ self.aiDifficultyLayout = QGridLayout()
self.playerCoalitionSkill = QComboBox()
self.enemyCoalitionSkill = QComboBox()
self.enemyAASkill = QComboBox()
@@ -147,31 +160,40 @@ class QSettingsWindow(QDialog):
self.enemyCoalitionSkill.setCurrentIndex(CONST.SKILL_OPTIONS.index(self.game.settings.enemy_skill))
self.enemyAASkill.setCurrentIndex(CONST.SKILL_OPTIONS.index(self.game.settings.enemy_vehicle_skill))
+ self.player_income = TenthsSpinSlider(
+ "Player income multiplier", 1, 50,
+ int(self.game.settings.player_income_multiplier * 10))
+ self.player_income.spinner.valueChanged.connect(self.applySettings)
+ self.enemy_income = TenthsSpinSlider(
+ "Enemy income multiplier", 1, 50,
+ int(self.game.settings.enemy_income_multiplier * 10))
+ self.enemy_income.spinner.valueChanged.connect(self.applySettings)
+
self.playerCoalitionSkill.currentIndexChanged.connect(self.applySettings)
self.enemyCoalitionSkill.currentIndexChanged.connect(self.applySettings)
self.enemyAASkill.currentIndexChanged.connect(self.applySettings)
- self.difficultyLayout.addWidget(QLabel("Player coalition skill"), 0, 0)
- self.difficultyLayout.addWidget(self.playerCoalitionSkill, 0, 1, Qt.AlignRight)
- self.difficultyLayout.addWidget(QLabel("Enemy skill"), 1, 0)
- self.difficultyLayout.addWidget(self.enemyCoalitionSkill, 1, 1, Qt.AlignRight)
- self.difficultyLayout.addWidget(QLabel("Enemy AA and vehicles skill"), 2, 0)
- self.difficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
+ # Mission generation settings related to difficulty
+ self.missionSettings = QGroupBox("Mission Difficulty")
+ self.missionLayout = QGridLayout()
+
+ self.manpads = QCheckBox()
+ self.manpads.setChecked(self.game.settings.manpads)
+ self.manpads.toggled.connect(self.applySettings)
+
+ self.noNightMission = QCheckBox()
+ self.noNightMission.setChecked(self.game.settings.night_disabled)
+ self.noNightMission.toggled.connect(self.applySettings)
+
+ # DCS Mission options
+ self.missionRestrictionsSettings = QGroupBox("Mission Restrictions")
+ self.missionRestrictionsLayout = QGridLayout()
self.difficultyLabel = QComboBox()
[self.difficultyLabel.addItem(t) for t in CONST.LABELS_OPTIONS]
self.difficultyLabel.setCurrentIndex(CONST.LABELS_OPTIONS.index(self.game.settings.labels))
self.difficultyLabel.currentIndexChanged.connect(self.applySettings)
- self.difficultyLayout.addWidget(QLabel("In Game Labels"), 3, 0)
- self.difficultyLayout.addWidget(self.difficultyLabel, 3, 1, Qt.AlignRight)
-
- self.noNightMission = QCheckBox()
- self.noNightMission.setChecked(self.game.settings.night_disabled)
- self.noNightMission.toggled.connect(self.applySettings)
- self.difficultyLayout.addWidget(QLabel("No night missions"), 4, 0)
- self.difficultyLayout.addWidget(self.noNightMission, 4, 1, Qt.AlignRight)
-
self.mapVisibiitySelection = QComboBox()
self.mapVisibiitySelection.addItem("All", ForcedOptions.Views.All)
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All:
@@ -189,15 +211,82 @@ class QSettingsWindow(QDialog):
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
self.mapVisibiitySelection.setCurrentIndex(4)
self.mapVisibiitySelection.currentIndexChanged.connect(self.applySettings)
- self.difficultyLayout.addWidget(QLabel("Map visibility options"), 5, 0)
- self.difficultyLayout.addWidget(self.mapVisibiitySelection, 5, 1, Qt.AlignRight)
self.ext_views = QCheckBox()
self.ext_views.setChecked(self.game.settings.external_views_allowed)
self.ext_views.toggled.connect(self.applySettings)
- self.difficultyLayout.addWidget(QLabel("Allow external views"), 6, 0)
- self.difficultyLayout.addWidget(self.ext_views, 6, 1, Qt.AlignRight)
+ self.aiDifficultyLayout.addWidget(QLabel("Player coalition skill"), 0, 0)
+ self.aiDifficultyLayout.addWidget(self.playerCoalitionSkill, 0, 1, Qt.AlignRight)
+ self.aiDifficultyLayout.addWidget(QLabel("Enemy coalition skill"), 1, 0)
+ self.aiDifficultyLayout.addWidget(self.enemyCoalitionSkill, 1, 1, Qt.AlignRight)
+ self.aiDifficultyLayout.addWidget(QLabel("Enemy AA and vehicles skill"), 2, 0)
+ self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
+ self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
+ self.aiDifficultyLayout.addLayout(self.enemy_income, 4, 0)
+ self.aiDifficultySettings.setLayout(self.aiDifficultyLayout)
+ self.difficultyLayout.addWidget(self.aiDifficultySettings)
+
+ self.missionLayout.addWidget(QLabel("Manpads on frontlines"), 0, 0)
+ self.missionLayout.addWidget(self.manpads, 0, 1, Qt.AlignRight)
+ self.missionLayout.addWidget(QLabel("No night missions"), 1, 0)
+ self.missionLayout.addWidget(self.noNightMission, 1, 1, Qt.AlignRight)
+ self.missionSettings.setLayout(self.missionLayout)
+ self.difficultyLayout.addWidget(self.missionSettings)
+
+ self.missionRestrictionsLayout.addWidget(QLabel("In Game Labels"), 0, 0)
+ self.missionRestrictionsLayout.addWidget(self.difficultyLabel, 0, 1, Qt.AlignRight)
+ self.missionRestrictionsLayout.addWidget(QLabel("Map visibility options"), 1, 0)
+ self.missionRestrictionsLayout.addWidget(self.mapVisibiitySelection, 1, 1, Qt.AlignRight)
+ self.missionRestrictionsLayout.addWidget(QLabel("Allow external views"), 2, 0)
+ self.missionRestrictionsLayout.addWidget(self.ext_views, 2, 1, Qt.AlignRight)
+ self.missionRestrictionsSettings.setLayout(self.missionRestrictionsLayout)
+ self.difficultyLayout.addWidget(self.missionRestrictionsSettings)
+
+ def init_campaign_management_layout(self) -> None:
+ campaign_layout = QVBoxLayout()
+ campaign_layout.setAlignment(Qt.AlignTop)
+ self.campaign_management_page.setLayout(campaign_layout)
+
+ automation = QGroupBox("HQ Automation")
+ campaign_layout.addWidget(automation)
+
+ automation_layout = QGridLayout()
+ automation.setLayout(automation_layout)
+
+ def set_runway_automation(value: bool) -> None:
+ self.game.settings.automate_runway_repair = value
+
+ def set_front_line_automation(value: bool) -> None:
+ self.game.settings.automate_front_line_reinforcements = value
+
+ def set_aircraft_automation(value: bool) -> None:
+ self.game.settings.automate_aircraft_reinforcements = value
+
+ runway_repair = QCheckBox()
+ runway_repair.setChecked(
+ self.game.settings.automate_runway_repair)
+ runway_repair.toggled.connect(set_runway_automation)
+
+ automation_layout.addWidget(QLabel("Automate runway repairs"), 0, 0)
+ automation_layout.addWidget(runway_repair, 0, 1, Qt.AlignRight)
+
+ front_line = QCheckBox()
+ front_line.setChecked(
+ self.game.settings.automate_front_line_reinforcements)
+ front_line.toggled.connect(set_front_line_automation)
+
+ automation_layout.addWidget(
+ QLabel("Automate front-line purchases"), 1, 0)
+ automation_layout.addWidget(front_line, 1, 1, Qt.AlignRight)
+
+ aircraft = QCheckBox()
+ aircraft.setChecked(
+ self.game.settings.automate_aircraft_reinforcements)
+ aircraft.toggled.connect(set_aircraft_automation)
+
+ automation_layout.addWidget(QLabel("Automate aircraft purchases"), 2, 0)
+ automation_layout.addWidget(aircraft, 2, 1, Qt.AlignRight)
def initGeneratorLayout(self):
self.generatorPage = QWidget()
@@ -274,11 +363,15 @@ class QSettingsWindow(QDialog):
self.culling.toggled.connect(self.applySettings)
self.culling_distance = QSpinBox()
- self.culling_distance.setMinimum(50)
+ self.culling_distance.setMinimum(10)
self.culling_distance.setMaximum(10000)
self.culling_distance.setValue(self.game.settings.perf_culling_distance)
self.culling_distance.valueChanged.connect(self.applySettings)
+ self.culling_do_not_cull_carrier = QCheckBox()
+ self.culling_do_not_cull_carrier.setChecked(self.game.settings.perf_do_not_cull_carrier)
+ self.culling_do_not_cull_carrier.toggled.connect(self.applySettings)
+
self.performanceLayout.addWidget(QLabel("Smoke visual effect on frontline"), 0, 0)
self.performanceLayout.addWidget(self.smoke, 0, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("SAM starts in RED alert mode"), 1, 0)
@@ -299,6 +392,8 @@ class QSettingsWindow(QDialog):
self.performanceLayout.addWidget(self.culling, 8, 1, alignment=Qt.AlignRight)
self.performanceLayout.addWidget(QLabel("Culling distance (km)"), 9, 0)
self.performanceLayout.addWidget(self.culling_distance, 9, 1, alignment=Qt.AlignRight)
+ self.performanceLayout.addWidget(QLabel("Do not cull carrier's surroundings"), 10, 0)
+ self.performanceLayout.addWidget(self.culling_do_not_cull_carrier, 10, 1, alignment=Qt.AlignRight)
self.generatorLayout.addWidget(self.gameplay)
self.generatorLayout.addWidget(QLabel("Disabling settings below may improve performance, but will impact the overall quality of the experience."))
@@ -347,6 +442,9 @@ class QSettingsWindow(QDialog):
self.game.settings.player_skill = CONST.SKILL_OPTIONS[self.playerCoalitionSkill.currentIndex()]
self.game.settings.enemy_skill = CONST.SKILL_OPTIONS[self.enemyCoalitionSkill.currentIndex()]
self.game.settings.enemy_vehicle_skill = CONST.SKILL_OPTIONS[self.enemyAASkill.currentIndex()]
+ self.game.settings.player_income_multiplier = self.player_income.value
+ self.game.settings.enemy_income_multiplier = self.enemy_income.value
+ self.game.settings.manpads = self.manpads.isChecked()
self.game.settings.labels = CONST.LABELS_OPTIONS[self.difficultyLabel.currentIndex()]
self.game.settings.night_disabled = self.noNightMission.isChecked()
self.game.settings.map_coalition_visibility = self.mapVisibiitySelection.currentData()
@@ -366,9 +464,11 @@ class QSettingsWindow(QDialog):
self.game.settings.perf_culling = self.culling.isChecked()
self.game.settings.perf_culling_distance = int(self.culling_distance.value())
+ self.game.settings.perf_do_not_cull_carrier = self.culling_do_not_cull_carrier.isChecked()
self.game.settings.show_red_ato = self.cheat_options.show_red_ato
+ self.game.compute_conflicts_position()
GameUpdateSignal.get_instance().updateGame(self.game)
def onSelectionChanged(self):
diff --git a/requirements.txt b/requirements.txt
index 41233a08..1bedb462 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@ tabulate~=0.8.7
mypy==0.782
mypy-extensions==0.4.3
jinja2>=2.11.2
+shapely==1.7.1
diff --git a/resources/briefing/templates/briefingtemplate_CN.j2 b/resources/briefing/templates/briefingtemplate_CN.j2
new file mode 100644
index 00000000..505d7122
--- /dev/null
+++ b/resources/briefing/templates/briefingtemplate_CN.j2
@@ -0,0 +1,105 @@
+DCS Liberation 第 {{ game.turn }} 回合
+====================
+
+简报中的大部分信息,包括通讯、飞行计划等,都可以在你的膝板中找到。
+
+当前局势:
+====================
+{% if not frontlines %}
+目前没有地面战斗发生。
+{% endif %}
+{% if frontlines %}
+{%+ for frontline in frontlines %}
+{% if frontline.player_zero %}
+前线已经没有任何地面力量来进行防御了。 情况极其危急,我们将不可避免地失去{{ frontline.player_base.name }} 和 {{ frontline.enemy_base.name }}之间的区域控制权。
+{% endif %}
+{% if frontline.enemy_zero %}
+我们已经击溃了敌军部队,将在 {{ frontline.enemy_base.name }} 区域取得重大突破。
+{% endif %}
+{% if not frontline.player_zero %}
+{# Pick a random sentence to describe each frontline #}
+{% set fl_sent1 %}在 {{ frontline.player_base.name }} 和 {{frontline.enemy_base.name}} 之间仍有交火发生。 {%+ endset %}
+{% set fl_sent2 %}在 {{frontline.player_base.name}} 和 {{frontline.enemy_base.name}} 之间的地面战斗仍在继续。 {%+ endset %}
+{% set fl_sent3 %}我们 {{frontline.player_base.name}} 的部队,正在对抗来自 {{frontline.enemy_base.name}}的敌军部队。 {%+ endset %}
+{% set fl_sent4 %}来自 {{frontline.player_base.name}} 的我军部队,正与来自 {{frontline.enemy_base.name}} 的敌军部队交战。 {%+ endset %}
+{% set fl_sent5 %}当前的战斗前线在 {{frontline.player_base.name}} 和 {{frontline.enemy_base.name}} 之间。 {%+ endset %}
+{% set frontline_sentences = [fl_sent1, fl_sent2, fl_sent3, fl_sent4, fl_sent5] %}
+{{ frontline_sentences | random }}
+{%- if frontline.advantage %}
+ {%- if frontline.stance == frontline.combat_stances.AGGRESSIVE %}
+我军部队将从这个位置出发向敌军发起进攻,鉴于敌军已经寡不敌众,毫无疑问我军将取得进展。
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.ELIMINATION %}
+我军地面部队将从这个位置出发,集中力量优先摧毁敌方有生力量,然后再向 {{frontline.enemy_base.name}} 进发。 敌军已经寡不敌众,此举或将给予他们致命一击。"
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.BREAKTHROUGH %}
+我军部队将从这个位置出发,集中力量突击 {{ frontline.enemy_base.name }} 。
+ {% endif %}
+ {%- if frontline.stance in [frontline.combat_stances.DEFENSIVE, frontline.combat_stances.AMBUSH] %}
+我们的地面部队将在当前位置驻守防御。我们并不期望敌人会来一次突击。
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.RETREAT %}
+{# TODO: Write a retreat sentence #}
+ {% endif %}
+{%- else %}
+ {%- if frontline.stance == frontline.combat_stances.AGGRESSIVE %}
+我们的地面部队将试图利用优势数量从当前位置向敌军发起一次大胆的突击。这次行动有些冒险,敌人也可能会发动反击。
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.ELIMINATION %}
+我们的地面部队将试图利用优势数量从当前位置向敌军发起一次大胆的突击。这次行动有些冒险,敌人也可能会发动反击。
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.BREAKTHROUGH %}
+我们的地面部队将从当前位置向 {{frontline.enemy_base.name}} 发起一次主要突破攻势。祝他们好运,预计会有敌方反击。
+ {% endif %}
+ {%- if frontline.stance in [frontline.combat_stances.DEFENSIVE, frontline.combat_stances.AMBUSH] %}
+我们的地面部队奉命驻守在当前位置,以防御敌人的攻击。 预计敌方即将发起突击行动。
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.RETREAT %}
+{# TODO: Write a retreat sentence #}
+ {% endif %}
+{% endif %}
+{% endif %}
+
+{%+ endfor %}{% endif %}
+
+本小队:
+====================
+{% for flight in flights if flight.client_units %}
+--------------------------------------------------
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{% for waypoint in flight.waypoints %}
+{{ loop.index0 }} {{waypoint|waypoint_timing("Depart ")}}-- {{waypoint.name}} : {{ waypoint.description}}
+{% endfor %}
+--------------------------------------------------{% endfor %}
+
+
+其他友军单位飞行计划:
+====================
+{% for dep in allied_flights_by_departure %}
+{{ dep }}
+---------------------------------------------------
+{% for flight in allied_flights_by_departure[dep] %}
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{% endfor %}
+{% endfor %}
+
+航母及FARPs:
+====================
+{% for runway in dynamic_runways %}
+{{ runway.airfield_name}}
+--------------------------------------------------
+无线电 : {{ runway.atc }}
+TACAN : {{ runway.tacan }} {{ runway.tacan_callsign }}
+{% if runway.icls %}ILS/ICLS频道: {{ runway.icls }}
+{% endif %}
+
+{% endfor %}
+AWACS:
+====================
+{% for i in awacs %}{{ i.callsign }} -- 频率 : {{i.freq.mhz}}
+{% endfor %}
+
+JTACS [F-10 菜单] :
+====================
+{% for jtac in jtacs %}前线 {{ jtac.region }} -- 激光编码 : {{ jtac.code }}
+{% endfor %}
diff --git a/resources/briefing/templates/briefingtemplate_EN.j2 b/resources/briefing/templates/briefingtemplate_EN.j2
index 864fbabc..7ec4341d 100644
--- a/resources/briefing/templates/briefingtemplate_EN.j2
+++ b/resources/briefing/templates/briefingtemplate_EN.j2
@@ -66,8 +66,9 @@ Your flights:
====================
{% for flight in flights if flight.client_units %}
--------------------------------------------------
-{{ flight.flight_type.name }} {{ flight.units[0].type }} x {{ flight.size }}, {{ flight.package.target.name}}
-{% for waypoint in flight.waypoints %}{{ loop.index }} -- {{waypoint.name}} : {{ waypoint.description}}
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{ flight.size }}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{% for waypoint in flight.waypoints %}
+{{ loop.index0 }} {{waypoint|waypoint_timing("Depart ")}}-- {{waypoint.name}} : {{ waypoint.description}}
{% endfor %}
--------------------------------------------------{% endfor %}
@@ -78,7 +79,7 @@ Planned ally flights:
{{ dep }}
---------------------------------------------------
{% for flight in allied_flights_by_departure[dep] %}
-{{ flight.flight_type.name }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
{% endfor %}
{% endfor %}
diff --git a/resources/briefing/templates/briefingtemplate_FR.j2 b/resources/briefing/templates/briefingtemplate_FR.j2
new file mode 100644
index 00000000..993d417c
--- /dev/null
+++ b/resources/briefing/templates/briefingtemplate_FR.j2
@@ -0,0 +1,105 @@
+DCS Liberation Tour {{ game.turn }}
+====================
+
+La plupart des informations importantes, en particulier votre plan de vol et les fréquences de communications à utiliser, sont disponibles sur votre kneeboard.
+
+Situation actuelle :
+====================
+{% if not frontlines %}
+Pour le moment, il n'y a pas de combats au sol.
+{% endif %}
+{% if frontlines %}
+{%+ for frontline in frontlines %}
+{% if frontline.player_zero %}
+Nous n'avons pas un seul véhicule disponible pour tenir nos positions. La situation est critique, nous allons inévitablement perdre du terrain sur le front entre {{ frontline.player_base.name }} et {{ frontline.enemy_base.name }}.
+{% endif %}
+{% if frontline.enemy_zero %}
+Les forces ennemies ont été écrasées, nous devrions être capable de progresser rapidement vers {{ frontline.enemy_base.name }}
+{% endif %}
+{% if not frontline.player_zero %}
+{# Pick a random sentence to describe each frontline #}
+{% set fl_sent1 %}Il y a des comabats entre {{ frontline.player_base.name }} et {{frontline.enemy_base.name}}. {%+ endset %}
+{% set fl_sent2 %}Au sol, les combats se poursuivent entre {{frontline.player_base.name}} et {{frontline.enemy_base.name}}. {%+ endset %}
+{% set fl_sent3 %}Nos forces au sol, basées à {{frontline.player_base.name}} sont opposées aux forces ennemies provenant de {{frontline.enemy_base.name}}. {%+ endset %}
+{% set fl_sent4 %}Nos forces de {{frontline.player_base.name}} se battent contre les ennemis basés à {{frontline.enemy_base.name}}. {%+ endset %}
+{% set fl_sent5 %}Il y a une ligne de front active entre {{frontline.player_base.name}} et {{frontline.enemy_base.name}}. {%+ endset %}
+{% set frontline_sentences = [fl_sent1, fl_sent2, fl_sent3, fl_sent4, fl_sent5] %}
+{{ frontline_sentences | random }}
+{%- if frontline.advantage %}
+ {%- if frontline.stance == frontline.combat_stances.AGGRESSIVE %}
+Sur ce front, nos forces vont tenter de progresser. Comme l'ennemi est en infériorité numérique, nous sommes confiant sur l'issue du combat.
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.ELIMINATION %}
+Sur ce front, nos forces vont se focaliser sur la destruction des ennemis avant de tenter une progression vers {{frontline.enemy_base.name}}. L'ennemi est déjà en infériorité, cette manoeuvre pourrait porter le coup final."
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.BREAKTHROUGH %}
+Sur ce front, nos forces vont tenter une progression rapide vers {{ frontline.enemy_base.name }}
+ {% endif %}
+ {%- if frontline.stance in [frontline.combat_stances.DEFENSIVE, frontline.combat_stances.AMBUSH] %}
+Sur ce front, nos forces vont garder leurs positions. Nous ne nous attendons pas à un assaut de l'ennemi.
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.RETREAT %}
+{# TODO: Write a retreat sentence #}
+ {% endif %}
+{%- else %}
+ {%- if frontline.stance == frontline.combat_stances.AGGRESSIVE %}
+Sur ce front, nos forces au sol vont tenter un assaut audacieux contre un ennemi en surnombre. L'opération est périlleuse, et l'ennemi risque de contre-attaquer.
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.ELIMINATION %}
+Sur ce front, nos forces au sol vont tenter une manoeuvre audacieuse pour éliminer des ennemis en surnombre. L'opération est périlleuse, et l'ennemi risque de contre-attaquer.
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.BREAKTHROUGH %}
+Sur ce front, nos forces vont se précipiter sur l'objectif {{frontline.enemy_base.name}}, malgré leur infériorité numérique. Souhaitons-leur bonne chance... Nous nous attendons aussi à une contre-attaque.
+ {% endif %}
+ {%- if frontline.stance in [frontline.combat_stances.DEFENSIVE, frontline.combat_stances.AMBUSH] %}
+Sur ce front, nos forces au sol renforcent leurs positions, et se préparent à défendre. Un assaut ennemi serait imminent.
+ {% endif %}
+ {%- if frontline.stance == frontline.combat_stances.RETREAT %}
+{# TODO: Write a retreat sentence #}
+ {% endif %}
+{% endif %}
+{% endif %}
+
+{%+ endfor %}{% endif %}
+
+Vols :
+==========
+{% for flight in flights if flight.client_units %}
+--------------------------------------------------
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, départ dans {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{% for waypoint in flight.waypoints %}
+{{ loop.index0 }} {{waypoint|waypoint_timing("Départ dans ")}}-- {{waypoint.name}} : {{ waypoint.description}}
+{% endfor %}
+--------------------------------------------------{% endfor %}
+
+
+Vols alliés prévus :
+====================
+{% for dep in allied_flights_by_departure %}
+{{ dep }}
+---------------------------------------------------
+{% for flight in allied_flights_by_departure[dep] %}
+{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, départ dans {{ flight.departure_delay }}, {{ flight.package.target.name}}
+{% endfor %}
+{% endfor %}
+
+Porte-avions et FARPS :
+=======================
+{% for runway in dynamic_runways %}
+{{ runway.airfield_name}}
+--------------------------------------------------
+RADIO : {{ runway.atc }}
+TACAN : {{ runway.tacan }} {{ runway.tacan_callsign }}
+{% if runway.icls %}Channel ICLS : {{ runway.icls }}
+{% endif %}
+
+{% endfor %}
+AWACS:
+====================
+{% for i in awacs %}{{ i.callsign }} -- Fréq : {{i.freq.mhz}}
+{% endfor %}
+
+JTACS [Menu F-10] :
+====================
+{% for jtac in jtacs %}Ligne de front {{ jtac.region }} -- Code : {{ jtac.code }}
+{% endfor %}
\ No newline at end of file
diff --git a/resources/campaigns/battle_of_britain.json b/resources/campaigns/battle_of_britain.json
index 4515bac1..e4ab5896 100644
--- a/resources/campaigns/battle_of_britain.json
+++ b/resources/campaigns/battle_of_britain.json
@@ -3,86 +3,5 @@
"theater": "The Channel",
"authors": "Khopa",
"description": "
Experience the Battle of Britain on the Channel map !
Note: It is not possible to cross the channel to capture enemy bases yet, but you can consider you won if you manage to destroy all the ennemy targets
", - "player_points": [ - { - "type": "airbase", - "id": "Hawkinge", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Lympne", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Manston", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "High Halden", - "size": 600, - "importance": 1 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Dunkirk Mardyck", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Saint Omer Longuenesse", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Merville Calonne", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Abbeville Drucat", - "size": 600, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Hawkinge", - "Lympne" - ], - [ - "Hawkinge", - "Manston" - ], - [ - "High Halden", - "Lympne" - ], - [ - "Dunkirk Mardyck", - "Saint Omer Longuenesse" - ], - [ - "Merville Calonne", - "Saint Omer Longuenesse" - ], - [ - "Abbeville Drucat", - "Saint Omer Longuenesse" - ] - ] + "miz": "battle_of_britain.miz" } \ No newline at end of file diff --git a/resources/campaigns/battle_of_britain.miz b/resources/campaigns/battle_of_britain.miz new file mode 100644 index 00000000..754e3f7a Binary files /dev/null and b/resources/campaigns/battle_of_britain.miz differ diff --git a/resources/campaigns/black_sea.json b/resources/campaigns/black_sea.json new file mode 100644 index 00000000..116c7a3b --- /dev/null +++ b/resources/campaigns/black_sea.json @@ -0,0 +1,7 @@ +{ + "name": "Caucasus - Black Sea", + "theater": "Caucasus", + "authors": "Colonel Panic", + "description": "A medium sized theater with bases along the coast of the Black Sea.
", + "miz": "black_sea.miz" +} \ No newline at end of file diff --git a/resources/campaigns/black_sea.miz b/resources/campaigns/black_sea.miz new file mode 100644 index 00000000..e863ea83 Binary files /dev/null and b/resources/campaigns/black_sea.miz differ diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json index b8a1e150..5cd60c84 100644 --- a/resources/campaigns/desert_war.json +++ b/resources/campaigns/desert_war.json @@ -3,61 +3,5 @@ "theater": "Persian Gulf", "authors": "Khopa", "description": "This is a simple scenario in the Desert near Dubai and Abu-Dhabi. Progress from Liwa airbase to Al-Minhad.
This scenario shouldn't require too much performance.
", - "player_points": [ - { - "type": "airbase", - "id": "Liwa Airbase", - "size": 2000, - "importance": 1.2 - }, - { - "type": "lha", - "id": 1002, - "x": -164000, - "y": -257000, - "captured_invert": true - }, - { - "type": "carrier", - "id": 1001, - "x": -124000, - "y": -303000, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Al Ain International Airport", - "size": 2000, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Al Maktoum Intl", - "size": 2000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Al Minhad AB", - "size": 1000, - "importance": 1 - } - ], - "links": [ - [ - "Al Ain International Airport", - "Liwa Airbase" - ], - [ - "Al Ain International Airport", - "Al Maktoum Intl" - ], - [ - "Al Maktoum Intl", - "Al Minhad AB" - ] - ] + "miz": "desert_war.miz" } \ No newline at end of file diff --git a/resources/campaigns/desert_war.miz b/resources/campaigns/desert_war.miz new file mode 100644 index 00000000..0a11477e Binary files /dev/null and b/resources/campaigns/desert_war.miz differ diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json index 09a70d2d..8ccbe8ba 100644 --- a/resources/campaigns/dunkirk.json +++ b/resources/campaigns/dunkirk.json @@ -3,77 +3,5 @@ "theater": "The Channel", "authors": "Khopa", "description": "In this scenario, your forces starts in Dunkirk and can be supported by the airfields on the other side of the Channel.
If you select the inverted configuration, you can play a German invasion of England.
Note: B-17 should be operated from Manston airfield
", - "player_points": [ - { - "type": "airbase", - "id": "Hawkinge", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Lympne", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Manston", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Dunkirk Mardyck", - "size": 600, - "importance": 1, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Saint Omer Longuenesse", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Merville Calonne", - "size": 600, - "importance": 1, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Abbeville Drucat", - "size": 600, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Hawkinge", - "Lympne" - ], - [ - "Hawkinge", - "Manston" - ], - [ - "Dunkirk Mardyck", - "Saint Omer Longuenesse" - ], - [ - "Merville Calonne", - "Saint Omer Longuenesse" - ], - [ - "Abbeville Drucat", - "Saint Omer Longuenesse" - ] - ] + "miz": "dunkirk.miz" } \ No newline at end of file diff --git a/resources/campaigns/dunkirk.miz b/resources/campaigns/dunkirk.miz new file mode 100644 index 00000000..e722cebe Binary files /dev/null and b/resources/campaigns/dunkirk.miz differ diff --git a/resources/campaigns/full_caucasus.json b/resources/campaigns/full_caucasus.json index ce4d3858..a11173ef 100644 --- a/resources/campaigns/full_caucasus.json +++ b/resources/campaigns/full_caucasus.json @@ -1,168 +1,7 @@ { - "name": "Caucasus - Full Map", - "theater": "Caucasus", - "authors": "george", - "description": "Full map of the Caucasus
Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.
", - "player_points": [ - { - "type": "airbase", - "id": "Kobuleti", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Senaki-Kolkhi", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Kutaisi", - "size": 600, - "importance": 1 - }, - { - "type": "carrier", - "id": 1001, - "x": -304708, - "y": 552839, - "captured_invert": true - }, - { - "type": "lha", - "id": 1002, - "x": -326050.6875, - "y": 519452.1875, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Beslan", - "size": 1000, - "importance": 1.3 - }, - { - "type": "airbase", - "id": "Nalchik", - "size": 1000, - "importance": 1.1 - }, - { - "type": "airbase", - "id": "Mozdok", - "size": 2000, - "importance": 1.1 - }, - { - "type": "airbase", - "id": "Mineralnye Vody", - "size": 2000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Maykop-Khanskaya", - "size": 3000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Sukhumi-Babushara", - "size": 2000, - "importance": 1.3 - }, - { - "type": "airbase", - "id": "Gudauta", - "size": 2000, - "importance": 1.3 - }, - { - "type": "airbase", - "id": "Sochi-Adler", - "size": 2000, - "importance": 1.1 - }, - { - "type": "airbase", - "id": "Gelendzhik", - "size": 2000, - "importance": 1.3 - }, - { - "type": "airbase", - "id": "Vaziani", - "size": 2000, - "importance": 1.3 - } - ], - "links": [ - [ - "Kutaisi", - "Vaziani" - ], - [ - "Beslan", - "Vaziani" - ], - [ - "Beslan", - "Mozdok" - ], - [ - "Beslan", - "Nalchik" - ], - [ - "Mozdok", - "Nalchik" - ], - [ - "Mineralnye Vody", - "Nalchik" - ], - [ - "Mineralnye Vody", - "Mozdok" - ], - [ - "Maykop-Khanskaya", - "Mineralnye Vody" - ], - [ - "Maykop-Khanskaya", - "Gelendzhik" - ], - [ - "Gelendzhik", - "Sochi-Adler" - ], - [ - "Gudauta", - "Sochi-Adler" - ], - [ - "Gudauta", - "Sukhumi-Babushara" - ], - [ - "Senaki-Kolkhi", - "Sukhumi-Babushara" - ], - [ - "Kutaisi", - "Senaki-Kolkhi" - ], - [ - "Senaki-Kolkhi", - "Kobuleti" - ], - [ - "Kobuleti", - "Kutaisi" - ] - ] + "name": "Caucasus - Full Map", + "theater": "Caucasus", + "authors": "george", + "description": "Full map of the Caucasus
Note: This scenario is heavy on performance, enabling \"culling\" in settings is highly recommended.
", + "miz": "full_caucasus.miz" } \ No newline at end of file diff --git a/resources/campaigns/full_caucasus.miz b/resources/campaigns/full_caucasus.miz new file mode 100644 index 00000000..91907cb6 Binary files /dev/null and b/resources/campaigns/full_caucasus.miz differ diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json index fc5969a5..66befcd5 100644 --- a/resources/campaigns/inherent_resolve.json +++ b/resources/campaigns/inherent_resolve.json @@ -3,82 +3,5 @@ "theater": "Syria", "authors": "Khopa", "description": "In this scenario, you start from Jordan, and have to fight your way through eastern Syria.
", - "player_points": [ - { - "type": "airbase", - "id": "King Hussein Air College", - "size": 1000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Incirlik", - "size": 1000, - "importance": 1.4, - "captured_invert": true - }, - { - "type": "carrier", - "id": 1001, - "x": -210000, - "y": -200000, - "captured_invert": true - }, - { - "type": "lha", - "id": 1002, - "x": -131000, - "y": -161000, - "captured_invert": true - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Khalkhalah", - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Palmyra", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Tabqa", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Jirah", - "size": 1000, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Khalkhalah", - "King Hussein Air College" - ], - [ - "Incirlik", - "Incirlik" - ], - [ - "Khalkhalah", - "Palmyra" - ], - [ - "Palmyra", - "Tabqa" - ], - [ - "Jirah", - "Tabqa" - ] - ] + "miz": "inherent_resolve.miz" } \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.miz b/resources/campaigns/inherent_resolve.miz new file mode 100644 index 00000000..64a74485 Binary files /dev/null and b/resources/campaigns/inherent_resolve.miz differ diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json index 41cd39a2..f259a1e4 100644 --- a/resources/campaigns/invasion_from_turkey.json +++ b/resources/campaigns/invasion_from_turkey.json @@ -3,85 +3,5 @@ "theater": "Syria", "authors": "Khopa", "description": "In this scenario, you start from Turkey and have to invade territories in northern Syria.
", - "player_points": [ - { - "type": "airbase", - "id": "Incirlik", - "size": 1000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Hatay", - "size": 1000, - "importance": 1.4 - }, - { - "type": "carrier", - "id": 1001, - "x": 133000, - "y": -54000 - }, - { - "type": "lha", - "id": 1002, - "x": 155000, - "y": -19000 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Minakh", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Aleppo", - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Kuweires", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Jirah", - "size": 1000, - "importance": 1 - }, - { - "type": "airbase", - "id": "Tabqa", - "size": 1000, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Hatay", - "Minakh" - ], - [ - "Aleppo", - "Minakh" - ], - [ - "Aleppo", - "Kuweires" - ], - [ - "Jirah", - "Kuweires" - ], - [ - "Jirah", - "Tabqa" - ] - ] + "miz": "invasion_from_turkey.miz" } \ No newline at end of file diff --git a/resources/campaigns/invasion_from_turkey.miz b/resources/campaigns/invasion_from_turkey.miz new file mode 100644 index 00000000..20527bc5 Binary files /dev/null and b/resources/campaigns/invasion_from_turkey.miz differ diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json index 248356fb..8a8d475f 100644 --- a/resources/campaigns/invasion_of_iran.json +++ b/resources/campaigns/invasion_of_iran.json @@ -3,141 +3,5 @@ "theater": "Persian Gulf", "authors": "Khopa", "description": "In this scenario, you start in Bandar Abbas, and must invade Iran.
", - "player_points": [ - { - "type": "airbase", - "id": "Ras Al Khaimah", - "size": 1000, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Khasab", - "size": 600, - "importance": 1.2 - }, - { - "type": "airbase", - "id": "Qeshm Island", - "radials": [ - 270, - 315, - 0, - 45, - 90, - 135, - 180 - ], - "size": 600, - "importance": 1.1 - }, - { - "type": "airbase", - "id": "Havadarya", - "radials": [ - 225, - 270, - 315, - 0, - 45 - ], - "size": 1000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Bandar Abbas Intl", - "size": 2000, - "importance": 1.4 - }, - { - "type": "carrier", - "id": 1001, - "x": 59514.324335475, - "y": 28165.517980635 - }, - { - "type": "lha", - "id": 1002, - "x": -27500.813952358, - "y": -147000.65947136 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Bandar Lengeh", - "radials": [ - 270, - 315, - 0, - 45 - ], - "size": 600, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Shiraz International Airport", - "size": 2000, - "importance": 1.4, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Jiroft Airport", - "size": 2000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Kerman Airport", - "size": 2000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Lar Airbase", - "size": 1000, - "importance": 1.4 - } - ], - "links": [ - [ - "Khasab", - "Ras Al Khaimah" - ], - [ - "Bandar Lengeh", - "Lar Airbase" - ], - [ - "Havadarya", - "Lar Airbase" - ], - [ - "Bandar Abbas Intl", - "Havadarya" - ], - [ - "Bandar Abbas Intl", - "Jiroft Airport" - ], - [ - "Lar Airbase", - "Shiraz International Airport" - ], - [ - "Kerman Airport", - "Shiraz International Airport" - ], - [ - "Jiroft Airport", - "Kerman Airport" - ], - [ - "Kerman Airport", - "Lar Airbase" - ] - ] + "miz": "invasion_of_iran.miz" } \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran.miz b/resources/campaigns/invasion_of_iran.miz new file mode 100644 index 00000000..ac7ed7f6 Binary files /dev/null and b/resources/campaigns/invasion_of_iran.miz differ diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json index 840fb55c..684c9cc9 100644 --- a/resources/campaigns/invasion_of_iran_[lite].json +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -3,75 +3,5 @@ "theater": "Persian Gulf", "authors": "Khopa", "description": "This is lighter version of the invasion of Iran scenario.
", - "player_points": [ - { - "type": "airbase", - "id": "Bandar Lengeh", - "radials": [ - 270, - 315, - 0, - 45 - ], - "size": 600, - "importance": 1.4 - }, - { - "type": "carrier", - "id": 1001, - "x": 72000.324335475, - "y": -376000 - }, - { - "type": "lha", - "id": 1002, - "x": -27500.813952358, - "y": -147000.65947136 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Shiraz International Airport", - "size": 2000, - "importance": 1.4, - "captured_invert": true - }, - { - "type": "airbase", - "id": "Jiroft Airport", - "size": 2000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Kerman Airport", - "size": 2000, - "importance": 1.4 - }, - { - "type": "airbase", - "id": "Lar Airbase", - "size": 1000, - "importance": 1.4 - } - ], - "links": [ - [ - "Bandar Lengeh", - "Lar Airbase" - ], - [ - "Lar Airbase", - "Shiraz International Airport" - ], - [ - "Kerman Airport", - "Shiraz International Airport" - ], - [ - "Jiroft Airport", - "Kerman Airport" - ] - ] + "miz": "invasion_of_iran_lite.miz" } \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran_lite.miz b/resources/campaigns/invasion_of_iran_lite.miz new file mode 100644 index 00000000..5c0b2aad Binary files /dev/null and b/resources/campaigns/invasion_of_iran_lite.miz differ diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json index b656709b..caf3b123 100644 --- a/resources/campaigns/normandy.json +++ b/resources/campaigns/normandy.json @@ -3,83 +3,5 @@ "theater": "Normandy", "authors": "Khopa", "description": "Normandy 1944 D-Day scenario.
", - "player_points": [ - { - "type": "airbase", - "id": "Chailey", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Needs Oar Point", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Deux Jumeaux", - "size": 600, - "importance": 1 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Lignerolles", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Lessay", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Carpiquet", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Maupertus", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Evreux", - "size": 600, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Chailey", - "Needs Oar Point" - ], - [ - "Deux Jumeaux", - "Lignerolles" - ], - [ - "Lessay", - "Lignerolles" - ], - [ - "Carpiquet", - "Lignerolles" - ], - [ - "Lessay", - "Maupertus" - ], - [ - "Carpiquet", - "Evreux" - ] - ] + "miz":"normandy.miz" } \ No newline at end of file diff --git a/resources/campaigns/normandy.miz b/resources/campaigns/normandy.miz new file mode 100644 index 00000000..bdda71e8 Binary files /dev/null and b/resources/campaigns/normandy.miz differ diff --git a/resources/campaigns/normandy_full.miz b/resources/campaigns/normandy_full.miz new file mode 100644 index 00000000..6d78f013 Binary files /dev/null and b/resources/campaigns/normandy_full.miz differ diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json index 3bbee783..9aa63e78 100644 --- a/resources/campaigns/normandy_small.json +++ b/resources/campaigns/normandy_small.json @@ -3,53 +3,5 @@ "theater": "Normandy", "authors": "Khopa", "description": "A lighter version of the Normandy 1944 D-Day scenario.
", - "player_points": [ - { - "type": "airbase", - "id": "Needs Oar Point", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Deux Jumeaux", - "size": 600, - "importance": 1 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Lignerolles", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Carpiquet", - "size": 600, - "importance": 1 - }, - { - "type": "airbase", - "id": "Evreux", - "size": 600, - "importance": 1, - "captured_invert": true - } - ], - "links": [ - [ - "Deux Jumeaux", - "Lignerolles" - ], - [ - "Carpiquet", - "Lignerolles" - ], - [ - "Carpiquet", - "Evreux" - ] - ] + "miz": "normandy_small.miz" } \ No newline at end of file diff --git a/resources/campaigns/normandy_small.miz b/resources/campaigns/normandy_small.miz new file mode 100644 index 00000000..f422a7bb Binary files /dev/null and b/resources/campaigns/normandy_small.miz differ diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json index d79c4d92..490ebf93 100644 --- a/resources/campaigns/russia_small.json +++ b/resources/campaigns/russia_small.json @@ -3,37 +3,5 @@ "theater": "Caucasus", "authors": "Khopa", "description": "A small theater in Russia, progress from Mozdok to Maykop.
This scenario is pretty simple, it is ideal if you want to run a short campaign. If your PC is not powerful, this is also the less performance heavy scenario.
", - "player_points": [ - { - "type": "airbase", - "id": "Mozdok", - "size": 2000, - "importance": 1.1 - } - ], - "enemy_points": [ - { - "type": "airbase", - "id": "Mineralnye Vody", - "size": 2000, - "importance": 1.3 - }, - { - "type": "airbase", - "id": "Maykop-Khanskaya", - "size": 3000, - "importance": 1.4, - "captured_invert": true - } - ], - "links": [ - [ - "Mineralnye Vody", - "Mozdok" - ], - [ - "Maykop-Khanskaya", - "Mineralnye Vody" - ] - ] + "miz": "russia_small.miz" } \ No newline at end of file diff --git a/resources/campaigns/russia_small.miz b/resources/campaigns/russia_small.miz new file mode 100644 index 00000000..de890adb Binary files /dev/null and b/resources/campaigns/russia_small.miz differ diff --git a/resources/caulandmap.p b/resources/caulandmap.p index 5736275a..a65459cd 100644 Binary files a/resources/caulandmap.p and b/resources/caulandmap.p differ diff --git a/resources/channellandmap.p b/resources/channellandmap.p index 649ad174..9f5ba1f8 100644 Binary files a/resources/channellandmap.p and b/resources/channellandmap.p differ diff --git a/resources/customized_payloads/F-22A.lua b/resources/customized_payloads/F-22A.lua new file mode 100644 index 00000000..a14761df --- /dev/null +++ b/resources/customized_payloads/F-22A.lua @@ -0,0 +1,329 @@ +local unitPayloads = { + ["name"] = "F-22A", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [6] = { + ["name"] = "BAI", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [7] = { + ["name"] = "OCA", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [8] = { + ["name"] = "RUNWAY STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [8] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 11, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "F-22A", +} +return unitPayloads diff --git a/resources/customized_payloads/FA-18C_hornet.lua b/resources/customized_payloads/FA-18C_hornet.lua index bcf90452..085c0fcb 100644 --- a/resources/customized_payloads/FA-18C_hornet.lua +++ b/resources/customized_payloads/FA-18C_hornet.lua @@ -265,6 +265,50 @@ local unitPayloads = { [1] = 11, }, }, + [7] = { + ["name"] = "RUNWAY_ATTACK", + ["pylons"] = { + [1] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{BRU33_2X_MK-82}", + ["num"] = 5, + }, + [4] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "{FPU_8A_FUEL_TANK}", + ["num"] = 7, + }, + [7] = { + ["CLSID"] = "{FPU_8A_FUEL_TANK}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{BRU33_2X_MK-82}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{BRU33_2X_MK-82}", + ["num"] = 8, + }, + }, + ["tasks"] = { + [1] = 34, + }, + }, }, ["tasks"] = { }, diff --git a/resources/customized_payloads/Hercules.lua b/resources/customized_payloads/Hercules.lua new file mode 100644 index 00000000..2fc8590e --- /dev/null +++ b/resources/customized_payloads/Hercules.lua @@ -0,0 +1,189 @@ +local unitPayloads = { + ["name"] = "Hercules", + ["payloads"] = { + [1] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [2] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [3] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [5] = { + ["name"] = "DEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [6] = { + ["name"] = "OCA", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_BattleStation", + ["num"] = 9, + }, + [2] = { + ["CLSID"] = "{Herc_105mm_Howitzer}", + ["num"] = 8, + }, + [3] = { + ["CLSID"] = "{Herc_GAU_23A_Chain_Gun}", + ["num"] = 7, + }, + [4] = { + ["CLSID"] = "{Herc_M61_Vulcan_Rotary_Cannon}", + ["num"] = 6, + }, + [5] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + [7] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "Herc_JATO", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 31, + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "Hercules", +} +return unitPayloads diff --git a/resources/customized_payloads/I-16.lua b/resources/customized_payloads/I-16.lua new file mode 100644 index 00000000..df92fcd0 --- /dev/null +++ b/resources/customized_payloads/I-16.lua @@ -0,0 +1,257 @@ +local unitPayloads = { + ["name"] = "I-16", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + }, + ["tasks"] = { + [1] = 11, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [3] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + [4] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [5] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [4] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + [4] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [5] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [5] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [6] = { + ["name"] = "DEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [7] = { + ["name"] = "BAI", + ["pylons"] = { + [1] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 8, + }, + [2] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 7, + }, + [3] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 5, + }, + [5] = { + ["CLSID"] = "I16_FAB_100SV", + ["num"] = 4, + }, + [6] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 3, + }, + [7] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 2, + }, + [8] = { + ["CLSID"] = "I16_RS_82", + ["num"] = 1, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "I-16", +} +return unitPayloads diff --git a/resources/customized_payloads/M-2000C.lua b/resources/customized_payloads/M-2000C.lua index 52c706a1..03cd98e0 100644 --- a/resources/customized_payloads/M-2000C.lua +++ b/resources/customized_payloads/M-2000C.lua @@ -189,6 +189,54 @@ local unitPayloads = { [1] = 11, }, }, + [6] = { + ["name"] = "RUNWAY_ATTACK", + ["pylons"] = { + [1] = { + ["CLSID"] = "{Eclair}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{MMagicII}", + ["num"] = 9, + }, + [3] = { + ["CLSID"] = "{MMagicII}", + ["num"] = 1, + }, + [4] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 8, + }, + [5] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 2, + }, + [6] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 7, + }, + [7] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 4, + }, + [9] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 6, + }, + [10] = { + ["CLSID"] = "{BLG66_BELOUGA_AC}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 34, + }, + }, }, ["tasks"] = { }, diff --git a/resources/customized_payloads/Rafale_A_S.lua b/resources/customized_payloads/Rafale_A_S.lua index 40c61645..42f3abfd 100644 --- a/resources/customized_payloads/Rafale_A_S.lua +++ b/resources/customized_payloads/Rafale_A_S.lua @@ -2,38 +2,14 @@ local unitPayloads = { ["name"] = "Rafale_A_S", ["payloads"] = { [1] = { - ["name"] = "CAP", - ["pylons"] = { - [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", - ["num"] = 10, - }, - [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", - ["num"] = 1, - }, - [3] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", - ["num"] = 6, - }, - [4] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", - ["num"] = 4, - }, - }, - ["tasks"] = { - [1] = 11, - }, - }, - [2] = { ["name"] = "CAS", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -73,15 +49,15 @@ local unitPayloads = { [1] = 11, }, }, - [3] = { + [2] = { ["name"] = "ANTISHIP", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -121,15 +97,39 @@ local unitPayloads = { [1] = 11, }, }, + [3] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, [4] = { ["name"] = "SEAD", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -173,11 +173,11 @@ local unitPayloads = { ["name"] = "STRIKE", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -193,11 +193,11 @@ local unitPayloads = { ["num"] = 9, }, [6] = { - ["CLSID"] = "{SCALP}", + ["CLSID"] = "{GBU_49}", ["num"] = 8, }, [7] = { - ["CLSID"] = "{SCALP}", + ["CLSID"] = "{GBU_49}", ["num"] = 3, }, [8] = { diff --git a/resources/customized_payloads/Rafale_B.lua b/resources/customized_payloads/Rafale_B.lua new file mode 100644 index 00000000..f663618e --- /dev/null +++ b/resources/customized_payloads/Rafale_B.lua @@ -0,0 +1,265 @@ +local unitPayloads = { + ["name"] = "Rafale_B", + ["payloads"] = { + [1] = { + ["name"] = "CAP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + [2] = { + ["name"] = "CAS", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 8, + }, + [7] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 9, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + [3] = { + ["name"] = "ANTISHIP", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 8, + }, + [7] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 9, + }, + [10] = { + ["CLSID"] = "{Exocet}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + [4] = { + ["name"] = "SEAD", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", + ["num"] = 8, + }, + [7] = { + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}", + ["num"] = 9, + }, + [10] = { + ["CLSID"] = "{SCALP}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + [5] = { + ["name"] = "BAI", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 8, + }, + [7] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{AS_30L}", + ["num"] = 9, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + [6] = { + ["name"] = "STRIKE", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [4] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [5] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + [6] = { + ["CLSID"] = "{GBU_49}", + ["num"] = 8, + }, + [7] = { + ["CLSID"] = "{GBU_49}", + ["num"] = 3, + }, + [8] = { + ["CLSID"] = "{GBU_49}", + ["num"] = 2, + }, + [9] = { + ["CLSID"] = "{GBU_49}", + ["num"] = 9, + }, + [10] = { + ["CLSID"] = "{GBU_49}", + ["num"] = 5, + }, + }, + ["tasks"] = { + [1] = 32, + }, + }, + }, + ["tasks"] = { + }, + ["unitType"] = "Rafale_B", +} +return unitPayloads diff --git a/resources/customized_payloads/Rafale_M.lua b/resources/customized_payloads/Rafale_M.lua index ca564585..d0aaa5dc 100644 --- a/resources/customized_payloads/Rafale_M.lua +++ b/resources/customized_payloads/Rafale_M.lua @@ -2,10 +2,10 @@ local unitPayloads = { ["name"] = "Rafale_M", ["payloads"] = { [1] = { - ["name"] = "CAP", + ["name"] = "CAS", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { @@ -13,15 +13,15 @@ local unitPayloads = { ["num"] = 1, }, [3] = { - ["CLSID"] = "LAU-115_2*LAU-127_AIM-120C", + ["CLSID"] = "LAU3_HE5", ["num"] = 2, }, [4] = { - ["CLSID"] = "LAU-115_2*LAU-127_AIM-120C", + ["CLSID"] = "LAU3_HE5", ["num"] = 9, }, [5] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["CLSID"] = "LAU3_WP156", ["num"] = 8, }, [6] = { @@ -29,39 +29,47 @@ local unitPayloads = { ["num"] = 6, }, [7] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 5, }, [8] = { - ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["CLSID"] = "LAU3_WP156", ["num"] = 3, }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, }, ["tasks"] = { [1] = 11, }, }, [2] = { - ["name"] = "CAS", + ["name"] = "STRIKE", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 2, }, [4] = { - ["CLSID"] = "LAU3_HE5", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 9, }, [5] = { - ["CLSID"] = "LAU3_WP156", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 8, }, [6] = { @@ -69,39 +77,47 @@ local unitPayloads = { ["num"] = 6, }, [7] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 5, }, [8] = { - ["CLSID"] = "LAU3_WP156", + ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", ["num"] = 3, }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, }, ["tasks"] = { [1] = 11, }, }, [3] = { - ["name"] = "STRIKE", + ["name"] = "CAP", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { - ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 2, }, [4] = { - ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 9, }, [5] = { - ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 8, }, [6] = { @@ -109,13 +125,17 @@ local unitPayloads = { ["num"] = 6, }, [7] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 5, }, [8] = { - ["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 3, }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, }, ["tasks"] = { [1] = 11, @@ -125,11 +145,11 @@ local unitPayloads = { ["name"] = "SEAD", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -149,13 +169,21 @@ local unitPayloads = { ["num"] = 6, }, [7] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 5, }, [8] = { ["CLSID"] = "{D5D51E24-348C-4702-96AF-97A714E72697}", ["num"] = 3, }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, }, ["tasks"] = { [1] = 11, @@ -165,11 +193,11 @@ local unitPayloads = { ["name"] = "ANTISHIP", ["pylons"] = { [1] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 10, }, [2] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", ["num"] = 1, }, [3] = { @@ -189,13 +217,69 @@ local unitPayloads = { ["num"] = 6, }, [7] = { - ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["CLSID"] = "{RAFALE_MBDA_METEOR}", ["num"] = 5, }, [8] = { ["CLSID"] = "{18617C93-78E7-4359-A8CE-D754103EDF63}", ["num"] = 3, }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, + }, + ["tasks"] = { + [1] = 11, + }, + }, + [6] = { + ["name"] = "BAI", + ["pylons"] = { + [1] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 10, + }, + [2] = { + ["CLSID"] = "{FC23864E-3B80-48E3-9C03-4DA8B1D7497B}", + ["num"] = 1, + }, + [3] = { + ["CLSID"] = "{60CC734F-0AFA-4E2E-82B8-93B941AB11CF}", + ["num"] = 2, + }, + [4] = { + ["CLSID"] = "{60CC734F-0AFA-4E2E-82B8-93B941AB11CF}", + ["num"] = 3, + }, + [5] = { + ["CLSID"] = "{60CC734F-0AFA-4E2E-82B8-93B941AB11CF}", + ["num"] = 8, + }, + [6] = { + ["CLSID"] = "{60CC734F-0AFA-4E2E-82B8-93B941AB11CF}", + ["num"] = 9, + }, + [7] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 6, + }, + [8] = { + ["CLSID"] = "{RAFALE_MBDA_METEOR}", + ["num"] = 5, + }, + [9] = { + ["CLSID"] = "{0DA03783-61E4-40B2-8FAE-6AEE0A5C5AAE}", + ["num"] = 4, + }, + [10] = { + ["CLSID"] = "{DAMOCLES}", + ["num"] = 7, + }, }, ["tasks"] = { [1] = 11, diff --git a/resources/factions/allies_1944.json b/resources/factions/allies_1944.json index 77652519..1430b8f7 100644 --- a/resources/factions/allies_1944.json +++ b/resources/factions/allies_1944.json @@ -38,10 +38,7 @@ "Infantry_SMLE_No_4_Mk_1", "Infantry_M1_Garand" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ "AllyWW2FlakGenerator", "BoforsGenerator" ], diff --git a/resources/factions/allies_1944_free.json b/resources/factions/allies_1944_free.json index 56b1a781..133d3bed 100644 --- a/resources/factions/allies_1944_free.json +++ b/resources/factions/allies_1944_free.json @@ -25,10 +25,7 @@ "infantry_units": [ "Paratrooper_AKS" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ "BoforsGenerator" ], "aircraft_carrier": [ diff --git a/resources/factions/australia_2005.json b/resources/factions/australia_2005.json index 58f88930..8e9109d2 100644 --- a/resources/factions/australia_2005.json +++ b/resources/factions/australia_2005.json @@ -29,12 +29,10 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "RapierGenerator" - ], - "sams": [ + "air_defenses": [ "HawkGenerator", "RapierGenerator" ], diff --git a/resources/factions/bluefor_coldwar.json b/resources/factions/bluefor_coldwar.json index b06ad1fb..874484b4 100644 --- a/resources/factions/bluefor_coldwar.json +++ b/resources/factions/bluefor_coldwar.json @@ -37,13 +37,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ + "air_defenses": [ + "ChaparralGenerator", "EarlyColdWarFlakGenerator", - "VulcanGenerator" - ], - "sams": [ "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/bluefor_coldwar_a4.json b/resources/factions/bluefor_coldwar_a4.json index 78cc28fb..f5a5a56d 100644 --- a/resources/factions/bluefor_coldwar_a4.json +++ b/resources/factions/bluefor_coldwar_a4.json @@ -38,13 +38,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ + "air_defenses": [ + "ChaparralGenerator", "EarlyColdWarFlakGenerator", - "VulcanGenerator" - ], - "sams": [ "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/bluefor_coldwar_a4_mb339.json b/resources/factions/bluefor_coldwar_a4_mb339.json index a0c31139..57c50355 100644 --- a/resources/factions/bluefor_coldwar_a4_mb339.json +++ b/resources/factions/bluefor_coldwar_a4_mb339.json @@ -39,13 +39,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ + "air_defenses": [ + "ChaparralGenerator", "EarlyColdWarFlakGenerator", - "VulcanGenerator" - ], - "sams": [ "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/bluefor_modern.json b/resources/factions/bluefor_modern.json index 49e2e1cf..f422a6eb 100644 --- a/resources/factions/bluefor_modern.json +++ b/resources/factions/bluefor_modern.json @@ -54,12 +54,11 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ + "air_defenses": [ + "AvengerGenerator", "HawkGenerator", "PatriotGenerator" ], @@ -96,5 +95,6 @@ "ArleighBurkeGroupGenerator" ], "has_jtac": true, - "jtac_unit": "MQ_9_Reaper" + "jtac_unit": "MQ_9_Reaper", + "unrestricted_satnav": true } diff --git a/resources/factions/canada_2005.json b/resources/factions/canada_2005.json index 32883ce3..9a170a6f 100644 --- a/resources/factions/canada_2005.json +++ b/resources/factions/canada_2005.json @@ -29,14 +29,12 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator", - "AvengerGenerator" + "air_defenses": [ + "AvengerGenerator", + "HawkGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/china_2010.json b/resources/factions/china_2010.json index 28f5253d..2fe1cb6f 100644 --- a/resources/factions/china_2010.json +++ b/resources/factions/china_2010.json @@ -35,18 +35,21 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA9Generator", - "SA13Generator", - "ZSU23Generator", - "ZU23Generator" - ], - "sams": [ + "air_defenses": [ "HQ7Generator", + "SA2Generator", + "SA6Generator", + "SA9Generator", "SA10Generator", - "SA6Generator" + "SA11Generator", + "SA13Generator", + "Tier2SA10Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/france_1995.json b/resources/factions/france_1995.json index b548eebb..2c553009 100644 --- a/resources/factions/france_1995.json +++ b/resources/factions/france_1995.json @@ -35,13 +35,10 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "HQ7Generator", - "RolandGenerator" - ], - "sams": [ + "air_defenses": [ "RolandGenerator", "HawkGenerator" ], diff --git a/resources/factions/france_2005_frenchpack.json b/resources/factions/france_2005_frenchpack.json index d909c08b..cb13cb4e 100644 --- a/resources/factions/france_2005_frenchpack.json +++ b/resources/factions/france_2005_frenchpack.json @@ -45,11 +45,7 @@ "Soldier_M249", "Stinger_MANPADS" ], - "shorads": [ - "HQ7Generator", - "RolandGenerator" - ], - "sams": [ + "air_defenses": [ "RolandGenerator", "HawkGenerator" ], @@ -69,13 +65,14 @@ "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974" }, "carrier_names": [ + "R91 Charles de Gaulle" + ], + "helicopter_carrier_names": [ + "R97 Jeanne d'Arc", "L9013 Mistral", "L9014 Tonerre", "L9015 Dixmude" ], - "helicopter_carrier_names": [ - "Jeanne d'Arc" - ], "navy_generators": [ "ArleighBurkeGroupGenerator" ], diff --git a/resources/factions/france_2005_modded.json b/resources/factions/france_2005_modded.json index 87624cc8..34e4511a 100644 --- a/resources/factions/france_2005_modded.json +++ b/resources/factions/france_2005_modded.json @@ -8,6 +8,7 @@ "Mirage_2000_5", "Rafale_M", "Rafale_A_S", + "Rafale_B", "SA342M", "SA342L", "SA342Mistral" @@ -44,13 +45,10 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "HQ7Generator", - "RolandGenerator" - ], - "sams": [ + "air_defenses": [ "RolandGenerator", "HawkGenerator" ], @@ -71,16 +69,17 @@ ], "requirements": { "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974", - "RAFALE 2.5.5": "https://www.digitalcombatsimulator.com/fr/files/3307478/" + "RAFALE 2.5.6": "https://forums.eagle.ru/forum/english/dcs-world-topics/mods-and-apps/dcs-mods/7135261-download-rafales-pack-2-5-6-55960-and-openbeta-2-5-6-57530-by-cuesta-brothers" }, "carrier_names": [ + "R91 Charles de Gaulle" + ], + "helicopter_carrier_names": [ + "R97 Jeanne d'Arc", "L9013 Mistral", "L9014 Tonerre", "L9015 Dixmude" ], - "helicopter_carrier_names": [ - "Jeanne d'Arc" - ], "navy_generators": [ "ArleighBurkeGroupGenerator" ], diff --git a/resources/factions/georgia_2008.json b/resources/factions/georgia_2008.json index f575054c..45b8cfc1 100644 --- a/resources/factions/georgia_2008.json +++ b/resources/factions/georgia_2008.json @@ -20,7 +20,7 @@ "MBT_T_55" ], "artillery_units": [ - "MLRS_BM21_Grad", + "MLRS_BM_21_Grad", "SPH_2S1_Gvozdika", "SPH_2S3_Akatsia" ], @@ -30,15 +30,19 @@ ], "infantry_units": [ "Paratrooper_AKS", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA13Generator", - "SA8Generator" - ], - "sams": [ + "air_defenses": [ + "SA3Generator", "SA6Generator", - "SA11Generator" + "SA8Generator", + "SA11Generator", + "SA13Generator", + "SA15Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "requirements": {}, "has_jtac": true, diff --git a/resources/factions/germany_1942.json b/resources/factions/germany_1942.json index aaaeec17..fda75025 100644 --- a/resources/factions/germany_1942.json +++ b/resources/factions/germany_1942.json @@ -27,12 +27,9 @@ "infantry_units": [ "Infantry_Mauser_98" ], - "shorads": [ - "FlakGenerator" - ], - "sams": [ - "FreyaGenerator", - "FlakGenerator" + "air_defenses": [ + "FlakGenerator", + "FreyaGenerator" ], "aircraft_carrier": [ ], diff --git a/resources/factions/germany_1944.json b/resources/factions/germany_1944.json index 7d4dc7d6..273a4519 100644 --- a/resources/factions/germany_1944.json +++ b/resources/factions/germany_1944.json @@ -32,10 +32,7 @@ "infantry_units": [ "Infantry_Mauser_98" ], - "shorads": [ - "FlakGenerator" - ], - "sams": [ + "air_defenses": [ "FlakGenerator", "FreyaGenerator" ], diff --git a/resources/factions/germany_1944_free.json b/resources/factions/germany_1944_free.json index 30eb83f3..8fc2421c 100644 --- a/resources/factions/germany_1944_free.json +++ b/resources/factions/germany_1944_free.json @@ -20,10 +20,7 @@ "infantry_units": [ "Infantry_Soldier_Rus" ], - "shorads": [ - "Flak18Generator" - ], - "sams": [ + "air_defenses": [ "Flak18Generator" ], "aircraft_carrier": [ diff --git a/resources/factions/germany_1990.json b/resources/factions/germany_1990.json index 9d13533b..bdf243e7 100644 --- a/resources/factions/germany_1990.json +++ b/resources/factions/germany_1990.json @@ -31,17 +31,17 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "RolandGenerator" - ], - "sams": [ + "air_defenses": [ + "GepardGenerator", "HawkGenerator", - "RolandGenerator" + "RolandGenerator", + "PatriotGenerator" ], "ewrs": [ - "HawkEwrGenerator" + "PatriotEwrGenerator" ], "aircraft_carrier": [ ], diff --git a/resources/factions/india_2010.json b/resources/factions/india_2010.json index 0735d9a3..7b6cedb3 100644 --- a/resources/factions/india_2010.json +++ b/resources/factions/india_2010.json @@ -34,17 +34,21 @@ ], "infantry_units": [ "Infantry_M4", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ + "air_defenses": [ + "SA3Generator", + "SA6Generator", "SA8Generator", + "SA9Generator", + "SA10Generator", + "SA11Generator", "SA13Generator", "SA19Generator", - "ZSU23Generator" - ], - "sams": [ - "SA6Generator", - "SA3Generator" + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/insurgents.json b/resources/factions/insurgents.json index a124f93f..8634c534 100644 --- a/resources/factions/insurgents.json +++ b/resources/factions/insurgents.json @@ -21,15 +21,13 @@ ], "infantry_units": [ "Infantry_Soldier_Insurgents", - "Soldier_RPG" + "Soldier_RPG", + "SAM_SA_18_Igla_MANPADS" ], - "shorads": [ + "air_defenses": [ "SA9Generator", + "ZSU23Generator", "ZU23Generator", - "ZSU23Generator" - ], - "sams": [ - "ZU23Generator", - "ZSU23Generator" + "ZU23UralInsurgentGenerator" ] } diff --git a/resources/factions/insurgents_modded.json b/resources/factions/insurgents_modded.json index 1a199e53..296ee8b5 100644 --- a/resources/factions/insurgents_modded.json +++ b/resources/factions/insurgents_modded.json @@ -22,17 +22,14 @@ ], "infantry_units": [ "Infantry_Soldier_Insurgents", - "Soldier_RPG" + "Soldier_RPG", + "SAM_SA_18_Igla_MANPADS" ], - "shorads": [ + "air_defenses": [ "SA9Generator", "ZU23Generator", "ZSU23Generator" ], - "sams": [ - "ZU23Generator", - "ZSU23Generator" - ], "requirements": { "frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974" } diff --git a/resources/factions/iran_2015.json b/resources/factions/iran_2015.json index fee95e9e..cb6ba5f2 100644 --- a/resources/factions/iran_2015.json +++ b/resources/factions/iran_2015.json @@ -40,18 +40,20 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Insurgents", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "HQ7Generator", - "ZSU23Generator" - ], - "sams": [ + "air_defenses": [ + "HawkGenerator", + "RapierGenerator", "SA2Generator", "SA6Generator", "SA11Generator", - "HawkGenerator", - "HQ7Generator" + "SA15Generator", + "VulcanGenerator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "TallRackGenerator" diff --git a/resources/factions/israel_1948.json b/resources/factions/israel_1948.json index 99685c84..b2c86e7b 100644 --- a/resources/factions/israel_1948.json +++ b/resources/factions/israel_1948.json @@ -25,12 +25,9 @@ "infantry_units": [ "Infantry_SMLE_No_4_Mk_1" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ - "EarlyColdWarFlakGenerator", - "BoforsGenerator" + "air_defenses": [ + "BoforsGenerator", + "EarlyColdWarFlakGenerator" ], "aircraft_carrier": [ ], diff --git a/resources/factions/israel_1973.json b/resources/factions/israel_1973.json index 1ce2067d..c0d3482b 100644 --- a/resources/factions/israel_1973.json +++ b/resources/factions/israel_1973.json @@ -30,12 +30,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ + "BoforsGenerator", + "ChaparralGenerator", "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/israel_1982.json b/resources/factions/israel_1982.json index d81462e3..b527416a 100644 --- a/resources/factions/israel_1982.json +++ b/resources/factions/israel_1982.json @@ -33,12 +33,10 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "ChaparralGenerator" - ], - "sams": [ + "air_defenses": [ + "ChaparralGenerator", "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/israel_2000.json b/resources/factions/israel_2000.json index 9dd42d7d..c23c315e 100644 --- a/resources/factions/israel_2000.json +++ b/resources/factions/israel_2000.json @@ -34,14 +34,14 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "ChaparralGenerator" - ], - "sams": [ + "air_defenses": [ + "ChaparralGenerator", "HawkGenerator", - "ChaparralGenerator" + "PatriotGenerator", + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/italy_1990.json b/resources/factions/italy_1990.json index 30a05926..62ac4ce5 100644 --- a/resources/factions/italy_1990.json +++ b/resources/factions/italy_1990.json @@ -28,14 +28,12 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator", - "AvengerGenerator" + "air_defenses": [ + "AvengerGenerator", + "HawkGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/italy_1990_mb339.json b/resources/factions/italy_1990_mb339.json index bad67565..186926f7 100644 --- a/resources/factions/italy_1990_mb339.json +++ b/resources/factions/italy_1990_mb339.json @@ -29,14 +29,12 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator", - "AvengerGenerator" + "air_defenses": [ + "AvengerGenerator", + "HawkGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/japan_2005.json b/resources/factions/japan_2005.json index f3c6fcb4..de03dbd8 100644 --- a/resources/factions/japan_2005.json +++ b/resources/factions/japan_2005.json @@ -27,7 +27,8 @@ ], "artillery_units": [ "SPH_M109_Paladin", - "MLRS_M270" + "MLRS_M270", + "Stinger_MANPADS" ], "logistics_units": [ "Transport_M818" @@ -36,10 +37,8 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "GepardGenerator" - ], - "sams": [ + "air_defenses": [ + "GepardGenerator", "HawkGenerator", "PatriotGenerator" ], diff --git a/resources/factions/libya_2011.json b/resources/factions/libya_2011.json index 2bc06c93..8090ea3c 100644 --- a/resources/factions/libya_2011.json +++ b/resources/factions/libya_2011.json @@ -31,17 +31,19 @@ ], "infantry_units": [ "Infantry_Soldier_Insurgents", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_MANPADS" ], - "shorads": [ + "air_defenses": [ "HQ7Generator", - "SA8Generator", - "ZSU23Generator" - ], - "sams": [ + "RapierGenerator", "SA2Generator", "SA3Generator", - "SA6Generator" + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA13Generator", + "ZSU23Generator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/netherlands_1990.json b/resources/factions/netherlands_1990.json index c102df32..44fc54ed 100644 --- a/resources/factions/netherlands_1990.json +++ b/resources/factions/netherlands_1990.json @@ -26,16 +26,18 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator" + "air_defenses": [ + "AvengerGenerator", + "GepardGenerator", + "HawkGenerator", + "PatriotGenerator", + "RapierGenerator" ], "ewrs": [ - "HawkEwrGenerator" + "PatriotEwrGenerator" ], "aircraft_carrier": [ ], diff --git a/resources/factions/north_korea_2000.json b/resources/factions/north_korea_2000.json index ce1d4331..a790b576 100644 --- a/resources/factions/north_korea_2000.json +++ b/resources/factions/north_korea_2000.json @@ -38,18 +38,17 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA9Generator", - "SA13Generator", - "ZSU23Generator", - "ZU23Generator" - ], - "sams": [ + "air_defenses": [ "SA2Generator", "SA3Generator", - "SA6Generator" + "SA6Generator", + "SA9Generator", + "SA13Generator", + "ZU23Generator", + "ZSU23Generator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/pakistan_2015.json b/resources/factions/pakistan_2015.json index 28d14bb4..43ba4bba 100644 --- a/resources/factions/pakistan_2015.json +++ b/resources/factions/pakistan_2015.json @@ -36,16 +36,16 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "Stinger_MANPADS" ], - "shorads": [ + "air_defenses": [ "HQ7Generator", - "ZU23UralGenerator", - "ZU23Generator" - ], - "sams": [ + "SA2Generator", "SA10Generator", - "SA2Generator" + "SA11Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/pmc_russian.json b/resources/factions/pmc_russian.json index 57451f5d..844bdb3c 100644 --- a/resources/factions/pmc_russian.json +++ b/resources/factions/pmc_russian.json @@ -24,13 +24,11 @@ ], "infantry_units": [ "Paratrooper_AKS", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA13Generator", - "SA9Generator" - ], - "sams": [ + "air_defenses": [ + "SA9Generator", "SA13Generator" ] } diff --git a/resources/factions/pmc_us.json b/resources/factions/pmc_us.json index 4c8c6230..e75e193f 100644 --- a/resources/factions/pmc_us.json +++ b/resources/factions/pmc_us.json @@ -20,12 +20,10 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ + "air_defenses": [ "AvengerGenerator" ], "has_jtac": true, diff --git a/resources/factions/pmc_us_with_mb339.json b/resources/factions/pmc_us_with_mb339.json index 3d42fa03..ef01e093 100644 --- a/resources/factions/pmc_us_with_mb339.json +++ b/resources/factions/pmc_us_with_mb339.json @@ -21,12 +21,10 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ + "air_defenses": [ "AvengerGenerator" ], "requirements": { diff --git a/resources/factions/russia_1955.json b/resources/factions/russia_1955.json index a00d71cf..ca7e1994 100644 --- a/resources/factions/russia_1955.json +++ b/resources/factions/russia_1955.json @@ -30,10 +30,7 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ - "EarlyColdWarFlakGenerator" - ], - "sams": [ + "air_defenses": [ "EarlyColdWarFlakGenerator" ], "aircraft_carrier": [ diff --git a/resources/factions/russia_1965.json b/resources/factions/russia_1965.json index 6dcc7f1c..d1d7b870 100644 --- a/resources/factions/russia_1965.json +++ b/resources/factions/russia_1965.json @@ -34,15 +34,13 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ - "ZSU23Generator", + "air_defenses": [ "EarlyColdWarFlakGenerator", - "ZU23Generator" - ], - "sams": [ "SA2Generator", "SA3Generator", - "SA6Generator" + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "FlatFaceGenerator" diff --git a/resources/factions/russia_1970_limited_air.json b/resources/factions/russia_1970_limited_air.json new file mode 100644 index 00000000..a26cfda2 --- /dev/null +++ b/resources/factions/russia_1970_limited_air.json @@ -0,0 +1,69 @@ +{ + "country": "Russia", + "name": "Russia 1970 Limited Air", + "authors": "Starfire", + "description": "1970 Soviet Russia, with limited air units intended for a Viggen campaign.
", + "aircrafts": [ + "MiG_19P", + "MiG_21Bis", + "Mi_8MT", + "Mi_24V" + ], + "frontline_units": [ + "ARV_BRDM_2", + "APC_BTR_80", + "IFV_BMD_1", + "IFV_BMP_1", + "MBT_T_55" + ], + "artillery_units": [ + "MLRS_BM_21_Grad", + "SPH_2S9_Nona", + "SPH_2S1_Gvozdika" + ], + "logistics_units": [ + "Transport_Ural_375", + "Transport_UAZ_469" + ], + "infantry_units": [ + "Infantry_Soldier_Rus", + "Soldier_RPG" + ], + "air_defenses": [ + "SA2Generator", + "SA3Generator", + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA11Generator", + "ColdWarFlakGenerator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" + ], + "ewrs": [ + "FlatFaceGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "helicopter_carrier_names": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "missiles": [ + "ScudGenerator" + ], + "missiles_group_count": 1, + "requirements": {}, + "carrier_names": [ + ], + "navy_generators": [ + "RussianNavyGroupGenerator" + ], + "has_jtac": false, + "doctrine": "coldwar" +} diff --git a/resources/factions/russia_1975.json b/resources/factions/russia_1975.json index 337cf1bc..4d63cc15 100644 --- a/resources/factions/russia_1975.json +++ b/resources/factions/russia_1975.json @@ -40,14 +40,17 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ + "air_defenses": [ "ColdWarFlakGenerator", - "ZSU23Generator", - "ZU23Generator" - ], - "sams": [ + "SA2Generator", "SA3Generator", - "SA6Generator" + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA13Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "FlatFaceGenerator" diff --git a/resources/factions/russia_1990.json b/resources/factions/russia_1990.json index 01142f33..dd9048bd 100644 --- a/resources/factions/russia_1990.json +++ b/resources/factions/russia_1990.json @@ -42,15 +42,25 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA13Generator", - "SA8Generator" - ], - "sams": [ + "air_defenses": [ + "SA2Generator", + "SA3Generator", "SA6Generator", - "SA11Generator" + "SA8Generator", + "SA9Generator", + "SA10Generator", + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/russia_2010.json b/resources/factions/russia_2010.json index d911f2a6..cc2631df 100644 --- a/resources/factions/russia_2010.json +++ b/resources/factions/russia_2010.json @@ -46,17 +46,22 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_MANPADS" ], - "shorads": [ - "SA19Generator", - "SA13Generator" - ], - "sams": [ - "SA11Generator", + "air_defenses": [ + "SA8Generator", + "SA9Generator", "SA10Generator", - "SA6Generator", - "SA19Generator" + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/russia_2020.json b/resources/factions/russia_2020.json index 51dd2ea7..b1c45b8a 100644 --- a/resources/factions/russia_2020.json +++ b/resources/factions/russia_2020.json @@ -45,15 +45,21 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA19Generator" - ], - "sams": [ - "SA11Generator", + "air_defenses": [ + "SA8Generator", + "SA9Generator", "SA10Generator", - "SA19Generator" + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/soviet_union_1943.json b/resources/factions/soviet_union_1943.json new file mode 100644 index 00000000..e30a34dc --- /dev/null +++ b/resources/factions/soviet_union_1943.json @@ -0,0 +1,50 @@ +{ + "country": "USSR", + "name": "Soviet Union 1943", + "authors": "Khopa", + "description": "Soviet Union in 1943. Featuring the I16, and using some allies units to represent either lend leased vehicles or soviet equivalent vehicles. BM-21 is used to represent BM-13
", + "aircrafts": [ + "SpitfireLFMkIX", + "I_16" + ], + "frontline_units": [ + "MT_M4_Sherman", + "APC_M2A1", + "Daimler_Armoured_Car", + "LT_Mk_VII_Tetrarch" + ], + "artillery_units": [ + "MLRS_BM_21_Grad" + ], + "logistics_units": [ + "Bedford_MWD", + "CCKW_353" + ], + "infantry_units": [ + "Infantry_SMLE_No_4_Mk_1" + ], + "air_defenses": [ + "AllyWW2FlakGenerator", + "BoforsGenerator" + ], + "aircraft_carrier": [ + ], + "helicopter_carrier": [ + ], + "destroyers": [ + ], + "cruisers": [ + ], + "requirements": { + "WW2 Asset Pack": "https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/" + }, + "carrier_names": [ + ], + "helicopter_carrier_names": [ + ], + "navy_generators": [ + ], + "has_jtac": false, + "doctrine": "ww2", + "building_set": "ww2ally" +} diff --git a/resources/factions/spain_1990.json b/resources/factions/spain_1990.json index 991c929c..8eb658bf 100644 --- a/resources/factions/spain_1990.json +++ b/resources/factions/spain_1990.json @@ -31,14 +31,14 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator" + "air_defenses": [ + "AvengerGenerator", + "HawkGenerator", + "PatriotGenerator", + "RolandGenerator" ], "ewrs": [ - "HawkEwrGenerator" + "PatriotEwrGenerator" ], "aircraft_carrier": [ "CVN_74_John_C__Stennis" diff --git a/resources/factions/sweden_1970.json b/resources/factions/sweden_1970.json new file mode 100644 index 00000000..55362275 --- /dev/null +++ b/resources/factions/sweden_1970.json @@ -0,0 +1,47 @@ +{ + "country": "Sweden", + "name": "Sweden 1970s Alternate Universe", + "authors": "Starfire", + "description": "Sweden 1970
Since we do not yet have Heatblur's AI Draken, this faction includes the Mirage 2000C in order to provide Sweden with some form of A2A capability.
", + "aircrafts": [ + "AJS37", + "M_2000C", + "UH_1H" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "IFV_MCV_80", + "MBT_Leopard_2", + "APC_M1126_Stryker_ICV" + ], + "artillery_units": [ + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249" + ], + "air_defenses": [ + "ChaparralGenerator", + "EarlyColdWarFlakGenerator", + "AvengerGenerator", + "HawkGenerator", + "VulcanGenerator" + ], + "ewrs": [ + "HawkEwrGenerator" + ], + "navy_generators": [ + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": false, + "doctrine": "coldwar" + } diff --git a/resources/factions/sweden_1990.json b/resources/factions/sweden_1990.json index 6a844be5..c5d78b6e 100644 --- a/resources/factions/sweden_1990.json +++ b/resources/factions/sweden_1990.json @@ -26,12 +26,11 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ + "air_defenses": [ + "AvengerGenerator", "HawkGenerator" ], "ewrs": [ diff --git a/resources/factions/syria_1948.json b/resources/factions/syria_1948.json index 012cb500..b8814e6e 100644 --- a/resources/factions/syria_1948.json +++ b/resources/factions/syria_1948.json @@ -22,10 +22,7 @@ "infantry_units": [ "Infantry_SMLE_No_4_Mk_1" ], - "shorads": [ - "FlakGenerator" - ], - "sams": [ + "air_defenses": [ "FlakGenerator" ], "aircraft_carrier": [ diff --git a/resources/factions/syria_1967.json b/resources/factions/syria_1967.json index 22d1b445..b0e60931 100644 --- a/resources/factions/syria_1967.json +++ b/resources/factions/syria_1967.json @@ -33,13 +33,13 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ + "air_defenses": [ + "SA2Generator", + "SA3Generator", + "ZSU23Generator", "ZU23Generator", "ZU23UralGenerator" ], - "sams": [ - "SA2Generator" - ], "ewrs": [ "FlatFaceGenerator" ], diff --git a/resources/factions/syria_1967_with_ww2_weapons.json b/resources/factions/syria_1967_with_ww2_weapons.json index f7d95399..32d43a04 100644 --- a/resources/factions/syria_1967_with_ww2_weapons.json +++ b/resources/factions/syria_1967_with_ww2_weapons.json @@ -36,15 +36,14 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ + "air_defenses": [ + "EarlyColdWarFlakGenerator", + "SA2Generator", + "SA3Generator", + "ZSU23Generator", "ZU23Generator", - "EarlyColdWarFlakGenerator", "ZU23UralGenerator" ], - "sams": [ - "EarlyColdWarFlakGenerator", - "SA2Generator" - ], "ewrs": [ "FlatFaceGenerator" ], diff --git a/resources/factions/syria_1973.json b/resources/factions/syria_1973.json index 1cd10e20..fa882815 100644 --- a/resources/factions/syria_1973.json +++ b/resources/factions/syria_1973.json @@ -33,14 +33,16 @@ "Infantry_Soldier_Rus", "Soldier_RPG" ], - "shorads": [ + "air_defenses": [ "EarlyColdWarFlakGenerator", - "ZU23Generator" - ], - "sams": [ "SA2Generator", "SA3Generator", - "SA6Generator" + "SA6Generator", + "SA8Generator", + "SA9Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "FlatFaceGenerator" diff --git a/resources/factions/syria_1982.json b/resources/factions/syria_1982.json index 8dbfd92d..22c4b55a 100644 --- a/resources/factions/syria_1982.json +++ b/resources/factions/syria_1982.json @@ -35,15 +35,18 @@ "Infantry_Soldier_Rus", "Paratrooper_RPG_16" ], - "shorads": [ - "ZU23Generator", + "air_defenses": [ + "ColdWarFlakGenerator", "EarlyColdWarFlakGenerator", - "ColdWarFlakGenerator" - ], - "sams": [ "SA2Generator", "SA3Generator", - "SA6Generator" + "SA6Generator", + "SA8Generator", + "SA9Generator", + "SA13Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator" diff --git a/resources/factions/syria_2011.json b/resources/factions/syria_2011.json index 4de4c7d4..4153f679 100644 --- a/resources/factions/syria_2011.json +++ b/resources/factions/syria_2011.json @@ -1,5 +1,5 @@ { - "country": "Syria", + "country": "Combined Joint Task Forces Red", "name": "Syria 2011", "authors": "Khopa", "description": "Syrian Arab Army at the start of the Syrian Civil War.
", @@ -46,22 +46,24 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA8Generator", - "SA9Generator", - "SA13Generator", - "SA19Generator", - "ZSU23Generator", - "ColdWarFlakGenerator" - ], - "sams": [ + "air_defenses": [ + "ColdWarFlakGenerator", "SA2Generator", "SA3Generator", "SA6Generator", + "SA8Generator", + "SA8Generator", + "SA9Generator", "SA10Generator", - "SA11Generator" + "SA11Generator", + "SA13Generator", + "SA19Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BoxSpringGenerator", diff --git a/resources/factions/turkey_2005.json b/resources/factions/turkey_2005.json index 22c7d023..0a11e63a 100644 --- a/resources/factions/turkey_2005.json +++ b/resources/factions/turkey_2005.json @@ -32,15 +32,16 @@ "infantry_units": [ "Infantry_M4", "Soldier_M249", - "Paratrooper_AKS" + "Paratrooper_AKS", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ + "air_defenses": [ "AvengerGenerator", + "HawkGenerator", + "RapierGenerator", + "SA3Generator", "ZSU23Generator" ], - "sams": [ - "HawkGenerator" - ], "ewrs": [ "HawkEwrGenerator" ], diff --git a/resources/factions/uae_2005.json b/resources/factions/uae_2005.json index f7e65986..bff684e0 100644 --- a/resources/factions/uae_2005.json +++ b/resources/factions/uae_2005.json @@ -28,14 +28,13 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ + "air_defenses": [ + "HawkGenerator", "RapierGenerator" ], - "sams": [ - "HawkGenerator" - ], "ewrs": [ "HawkEwrGenerator" ], diff --git a/resources/factions/uk_1944.json b/resources/factions/uk_1944.json index 81129848..c771db2c 100644 --- a/resources/factions/uk_1944.json +++ b/resources/factions/uk_1944.json @@ -33,10 +33,7 @@ "infantry_units": [ "Infantry_SMLE_No_4_Mk_1" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ "AllyWW2FlakGenerator", "BoforsGenerator" ], diff --git a/resources/factions/uk_1990.json b/resources/factions/uk_1990.json index 39e7bedb..b3db7880 100644 --- a/resources/factions/uk_1990.json +++ b/resources/factions/uk_1990.json @@ -32,15 +32,14 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ + "air_defenses": [ "AvengerGenerator", + "HawkGenerator", "RapierGenerator" ], - "sams": [ - "HawkGenerator" - ], "ewrs": [ "HawkEwrGenerator" ], diff --git a/resources/factions/ukraine_2010.json b/resources/factions/ukraine_2010.json index f1a4d9cb..dac93cb8 100644 --- a/resources/factions/ukraine_2010.json +++ b/resources/factions/ukraine_2010.json @@ -36,17 +36,24 @@ "infantry_units": [ "Paratrooper_AKS", "Infantry_Soldier_Rus", - "Paratrooper_RPG_16" + "Paratrooper_RPG_16", + "SAM_SA_18_Igla_S_MANPADS" ], - "shorads": [ - "SA9Generator", - "SA13Generator", - "SA19Generator" - ], - "sams": [ + "air_defenses": [ "SA3Generator", + "SA6Generator", + "SA8Generator", + "SA9Generator", "SA10Generator", - "SA11Generator" + "SA11Generator", + "SA13Generator", + "SA15Generator", + "SA19Generator", + "Tier2SA10Generator", + "Tier3SA10Generator", + "ZSU23Generator", + "ZU23Generator", + "ZU23UralGenerator" ], "ewrs": [ "BigBirdGenerator" diff --git a/resources/factions/us_aggressors.json b/resources/factions/us_aggressors.json index e3bf8108..eefebd9a 100644 --- a/resources/factions/us_aggressors.json +++ b/resources/factions/us_aggressors.json @@ -46,10 +46,8 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ + "air_defenses": [ + "AvengerGenerator", "HawkGenerator", "PatriotGenerator" ], diff --git a/resources/factions/usa_1944.json b/resources/factions/usa_1944.json index 87725411..f1c78ece 100644 --- a/resources/factions/usa_1944.json +++ b/resources/factions/usa_1944.json @@ -30,10 +30,7 @@ "infantry_units": [ "Infantry_M1_Garand" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ "AllyWW2FlakGenerator", "BoforsGenerator" ], diff --git a/resources/factions/usa_1955.json b/resources/factions/usa_1955.json index c8a26c33..482fd683 100644 --- a/resources/factions/usa_1955.json +++ b/resources/factions/usa_1955.json @@ -23,10 +23,8 @@ "infantry_units": [ "Infantry_M4" ], - "shorads": [ - "BoforsGenerator" - ], - "sams": [ + "air_defenses": [ + "BoforsGenerator", "EarlyColdWarFlakGenerator" ], "doctrine": "ww2", diff --git a/resources/factions/usa_1960.json b/resources/factions/usa_1960.json index 34e3db33..4397748f 100644 --- a/resources/factions/usa_1960.json +++ b/resources/factions/usa_1960.json @@ -22,13 +22,10 @@ "infantry_units": [ "Infantry_M4" ], - "shorads": [ + "air_defenses": [ "EarlyColdWarFlakGenerator", "VulcanGenerator" ], - "sams": [ - "VulcanGenerator" - ], "requirements": {}, "doctrine": "coldwar" } \ No newline at end of file diff --git a/resources/factions/usa_1965.json b/resources/factions/usa_1965.json index 9b832a3d..2acd421a 100644 --- a/resources/factions/usa_1965.json +++ b/resources/factions/usa_1965.json @@ -23,14 +23,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ - "VulcanGenerator", + "air_defenses": [ "ChaparralGenerator", - "EarlyColdWarFlakGenerator" - ], - "sams": [ + "EarlyColdWarFlakGenerator", "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/usa_1975.json b/resources/factions/usa_1975.json index c1d8906f..a72383d8 100644 --- a/resources/factions/usa_1975.json +++ b/resources/factions/usa_1975.json @@ -24,14 +24,11 @@ "Infantry_M4", "Soldier_M249" ], - "shorads": [ + "air_defenses": [ + "ChaparralGenerator", "EarlyColdWarFlakGenerator", - "VulcanGenerator", - "ChaparralGenerator" - ], - "sams": [ "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "ewrs": [ "HawkEwrGenerator" diff --git a/resources/factions/usa_1990.json b/resources/factions/usa_1990.json index c09f23cf..dfe64438 100644 --- a/resources/factions/usa_1990.json +++ b/resources/factions/usa_1990.json @@ -43,16 +43,17 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator" + "air_defenses": [ + "AvengerGenerator", + "ChaparralGenerator", + "HawkGenerator", + "PatriotGenerator" ], "ewrs": [ - "HawkEwrGenerator" + "PatriotEwrGenerator" ], "aircraft_carrier": [ "CVN_74_John_C__Stennis" @@ -72,7 +73,8 @@ "CVN-71 Theodore Roosevelt", "CVN-72 Abraham Lincoln", "CVN-73 George Washington", - "CVN-74 John C. Stennis" + "CVN-74 John C. Stennis", + "CVN-75 Harry S. Truman" ], "helicopter_carrier_names": [ "LHA-1 Tarawa", diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json index f10e36bb..5ebe5d1c 100644 --- a/resources/factions/usa_2005.json +++ b/resources/factions/usa_2005.json @@ -43,13 +43,12 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "AvengerGenerator" - ], - "sams": [ - "HawkGenerator", + "air_defenses": [ + "AvengerGenerator", + "LinebackerGenerator", "PatriotGenerator" ], "ewrs": [ @@ -72,7 +71,8 @@ "CVN-71 Theodore Roosevelt", "CVN-72 Abraham Lincoln", "CVN-73 George Washington", - "CVN-74 John C. Stennis" + "CVN-74 John C. Stennis", + "CVN-75 Harry S. Truman" ], "helicopter_carrier_names": [ "LHA-1 Tarawa", diff --git a/resources/factions/usa_2005_c130.json b/resources/factions/usa_2005_c130.json new file mode 100644 index 00000000..0ee4d192 --- /dev/null +++ b/resources/factions/usa_2005_c130.json @@ -0,0 +1,111 @@ +{ + "country": "USA", + "name": "USA 2005", + "authors": "Khopa", + "description": "USA in the 2000s.
", + "aircrafts": [ + "F_15C", + "F_15E", + "F_14B", + "FA_18C_hornet", + "F_16C_50", + "A_10C", + "A_10C_2", + "AV8BNA", + "UH_1H", + "AH_64D", + "B_52H", + "B_1B", + "F_117A", + "Hercules" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "ATGM_M1134_Stryker", + "APC_M1126_Stryker_ICV", + "IFV_M2A2_Bradley", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249", + "Stinger_MANPADS" + ], + "air_defenses": [ + "AvengerGenerator", + "LinebackerGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "requirements": {}, + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis", + "CVN-75 Harry S. Truman" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator", + "OliverHazardPerryGroupGenerator" + ], + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper", + "liveries_overrides": { + "FA_18C_hornet": [ + "VFA-37", + "VFA-106", + "VFA-113", + "VFA-122", + "VFA-131", + "VFA-192", + "VFA-34", + "VFA-83", + "VFA-87", + "VFA-97", + "VMFA-122", + "VMFA-132", + "VMFA-251", + "VMFA-312", + "VMFA-314", + "VMFA-323" + ] + } +} \ No newline at end of file diff --git a/resources/factions/usa_2005_modded.json b/resources/factions/usa_2005_modded.json new file mode 100644 index 00000000..e4839515 --- /dev/null +++ b/resources/factions/usa_2005_modded.json @@ -0,0 +1,113 @@ +{ + "country": "USA", + "name": "USA 2005 Modded", + "authors": "Khopa", + "description": "USA 2005 with the Raptor mod, with the F-22 mod by Grinelli Designs.
", + "aircrafts": [ + "F_15C", + "F_15E", + "F_14B", + "FA_18C_hornet", + "F_16C_50", + "A_10C", + "A_10C_2", + "AV8BNA", + "UH_1H", + "AH_64D", + "B_52H", + "B_1B", + "F_117A", + "F_22A" + ], + "awacs": [ + "E_3A" + ], + "tankers": [ + "KC_135", + "KC130" + ], + "frontline_units": [ + "MBT_M1A2_Abrams", + "ATGM_M1134_Stryker", + "APC_M1126_Stryker_ICV", + "IFV_M2A2_Bradley", + "IFV_LAV_25", + "APC_M1043_HMMWV_Armament", + "ATGM_M1045_HMMWV_TOW" + ], + "artillery_units": [ + "MLRS_M270", + "SPH_M109_Paladin" + ], + "logistics_units": [ + "Transport_M818" + ], + "infantry_units": [ + "Infantry_M4", + "Soldier_M249", + "Stinger_MANPADS" + ], + "air_defenses": [ + "AvengerGenerator", + "HawkGenerator", + "LinebackerGenerator", + "PatriotGenerator" + ], + "ewrs": [ + "PatriotEwrGenerator" + ], + "aircraft_carrier": [ + "CVN_74_John_C__Stennis" + ], + "helicopter_carrier": [ + "LHA_1_Tarawa" + ], + "destroyers": [ + "USS_Arleigh_Burke_IIa" + ], + "cruisers": [ + "Ticonderoga_class" + ], + "carrier_names": [ + "CVN-71 Theodore Roosevelt", + "CVN-72 Abraham Lincoln", + "CVN-73 George Washington", + "CVN-74 John C. Stennis" + ], + "helicopter_carrier_names": [ + "LHA-1 Tarawa", + "LHA-2 Saipan", + "LHA-3 Belleau Wood", + "LHA-4 Nassau", + "LHA-5 Peleliu" + ], + "navy_generators": [ + "ArleighBurkeGroupGenerator", + "OliverHazardPerryGroupGenerator" + ], + "requirements": { + "F-22A mod by Grinnelli Designs": "https://drive.google.com/file/d/1aLDbSvkgOnwv4C-xtdc_dfMXNROynl_c/view?usp=sharing" + }, + "has_jtac": true, + "jtac_unit": "MQ_9_Reaper", + "liveries_overrides": { + "FA_18C_hornet": [ + "VFA-37", + "VFA-106", + "VFA-113", + "VFA-122", + "VFA-131", + "VFA-192", + "VFA-34", + "VFA-83", + "VFA-87", + "VFA-97", + "VMFA-122", + "VMFA-132", + "VMFA-251", + "VMFA-312", + "VMFA-314", + "VMFA-323" + ] + } +} \ No newline at end of file diff --git a/resources/factions/usn_1985.json b/resources/factions/usn_1985.json index fabb207f..55bc56e5 100644 --- a/resources/factions/usn_1985.json +++ b/resources/factions/usn_1985.json @@ -31,15 +31,13 @@ ], "infantry_units": [ "Infantry_M4", - "Soldier_M249" + "Soldier_M249", + "Stinger_MANPADS" ], - "shorads": [ - "VulcanGenerator", - "ChaparralGenerator" - ], - "sams": [ + "air_defenses": [ + "ChaparralGenerator", "HawkGenerator", - "ChaparralGenerator" + "VulcanGenerator" ], "aircraft_carrier": [ "CVN_74_John_C__Stennis" @@ -57,7 +55,8 @@ "CVN-71 Theodore Roosevelt", "CVN-72 Abraham Lincoln", "CVN-73 George Washington", - "CVN-74 John C. Stennis" + "CVN-74 John C. Stennis", + "CVN-75 Harry S. Truman" ], "helicopter_carrier_names": [ "LHA-1 Tarawa", diff --git a/resources/frontlines/caucasus.json b/resources/frontlines/caucasus.json new file mode 100644 index 00000000..c2f7a80c --- /dev/null +++ b/resources/frontlines/caucasus.json @@ -0,0 +1 @@ +{"28|26": {"points": [[-83519.432788948, 834083.89603137], [-76119.776928765, 743854.15425227], [-69364.629004892, 719860.24185348], [-66223.861015868, 712472.15322055], [-55063.838612323, 701790.55845189], [-51581.840958501, 704161.97484394]], "start_cp": 28}, "32|27": {"points": [[-148553.72718928, 843792.03708555], [-114733.18523211, 848507.87882073], [-93593.960629467, 841634.68667249], [-83721.151431284, 834200.8738046]], "start_cp": 32}, "32|28": {"points": [[-148554.87082704, 843689.04452871], [-114733.18523211, 845562.62834108], [-110339.43457942, 812629.20979521], [-111707.66520572, 785946.43211532], [-119336.17607842, 776106.17954928], [-124839.73032662, 760747.06391035]], "start_cp": 32}, "27|26": {"points": [[-124831.33530722, 760646.27221205], [-100762.17095534, 753347.75031825], [-86615.899059596, 729586.43747856], [-70460.285574716, 694513.4327179], [-50443.346691892, 703139.51687735]], "start_cp": 27}, "26|16": {"points": [[-51128.273849964, 705442.60983569], [-17096.829383992, 583922.76901062], [-9413.0964883513, 489204.29454645], [-26751.202695677, 458085.37842522]], "start_cp": 26}, "16|17": {"points": [[-26290.077082534, 457532.52869133], [-12087.691792587, 444620.30900361], [-9183.8957732278, 411618.86444668], [-19095.485460604, 393640.78914313], [-12512.173613316, 386130.07819866], [-6723.0801746587, 294506.97247323], [8289.1802310393, 267690.0671157], [-1709.4806012567, 249581.13338705], [-20329.725545816, 256364.19845868], [-32338.671748957, 281488.17801525], [-44457.480224214, 298277.60074695], [-50348.762426692, 298378.38229135]], "start_cp": 16}, "17|18": {"points": [[-50428.449268252, 298385.98215495], [-47925.882995864, 299522.84586356], [-47742.307667018, 302423.33605932], [-51670.819704316, 306425.27822816], [-52890.978467907, 309683.60757879], [-52158.285335864, 315248.49431547], [-54476.310454094, 321830.02991902], [-61058.499192715, 325379.38150096], [-61379.264542506, 327677.41312797], [-59644.880081364, 329300.50019054], [-59830.375745657, 332806.36824568], [-63794.637882896, 337172.7521818], [-67470.778941327, 340636.53352785], [-70179.929699705, 341613.85524925], [-69660.171385319, 350494.94296899], [-69388.993134334, 354630.4112965], [-75106.334592587, 354856.39317232], [-76281.440346853, 358652.8886861], [-81388.630740391, 366494.45977706], [-87241.561324136, 371036.69548105], [-89840.352896069, 379895.1850132], [-95874.068980471, 384753.79534334], [-98992.618866791, 390742.31505257], [-105500.89689041, 395510.53263238], [-109975.33803166, 401747.63240502], [-112664.52235392, 406199.47535868], [-118042.89099844, 411623.04037836], [-119433.38420159, 414545.36512974], [-121777.77888263, 415031.75406772], [-125094.95143961, 417580.4321027], [-127809.00171351, 419876.18788994], [-131233.17983685, 423387.91602212], [-134219.60791602, 427162.2941808], [-142354.54855856, 436882.58763511], [-142151.02610079, 439008.26663852], [-144141.02346569, 440738.2075296], [-154337.7160827, 451791.06647059], [-156349.9178749, 457412.78487971], [-161121.11800072, 459238.28753654], [-164180.10148029, 461855.90812217]], "start_cp": 17}, "18|21": {"points": [[-164264.74921603, 462240.8713078], [-170274.83407271, 474599.20385019], [-172409.88129215, 477254.97986699], [-173104.00129217, 479130.75653371], [-174533.55795887, 482196.45320045], [-175268.99462556, 485708.3698672], [-176186.22462558, 487716.35986724], [-177285.24795894, 488666.6432006], [-178400.79795897, 489162.44320061], [-180541.00129235, 490542.41986731], [-183995.07462577, 493831.22653406], [-185517.42530201, 501467.08245066], [-185474.55938496, 507404.01196323], [-187746.45298904, 511540.57295936], [-190489.87168078, 513490.97218551], [-196791.16148834, 516405.85454547]], "start_cp": 18}, "21|20": {"points": [[-196714.42589455, 516411.96814195], [-197450.59049978, 523168.80433533], [-197732.15105425, 528120.95761683], [-197699.02628313, 533768.73109171], [-198311.83454874, 540807.74495336], [-198842.52672667, 546607.07868341], [-199402.89801799, 549442.89703645], [-200896.04977609, 551418.00206698], [-202121.5817902, 551281.83184319], [-202802.43290916, 551962.68296214], [-202604.3671291, 553881.44520647], [-204919.26093354, 558771.19415168], [-207135.12184795, 559043.53459927], [-209821.38899001, 559390.14971437], [-212321.96946326, 561717.42263007], [-214993.578381, 562726.87596614], [-220482.29198081, 564029.17214026]], "start_cp": 21}, "20|23": {"points": [[-220672.07816602, 564666.16009935], [-225466.53293841, 577226.74074072], [-225867.80020827, 582560.25153588], [-226971.28520037, 588278.31013131], [-229011.0604888, 592424.73858648], [-232087.44289102, 595718.47409321], [-233993.46242283, 598192.95559065], [-234745.0795704, 605985.79430519], [-238229.0345155, 610496.39618843], [-245013.37774879, 611843.03758538], [-252127.56713626, 613492.22847069], [-260588.37132113, 620015.44606012], [-270266.03318718, 624574.33114385], [-274285.71028992, 632588.96571051], [-278888.32106515, 640507.86809234], [-281561.45308744, 647019.85888349]], "start_cp": 20}, "23|24": {"points": [[-281730.7296916, 647253.59774005], [-290982.81972459, 649906.82826413], [-294663.23013322, 650003.68116962], [-298488.96273308, 642238.09370951], [-301465.17238261, 635293.60452728], [-305217.78454941, 628521.64923777], [-309876.19965302, 626882.57725687], [-316044.28631799, 627745.2467205], [-318472.62171129, 628904.60667428], [-317887.74401676, 635471.94764428]], "start_cp": 23}, "24|25": {"points": [[-317873.22860422, 635639.0650959], [-312899.88159494, 646184.05618911], [-306709.82545376, 651773.13600592], [-302953.72342634, 651502.69665994], [-299948.8418044, 651262.30613019], [-297935.57111771, 652704.64930872], [-296162.69096076, 656130.21435772], [-295856.9172118, 663129.98092517], [-295423.18658872, 668374.17845869], [-293700.92883347, 671849.09769727], [-290615.92415898, 672244.90961777], [-288066.42972986, 674165.76158491], [-286820.78633299, 677600.01207161], [-286893.79300875, 679835.68894319], [-286618.90964092, 682436.5085003], [-285244.4928018, 683705.20096718]], "start_cp": 24}, "25|23": {"points": [[-284669.68592925, 683920.85836338], [-284033.62440161, 671710.30738432], [-284533.77033624, 669857.91503381], [-285774.87321108, 669765.29541629], [-288423.79427231, 670358.06096845], [-288982.2616923, 669926.15330961], [-288459.09137937, 667542.82188402], [-285718.97087515, 657267.36992099], [-281630.5138127, 647419.52117057]], "start_cp": 25}, "25|31": {"points": [[-284263.99293597, 684700.54580073], [-272604.87474782, 700374.86510124], [-273331.93020798, 704467.91806214], [-277532.6950889, 709395.73840322], [-281410.32420975, 715562.24582457], [-282763.16813743, 718965.99542922], [-284038.34156377, 723212.87736217], [-285647.59520339, 726629.3333984], [-286742.41427361, 730713.85069882], [-290637.44365803, 734503.60901881], [-294658.79831979, 742062.07144589], [-297037.92437623, 747430.89573254], [-299143.34566511, 751157.49141386], [-299648.64677444, 755747.30982361], [-297543.22548556, 760400.29087204], [-293500.81661091, 766927.09686757], [-290805.87736114, 772043.27059955], [-294728.00259664, 779192.46248204], [-289131.68727782, 794537.19803365], [-284302.60873658, 803382.98676341], [-286153.0033178, 818998.51176593], [-283670.76668445, 834568.9051933], [-290756.42398328, 855871.00866495], [-293497.37680705, 872691.40288022], [-298728.59569045, 876959.26028386], [-299033.75480734, 883966.6044418], [-299055.28124949, 884675.04815429], [-299818.06111468, 885322.52382059], [-308456.45796809, 893001.69982944], [-311598.60162174, 897764.58963223], [-312216.79613588, 902058.30176963], [-312264.20281806, 902423.79800646], [-312738.11562308, 902452.63317662], [-318952.43355462, 903032.81497309]], "start_cp": 25}, "31|32": {"points": [[-319413.50770939, 903161.13927527], [-312671.27979139, 903060.83924503], [-312324.50415159, 903053.25482189], [-312203.42896954, 902599.67200635], [-311123.51486173, 897768.90194652], [-308519.80029013, 892811.99843776], [-299725.00782398, 885575.0970382], [-298905.33045141, 884872.77713154], [-298821.06157145, 884019.77775077], [-298298.35634543, 877006.39271804], [-291393.57547003, 873411.8434301], [-288926.34291968, 873766.98136965], [-273396.69812304, 873213.96162732], [-272951.29274537, 873210.63633871], [-272449.20605747, 873723.537192], [-262767.79330839, 883418.21980792], [-255300.09049795, 886097.47196129], [-240022.65268728, 889745.81531906], [-225543.28998612, 892596.08356732], [-218724.28531507, 888448.82017709], [-215656.78759916, 883416.20673693], [-214086.63445796, 873664.97864894], [-221406.94515152, 861563.14860602], [-218652.16733902, 855560.98269335], [-218619.81218872, 855140.36573947], [-218296.26068574, 855027.12271343], [-211841.78700129, 853553.62800841], [-206210.96305046, 853832.24537185], [-200950.59208156, 852508.09558384], [-194072.72704603, 852086.25319499], [-191404.28495595, 852694.64158494], [-188982.449823, 852694.64158494], [-186034.22200645, 852522.66532349], [-183530.30158366, 851166.88402139], [-180342.38338683, 851032.52731578], [-178180.46185106, 851948.59576314], [-175212.40008161, 852779.16448874], [-171877.91093321, 852962.37817822], [-167460.79156926, 854118.55241154], [-163919.58243455, 854146.2181079], [-159410.07392707, 852181.95366599], [-155370.88225779, 848557.74744219], [-151193.36210669, 845265.52957477], [-148897.1093084, 843799.24766743]], "start_cp": 31}} \ No newline at end of file diff --git a/resources/gulflandmap.p b/resources/gulflandmap.p index 77b367f1..3e2e566e 100644 Binary files a/resources/gulflandmap.p and b/resources/gulflandmap.p differ diff --git a/resources/mizdata/caucasus/caucusus_frontline.miz b/resources/mizdata/caucasus/caucusus_frontline.miz new file mode 100644 index 00000000..53ca0e0f Binary files /dev/null and b/resources/mizdata/caucasus/caucusus_frontline.miz differ diff --git a/resources/nevlandmap.p b/resources/nevlandmap.p index 1086ba36..1b92631a 100644 Binary files a/resources/nevlandmap.p and b/resources/nevlandmap.p differ diff --git a/resources/normandylandmap.p b/resources/normandylandmap.p index e179bbf8..808a2a1e 100644 Binary files a/resources/normandylandmap.p and b/resources/normandylandmap.p differ diff --git a/resources/plugins/base/dcs_liberation.lua b/resources/plugins/base/dcs_liberation.lua index 722440a6..765edc61 100644 --- a/resources/plugins/base/dcs_liberation.lua +++ b/resources/plugins/base/dcs_liberation.lua @@ -6,7 +6,6 @@ logger:info("Check that json.lua is loaded : json = "..tostring(json)) killed_aircrafts = {} killed_ground_units = {} -weapons_fired = {} base_capture_events = {} destroyed_objects_positions = {} mission_ended = false @@ -33,7 +32,6 @@ function write_state() local game_state = { ["killed_aircrafts"] = killed_aircrafts, ["killed_ground_units"] = killed_ground_units, - ["weapons_fired"] = weapons_fired, ["base_capture_events"] = base_capture_events, ["mission_ended"] = mission_ended, ["destroyed_objects_positions"] = destroyed_objects_positions, diff --git a/resources/plugins/base/mist_4_3_74.lua b/resources/plugins/base/mist_4_4_90.lua similarity index 89% rename from resources/plugins/base/mist_4_3_74.lua rename to resources/plugins/base/mist_4_4_90.lua index ffb822a4..fbf4f6ac 100644 --- a/resources/plugins/base/mist_4_3_74.lua +++ b/resources/plugins/base/mist_4_4_90.lua @@ -34,12 +34,19 @@ mist = {} -- don't change these mist.majorVersion = 4 -mist.minorVersion = 3 -mist.build = 74 +mist.minorVersion = 4 +mist.build = 90 -- forward declaration of log shorthand local log +local mistSettings = { + errorPopup = false, -- errors printed by mist logger will create popup warning you + warnPopup = false, + infoPopup = false, + logLevel = 'warn', +} + do -- the main scope local coroutines = {} @@ -84,11 +91,7 @@ do -- the main scope end end -- if we add more coalition specific data then bullsye should be categorized by coaliton. For now its just the bullseye table - mist.DBs.missionData.bullseye = {red = {}, blue = {}} - mist.DBs.missionData.bullseye.red.x = env.mission.coalition.red.bullseye.x --should it be point.x? - mist.DBs.missionData.bullseye.red.y = env.mission.coalition.red.bullseye.y - mist.DBs.missionData.bullseye.blue.x = env.mission.coalition.blue.bullseye.x - mist.DBs.missionData.bullseye.blue.y = env.mission.coalition.blue.bullseye.y + mist.DBs.missionData.bullseye = {} end mist.DBs.zonesByName = {} @@ -114,11 +117,19 @@ do -- the main scope mist.DBs.navPoints = {} mist.DBs.units = {} --Build mist.db.units and mist.DBs.navPoints - for coa_name, coa_data in pairs(env.mission.coalition) do - - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + for coa_name_miz, coa_data in pairs(env.mission.coalition) do + local coa_name = coa_name_miz + if string.lower(coa_name_miz) == 'neutrals' then + coa_name = 'neutral' + end + if type(coa_data) == 'table' then mist.DBs.units[coa_name] = {} - + + if coa_data.bullseye then + mist.DBs.missionData.bullseye[coa_name] = {} + mist.DBs.missionData.bullseye[coa_name].x = coa_data.bullseye.x + mist.DBs.missionData.bullseye[coa_name].y = coa_data.bullseye.y + end -- build nav points DB mist.DBs.navPoints[coa_name] = {} if coa_data.nav_points then --navpoints @@ -464,7 +475,7 @@ do -- the main scope mist.DBs.unitsByCat[unit_data.category] = mist.DBs.unitsByCat[unit_data.category] or {} -- future-proofing against new categories... table.insert(mist.DBs.unitsByCat[unit_data.category], mist.utils.deepCopy(unit_data)) - dbLog:info('inserting $1', unit_data.unitName) + --dbLog:info('inserting $1', unit_data.unitName) table.insert(mist.DBs.unitsByNum, mist.utils.deepCopy(unit_data)) if unit_data.skill and (unit_data.skill == "Client" or unit_data.skill == "Player") then @@ -501,13 +512,13 @@ do -- the main scope local original_key = key --only for duplicate runtime IDs. local key_ind = 1 while mist.DBs.deadObjects[key] do - dbLog:warn('duplicate runtime id of previously dead object key: $1', key) + --dbLog:warn('duplicate runtime id of previously dead object key: $1', key) key = tostring(original_key) .. ' #' .. tostring(key_ind) key_ind = key_ind + 1 end if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then - --dbLog:info('object found in alive_units') + ----dbLog:info('object found in alive_units') val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_]) local pos = Object.getPosition(val.object) if pos then @@ -516,7 +527,7 @@ do -- the main scope val.objectType = mist.DBs.aliveUnits[val.object.id_].category elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units - --dbLog:info('object found in old_alive_units') + ----dbLog:info('object found in old_alive_units') val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_]) local pos = Object.getPosition(val.object) if pos then @@ -525,13 +536,13 @@ do -- the main scope val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category else --attempt to determine if static object... - --dbLog:info('object not found in alive units or old alive units') + ----dbLog:info('object not found in alive units or old alive units') local pos = Object.getPosition(val.object) if pos then local static_found = false for ind, static in pairs(mist.DBs.unitsByCat.static) do if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero... - dbLog:info('correlated dead static object to position') + --dbLog:info('correlated dead static object to position') val.objectData = static val.objectPos = pos.p val.objectType = 'static' @@ -585,7 +596,7 @@ do -- the main scope if lunits[i].category ~= 'static' then -- can't get statics with Unit.getByName :( local unit = lUnit.getByName(lunits[i].unitName) if unit then - --dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy + ----dbLog:info("unit named $1 alive!", lunits[i].unitName) -- spammy local pos = unit:getPosition() local newtbl = ldeepcopy(lunits[i]) if pos then @@ -612,7 +623,7 @@ do -- the main scope end local function dbUpdate(event, objType) - dbLog:info('dbUpdate') + --dbLog:info('dbUpdate') local newTable = {} newTable.startTime = 0 if type(event) == 'string' then -- if name of an object. @@ -623,7 +634,7 @@ do -- the main scope newObject = StaticObject.getByName(event) -- log:info('its static') else - log:warn('$1 is not a Unit or Static Object. This should not be possible', event) + log:warn('$1 is not a Group or Static Object. This should not be possible. Sent category is: $2', event, objType) return false end @@ -725,11 +736,14 @@ do -- the main scope newTable.units[unitId].skill = "High" newTable.units[unitId].alt_type = "BARO" end + if newTable.units[unitId].alt_type == "RADIO" then -- raw postition MSL was grabbed for group, but spawn is AGL, so re-offset it + newTable.units[unitId].alt = (newTable.units[unitId].alt - land.getHeight({x = newTable.units[unitId].x, y = newTable.units[unitId].y})) + end end end else -- its a static - newTable.category = 'static' + newTable.category = 'static' newTable.units[1] = {} newTable.units[1].unitName = newObject:getName() newTable.units[1].category = 'static' @@ -767,8 +781,9 @@ do -- the main scope newTable.units[1].mass = data.mass newTable.units[1].canCargo = data.canCargo newTable.units[1].categoryStatic = data.categoryStatic - newTable.units[1].type = 'cargo1' + newTable.units[1].type = data.type mistAddedObjects[index] = nil + break end end end @@ -777,7 +792,7 @@ do -- the main scope newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time --mist.debug.dumpDBs() --end - dbLog:info('endDbUpdate') + --dbLog:info('endDbUpdate') return newTable end @@ -800,54 +815,68 @@ do -- the main scope updatesPerRun = 5 end]] - dbLog:info('iterate') - for name, gType in pairs(tempSpawnedGroups) do - dbLog:info(name) + --dbLog:info('iterate') + for name, gData in pairs(tempSpawnedGroups) do + --env.info(name) local updated = false - - if mist.DBs.groupsByName[name] then - -- first check group level properties, groupId, countryId, coalition - dbLog:info('Found in DBs, check if updated') - local dbTable = mist.DBs.groupsByName[name] - dbLog:info(dbTable) - if gType ~= 'static' then - dbLog:info('Not static') - local _g = Group.getByName(name) - local _u = _g:getUnit(1) - if dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId then - dbLog:info('Group Data mismatch') - updated = true - else - dbLog:info('No Mismatch') - end + local stillExists = false + if not gData.checked then + tempSpawnedGroups[name].checked = true -- so if there was an error it will get cleared. + local _g = gData.gp or Group.getByName(name) + if mist.DBs.groupsByName[name] then + -- first check group level properties, groupId, countryId, coalition + -- dbLog:info('Found in DBs, check if updated') + local dbTable = mist.DBs.groupsByName[name] + -- dbLog:info(dbTable) + if gData.type ~= 'static' then + -- dbLog:info('Not static') + + if _g and _g:isExist() == true then + stillExists = true + local _u = _g:getUnit(1) - end - end - dbLog:info('Updated: $1', updated) - if updated == false and gType ~= 'static' then -- time to check units - dbLog:info('No Group Mismatch, Check Units') - for index, uObject in pairs(Group.getByName(name):getUnits()) do - dbLog:info(index) - if mist.DBs.unitsByName[uObject:getName()] then - dbLog:info('UnitByName table exists') - local uTable = mist.DBs.unitsByName[uObject:getName()] - if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type then - dbLog:info('Unit Data mismatch') - updated = true - break - end - end - end - end - - if updated == true or not mist.DBs.groupsByName[name] then - dbLog:info('Get Table') - writeGroups[#writeGroups+1] = {data = dbUpdate(name, gType), isUpdated = updated} - - end - -- Work done, so remove - tempSpawnedGroups[name] = nil - tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1 + if _u and (dbTable.groupId ~= tonumber(_g:getID()) or _u:getCountry() ~= dbTable.countryId or _u:getCoalition() ~= dbTable.coaltionId) then + --dbLog:info('Group Data mismatch') + updated = true + else + -- dbLog:info('No Mismatch') + end + else + dbLog:warn('$1 : Group was not accessible', name) + end + end + end + --dbLog:info('Updated: $1', updated) + if updated == false and gData.type ~= 'static' then -- time to check units + --dbLog:info('No Group Mismatch, Check Units') + if _g and _g:isExist() == true then + stillExists = true + for index, uObject in pairs(_g:getUnits()) do + --dbLog:info(index) + if mist.DBs.unitsByName[uObject:getName()] then + --dbLog:info('UnitByName table exists') + local uTable = mist.DBs.unitsByName[uObject:getName()] + if tonumber(uObject:getID()) ~= uTable.unitId or uObject:getTypeName() ~= uTable.type then + --dbLog:info('Unit Data mismatch') + updated = true + break + end + end + end + end + else + stillExists = true + end + + if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then + --dbLog:info('Get Table') + writeGroups[#writeGroups+1] = {data = dbUpdate(name, gData.type), isUpdated = updated} + + end + -- Work done, so remove + end + tempSpawnedGroups[name] = nil + tempSpawnGroupsCounter = tempSpawnGroupsCounter - 1 end end end @@ -860,10 +889,10 @@ do -- the main scope savesPerRun = 5 end if i > 0 then - dbLog:info('updateDBTables') + --dbLog:info('updateDBTables') local ldeepCopy = mist.utils.deepCopy for x = 1, i do - dbLog:info(writeGroups[x]) + --dbLog:info(writeGroups[x]) local newTable = writeGroups[x].data local updated = writeGroups[x].isUpdated local mistCategory @@ -884,34 +913,34 @@ do -- the main scope mistCategory = 'ship' newTable.category = mistCategory end - dbLog:info('Update unitsBy') + --dbLog:info('Update unitsBy') for newId, newUnitData in pairs(newTable.units) do - dbLog:info(newId) + --dbLog:info(newId) newUnitData.category = mistCategory if newUnitData.unitId then - dbLog:info('byId') + --dbLog:info('byId') mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData) end - dbLog:info(updated) + --dbLog:info(updated) if mist.DBs.unitsByName[newUnitData.unitName] and updated == true then--if unit existed before and something was updated, write over the entry for a given unit name just in case. - dbLog:info('Updating Unit Tables') + --dbLog:info('Updating Unit Tables') for i = 1, #mist.DBs.unitsByCat[mistCategory] do if mist.DBs.unitsByCat[mistCategory][i].unitName == newUnitData.unitName then - dbLog:info('Entry Found, Rewriting for unitsByCat') + --dbLog:info('Entry Found, Rewriting for unitsByCat') mist.DBs.unitsByCat[mistCategory][i] = ldeepCopy(newUnitData) break end end for i = 1, #mist.DBs.unitsByNum do if mist.DBs.unitsByNum[i].unitName == newUnitData.unitName then - dbLog:info('Entry Found, Rewriting for unitsByNum') + --dbLog:info('Entry Found, Rewriting for unitsByNum') mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData) break end end else - dbLog:info('Unitname not in use, add as normal') + --dbLog:info('Unitname not in use, add as normal') mist.DBs.unitsByCat[mistCategory][#mist.DBs.unitsByCat[mistCategory] + 1] = ldeepCopy(newUnitData) mist.DBs.unitsByNum[#mist.DBs.unitsByNum + 1] = ldeepCopy(newUnitData) end @@ -920,7 +949,7 @@ do -- the main scope end -- this is a really annoying DB to populate. Gotta create new tables in case its missing - dbLog:info('write mist.DBs.units') + --dbLog:info('write mist.DBs.units') if not mist.DBs.units[newTable.coalition] then mist.DBs.units[newTable.coalition] = {} end @@ -934,10 +963,10 @@ do -- the main scope end if updated == true then - dbLog:info('Updating DBsUnits') + --dbLog:info('Updating DBsUnits') for i = 1, #mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] do if mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i].groupName == newTable.groupName then - dbLog:info('Entry Found, Rewriting') + --dbLog:info('Entry Found, Rewriting') mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable) break end @@ -962,30 +991,35 @@ do -- the main scope if timer.getTime() > lastUpdateTime then lastUpdateTime = timer.getTime() end - dbLog:info('endUpdateTables') + --dbLog:info('endUpdateTables') end end local function groupSpawned(event) -- dont need to add units spawned in at the start of the mission if mist is loaded in init line if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then - dbLog:info('unitSpawnEvent') + --dbLog:info('unitSpawnEvent') --table.insert(tempSpawnedUnits,(event.initiator)) ------- -- New functionality below. ------- if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight - dbLog:info('Object is a Unit') - dbLog:info(Unit.getGroup(event.initiator):getName()) - if not tempSpawnedGroups[Unit.getGroup(event.initiator):getName()] then - dbLog:info('added') - tempSpawnedGroups[Unit.getGroup(event.initiator):getName()] = 'group' - tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + --dbLog:info('Object is a Unit') + if Unit.getGroup(event.initiator) then + --dbLog:info(Unit.getGroup(event.initiator):getName()) + local g = Unit.getGroup(event.initiator) + if not tempSpawnedGroups[g:getName()] then + --dbLog:info('added') + tempSpawnedGroups[g:getName()] = {type = 'group', gp = g} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + else + log:error('Group not accessible by unit in event handler. This is a DCS bug') end elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then - dbLog:info('Object is Static') - tempSpawnedGroups[StaticObject.getName(event.initiator)] = 'static' + --dbLog:info('Object is Static') + tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'} tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 end @@ -1113,20 +1147,54 @@ do -- the main scope mist.addEventHandler(addClientsToActive) ]] + local function verifyDB() + --log:warn('verfy Run') + for coaName, coaId in pairs(coalition.side) do + --env.info(coaName) + local gps = coalition.getGroups(coaId) + for i = 1, #gps do + if gps[i] and Group.getSize(gps[i]) > 0 then + local gName = Group.getName(gps[i]) + if not mist.DBs.groupsByName[gName] then + --env.info(Unit.getID(gUnits[j]) .. ' Not found in DB yet') + if not tempSpawnedGroups[gName] then + --dbLog:info('added') + tempSpawnedGroups[gName] = {type = 'group', gp = gps[i]} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + end + end + end + local st = coalition.getStaticObjects(coaId) + for i = 1, #st do + local s = st[i] + if StaticObject.isExist(s) then + if not mist.DBs.unitsByName[s:getName()] then + --env.info(StaticObject.getID(s) .. ' Not found in DB yet') + tempSpawnedGroups[s:getName()] = {type = 'static'} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end + end + end + + end + + end --- init function. -- creates logger, adds default event handler -- and calls main the first time. -- @function mist.init function mist.init() + -- create logger - mist.log = mist.Logger:new("MIST") - dbLog = mist.Logger:new('MISTDB', 'warning') + mist.log = mist.Logger:new("MIST", mistSettings.logLevel) + dbLog = mist.Logger:new('MISTDB', 'warn') log = mist.log -- log shorthand -- set warning log level, showing only -- warnings and errors - log:setLevel("warning") + --log:setLevel("warning") log:info("initializing databases") initDBs() @@ -1134,10 +1202,14 @@ do -- the main scope -- add event handler for group spawns mist.addEventHandler(groupSpawned) mist.addEventHandler(addDeadObject) + + log:warn('Init time: $1', timer.getTime()) -- call main the first time therafter it reschedules itself. mist.main() --log:msg('MIST version $1.$2.$3 loaded', mist.majorVersion, mist.minorVersion, mist.build) + + mist.scheduleFunction(verifyDB, {}, timer.getTime() + 1) return end @@ -1148,7 +1220,7 @@ do -- the main scope timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01) --reschedule first in case of Lua error updateTenthSecond = updateTenthSecond + 1 - if updateTenthSecond == 10 then + if updateTenthSecond == 20 then updateTenthSecond = 0 checkSpawnedEventsNew() @@ -1187,20 +1259,20 @@ do -- the main scope -- @treturn number next unit id. function mist.getNextUnitId() mist.nextUnitId = mist.nextUnitId + 1 - if mist.nextUnitId > 6900 then - mist.nextUnitId = 14000 + if mist.nextUnitId > 6900 and mist.nextUnitId < 30000 then + mist.nextUnitId = 30000 end - return mist.nextUnitId + return mist.utils.deepCopy(mist.nextUnitId) end --- Returns next group id. -- @treturn number next group id. function mist.getNextGroupId() mist.nextGroupId = mist.nextGroupId + 1 - if mist.nextGroupId > 6900 then - mist.nextGroupId = 14000 + if mist.nextGroupId > 6900 and mist.nextGroupId < 30000 then + mist.nextGroupId = 30000 end - return mist.nextGroupId + return mist.utils.deepCopy(mist.nextGroupId) end --- Returns timestamp of last database update. @@ -1213,7 +1285,7 @@ do -- the main scope -- @todo write good docs -- @tparam table staticObj table containing data needed for the object creation function mist.dynAddStatic(newObj) - + log:info(newObj) if newObj.units and newObj.units[1] then -- if its mist format for entry, val in pairs(newObj.units[1]) do if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then @@ -1258,6 +1330,9 @@ do -- the main scope newObj.unitId = mistUnitId end + + newObj.name = newObj.name or newObj.unitName + if newObj.clone or not newObj.name then mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1 newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static ']) @@ -1291,7 +1366,7 @@ do -- the main scope mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj) if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then - --log:info('addStaticObject') + log:info(newObj) coalition.addStaticObject(country.id[newCountry], newObj) return newObj @@ -1418,24 +1493,6 @@ do -- the main scope newGroup.units[unitIndex].skill = 'Random' end - if not unitData.alt then - if newCat == 'AIRPLANE' then - newGroup.units[unitIndex].alt = 2000 - newGroup.units[unitIndex].alt_type = 'RADIO' - newGroup.units[unitIndex].speed = 150 - elseif newCat == 'HELICOPTER' then - newGroup.units[unitIndex].alt = 500 - newGroup.units[unitIndex].alt_type = 'RADIO' - newGroup.units[unitIndex].speed = 60 - else - --[[log:info('check height') - newGroup.units[unitIndex].alt = land.getHeight({x = newGroup.units[unitIndex].x, y = newGroup.units[unitIndex].y}) - newGroup.units[unitIndex].alt_type = 'BARO']] - end - - - end - if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then if newGroup.units[unitIndex].alt_type and newGroup.units[unitIndex].alt_type ~= 'BARO' or not newGroup.units[unitIndex].alt_type then newGroup.units[unitIndex].alt_type = 'RADIO' @@ -1450,22 +1507,47 @@ do -- the main scope if not unitData.payload then newGroup.units[unitIndex].payload = mist.getPayload(originalName) end + if not unitData.alt then + if newCat == 'AIRPLANE' then + newGroup.units[unitIndex].alt = 2000 + newGroup.units[unitIndex].alt_type = 'RADIO' + newGroup.units[unitIndex].speed = 150 + elseif newCat == 'HELICOPTER' then + newGroup.units[unitIndex].alt = 500 + newGroup.units[unitIndex].alt_type = 'RADIO' + newGroup.units[unitIndex].speed = 60 + end + end + + elseif newCat == 'GROUND_UNIT' then + if nil == unitData.playerCanDrive then + unitData.playerCanDrive = true + end + end mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newGroup.units[unitIndex]) end mistAddedGroups[#mistAddedGroups + 1] = mist.utils.deepCopy(newGroup) - if newGroup.route and not newGroup.route.points then - if not newGroup.route.points and newGroup.route[1] then - local copyRoute = newGroup.route + if newGroup.route then + if newGroup.route and not newGroup.route.points then + if newGroup.route[1] then + local copyRoute = mist.utils.deepCopy(newGroup.route) + newGroup.route = {} + newGroup.route.points = copyRoute + end + end + else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately + if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then newGroup.route = {} - newGroup.route.points = copyRoute + newGroup.route.points = {} + newGroup.route.points[1] = {} end end newGroup.country = newCountry --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua') - + --log:warn(newGroup) -- sanitize table newGroup.groupName = nil newGroup.clone = nil @@ -1516,10 +1598,12 @@ do -- the main scope while i <= #scheduledTasks do if scheduledTasks[i].id == id then table.remove(scheduledTasks, i) + return true else i = i + 1 end end + return false end --- Registers an event handler. @@ -1729,7 +1813,7 @@ do end for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do @@ -2132,7 +2216,7 @@ do -- @treturn table @{UnitNameTable} function mist.makeUnitTable(tbl) --Assumption: will be passed a table of strings, sequential - log:info(tbl) + --log:info(tbl) local units_by_name = {} local l_munits = mist.DBs.units --local reference for faster execution @@ -2511,13 +2595,13 @@ function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) local units = {} for i = 1, #unit_names do - units[#units + 1] = Unit.getByName(unitNames[i]) + units[#units + 1] = Unit.getByName(unit_names[i]) end local inZoneUnits = {} for i =1, #units do if units[i]:isActive() and mist.pointInPolygon(units[i]:getPosition().p, polyZone, max_alt) then - inZoneUnits[inZoneUnits + 1] = units[i] + inZoneUnits[#inZoneUnits + 1] = units[i] end end @@ -2565,7 +2649,7 @@ function mist.getUnitsInZones(unit_names, zone_names, zone_type) end end local unit_pos = units[units_ind]:getPosition().p - if unit_pos and units[units_ind]:isActive() == true then + if unit_pos and units[units_ind]:isActive() == true then if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then in_zone_units[#in_zone_units + 1] = units[units_ind] break @@ -2725,8 +2809,8 @@ function mist.getAvgGroupPos(groupName) groupName = Group.getByName(groupName) end local units = {} - for i = 1, #groupName:getSize() do - table.insert(units, groupName.getUnit(i):getName()) + for i = 1, groupName:getSize() do + table.insert(units, groupName:getUnit(i):getName()) end return mist.getAvgPos(units) @@ -3111,7 +3195,7 @@ do -- group functions scope if gpId then for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do @@ -3141,10 +3225,15 @@ do -- group functions scope return end + + function mist.getValidRandomPoint(vars) + + + end function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call - local point = vars.point - + --log:info(vars) + local point = vars.point local gpName if vars.gpName then gpName = vars.gpName @@ -3157,12 +3246,7 @@ do -- group functions scope local action = vars.action local disperse = vars.disperse or false - local maxDisp = vars.maxDisp - if not vars.maxDisp then - maxDisp = 200 - else - maxDisp = vars.maxDisp - end + local maxDisp = vars.maxDisp or 200 local radius = vars.radius or 0 local innerRadius = vars.innerRadius @@ -3191,19 +3275,32 @@ do -- group functions scope --log:info('get Randomized Point') local diff = {x = 0, y = 0} - local newCoord, origCoord - if point then + local newCoord, origCoord + + local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} + if string.lower(newGroupData.category) == 'ship' then + validTerrain = {'SHALLOW_WATER' , 'WATER'} + elseif string.lower(newGroupData.category) == 'vehicle' then + validTerrain = {'LAND', 'ROAD'} + end + local offsets = {} + if point and radius >= 0 then local valid = false + -- new thoughts + --[[ Get AVG position of group and max radius distance to that avg point, otherwise use disperse data to get zone area to check + if disperse then + + else + + end + -- ]] + + + - local validTerrain - if string.lower(newGroupData.category) == 'ship' then - validTerrain = {'SHALLOW_WATER' , 'WATER'} - elseif string.lower(newGroupData.category) == 'vehicle' then - validTerrain = {'LAND', 'ROAD'} - else - validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} - end + + ---- old for i = 1, 100 do newCoord = mist.getRandPointInCircle(point, radius, innerRadius) if mist.isTerrainValid(newCoord, validTerrain) then @@ -3224,27 +3321,43 @@ do -- group functions scope if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category end - + --log:info(point) for unitNum, unitData in pairs(newGroupData.units) do - if disperse then - if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then - newCoord = mist.getRandPointInCircle(origCoord, maxDisp) + --log:info(unitNum) + if disperse then + local unitCoord + if maxDisp and type(maxDisp) == 'number' and unitNum ~= 1 then + for i = 1, 100 do + unitCoord = mist.getRandPointInCircle(origCoord, maxDisp) + if mist.isTerrainValid(unitCoord, validTerrain) == true then + --log:warn('Index: $1, Itered: $2. AT: $3', unitNum, i, unitCoord) + break + end + end + --else --newCoord = mist.getRandPointInCircle(zone.point, zone.radius) end - - newGroupData.units[unitNum].x = newCoord.x - newGroupData.units[unitNum].y = newCoord.y + if unitNum == 1 then + unitCoord = mist.utils.deepCopy(newCoord) + end + if unitCoord then + newGroupData.units[unitNum].x = unitCoord.x + newGroupData.units[unitNum].y = unitCoord.y + end else newGroupData.units[unitNum].x = unitData.x + diff.x newGroupData.units[unitNum].y = unitData.y + diff.y end if point then if (newGroupData.category == 'plane' or newGroupData.category == 'helicopter') then - if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then + if point.z and point.y > 0 and point.y > land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + 10 then newGroupData.units[unitNum].alt = point.y + --log:info('far enough from ground') else + if newGroupData.category == 'plane' then + --log:info('setNewAlt') newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(300, 9000) else newGroupData.units[unitNum].alt = land.getHeight({newGroupData.units[unitNum].x, newGroupData.units[unitNum].y}) + math.random(200, 3000) @@ -3271,6 +3384,7 @@ do -- group functions scope if route then newGroupData.route = route end + --log:info(newGroupData) --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') if string.lower(newGroupData.category) == 'static' then --log:info(newGroupData) @@ -3586,7 +3700,7 @@ do -- group functions scope s1 = string.lower(s1) s2 = string.lower(s2) end - log:info('Comparing: $1 and $2', s1, s2) + --log:info('Comparing: $1 and $2', s1, s2) if s1 == s2 then return true else @@ -3697,7 +3811,173 @@ do -- mist.util scope function mist.utils.kmphToMps(kmph) return kmph/3.6 end + + function mist.utils.kelvinToCelsius(t) + return t - 273.15 + end + + function mist.utils.FahrenheitToCelsius(f) + return (f - 32) * (5/9) + end + + function mist.utils.celsiusToFahrenheit(c) + return c*(9/5)+32 + end + + function mist.utils.converter(t1, t2, val) + if type(t1) == 'string' then + t1 = string.lower(t1) + end + if type(t2) == 'string' then + t2 = string.lower(t2) + end + if val and type(val) ~= 'number' then + if tonumber(val) then + val = tonumber(val) + else + log:warn("Value given is not a number: $1", val) + return 0 + end + end + + -- speed + if t1 == 'mps' then + if t2 == 'kmph' then + return val * 3.6 + elseif t2 == 'knots' or t2 == 'knot' then + return val * 3600/1852 + end + elseif t1 == 'kmph' then + if t2 == 'mps' then + return val/3.6 + elseif t2 == 'knots' or t2 == 'knot' then + return val*0.539957 + end + elseif t1 == 'knot' or t1 == 'knots' then + if t2 == 'kmph' then + return val * 1.852 + elseif t2 == 'mps' then + return val * 0.514444 + end + + -- Distance + elseif t1 == 'feet' or t1 == 'ft' then + if t2 == 'nm' then + return val/6076.12 + elseif t2 == 'km' then + return (val*0.3048)/1000 + elseif t2 == 'm' then + return val*0.3048 + end + elseif t1 == 'nm' then + if t2 == 'feet' or t2 == 'ft' then + return val*6076.12 + elseif t2 == 'km' then + return val*1.852 + elseif t2 == 'm' then + return val*1852 + end + elseif t1 == 'km' then + if t2 == 'nm' then + return val/1.852 + elseif t2 == 'feet' or t2 == 'ft' then + return (val/0.3048)*1000 + elseif t2 == 'm' then + return val*1000 + end + elseif t1 == 'm' then + if t2 == 'nm' then + return val/1852 + elseif t2 == 'km' then + return val/1000 + elseif t2 == 'feet' or t2 == 'ft' then + return val/0.3048 + end + + -- Temperature + elseif t1 == 'f' or t1 == 'fahrenheit' then + if t2 == 'c' or t2 == 'celsius' then + return (val - 32) * (5/9) + elseif t2 == 'k' or t2 == 'kelvin' then + return (val + 459.67) * (5/9) + end + elseif t1 == 'c' or t1 == 'celsius' then + if t2 == 'f' or t2 == 'fahrenheit' then + return val*(9/5)+32 + elseif t2 == 'k' or t2 == 'kelvin' then + return val + 273.15 + end + elseif t1 == 'k' or t1 == 'kelvin' then + if t2 == 'c' or t2 == 'celsius' then + return val - 273.15 + elseif t2 == 'f' or t2 == 'fahrenheit' then + return ((val*(9/5))-459.67) + end + + -- Pressure + elseif t1 == 'p' or t1 == 'pascal' or t1 == 'pascals' then + if t2 == 'hpa' or t2 == 'hectopascal' then + return val/100 + elseif t2 == 'mmhg' then + return val * 0.00750061561303 + elseif t2 == 'inhg' then + return val * 0.0002953 + end + elseif t1 == 'hpa' or t1 == 'hectopascal' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val*100 + elseif t2 == 'mmhg' then + return val * 0.00750061561303 + elseif t2 == 'inhg' then + return val * 0.02953 + end + elseif t1 == 'mmhg' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val / 0.00750061561303 + elseif t2 == 'hpa' or t2 == 'hectopascal' then + return val * 1.33322 + elseif t2 == 'inhg' then + return val/25.4 + end + elseif t1 == 'inhg' then + if t2 == 'p' or t2 == 'pascal' or t2 == 'pascals' then + return val*3386.39 + elseif t2 == 'mmhg' then + return val*25.4 + elseif t2 == 'hpa' or t2 == 'hectopascal' then + return val * 33.8639 + end + else + log:warn("First value doesn't match with list. Value given: $1", t1) + end + log:warn("Match not found. Unable to convert: $1 into $2", t1, t2) + + end + + mist.converter = mist.utils.converter + + function mist.utils.getQFE(point, inchHg) + + local t, p = 0, 0 + if atmosphere.getTemperatureAndPressure then + t, p = atmosphere.getTemperatureAndPressure(mist.utils.makeVec3GL(point)) + end + if p == 0 then + local h = land.getHeight(mist.utils.makeVec2(point))/0.3048 -- convert to feet + if inchHg then + return (env.mission.weather.qnh - (h/30)) * 0.0295299830714 + else + return env.mission.weather.qnh - (h/30) + end + else + if inchHg then + return mist.converter('p', 'inhg', p) + else + return mist.converter('p', 'hpa', p) + end + end + end --- Converts a Vec3 to a Vec2. -- @tparam Vec3 vec the 3D vector -- @return vector converted to Vec2 @@ -4016,9 +4296,12 @@ function mist.utils.serialize(name, value, level) local function serializeToTbl(name, value, level) local var_str_tbl = {} - if level == nil then level = "" end - if level ~= "" then level = level.." " end - + if level == nil then + level = "" + end + if level ~= "" then + level = level.."" + end table.insert(var_str_tbl, level .. name .. " = ") if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then @@ -4033,7 +4316,6 @@ function mist.utils.serialize(name, value, level) else key = string.format("[%q]", k) end - table.insert(var_str_tbl, mist.utils.serialize(key, v, level.." ")) end @@ -4132,12 +4414,14 @@ function mist.utils.oneLineSerialize(tbl) tbl_str[#tbl_str + 1] = mist.utils.oneLineSerialize(val) tbl_str[#tbl_str + 1] = ', ' --I think this is right, I just added it else - log:war('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind)) + log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind)) end end tbl_str[#tbl_str + 1] = '}' return table.concat(tbl_str) + else + return mist.utils.basicSerialize(tbl) end end @@ -4187,7 +4471,7 @@ function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on seria else tableshow_tbls[val] = loc .. '[' .. mist.utils.basicSerialize(ind) .. ']' tbl_str[#tbl_str + 1] = tostring(val) .. ' ' - tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. ' ', tableshow_tbls) + tbl_str[#tbl_str + 1] = mist.utils.tableShow(val, loc .. '[' .. mist.utils.basicSerialize(ind).. ']', indent .. ' ', tableshow_tbls) tbl_str[#tbl_str + 1] = ',\n' end elseif type(val) == 'function' then @@ -5004,12 +5288,11 @@ do -- mist.msg scope if type(value) == 'table' then for roleName, roleVal in pairs(value) do for rIndex, rVal in pairs(roleVal) do - if rIndex == 'red' or rIndex == 'blue' then - if env.mission.groundControl[index][roleName][rIndex] > 0 then - caSlots = true - break - end - end + if env.mission.groundControl[index][roleName][rIndex] > 0 then + caSlots = true + break + end + end end elseif type(value) == 'boolean' and value == true then @@ -5451,11 +5734,8 @@ vars.displayTime vars.msgFor - scope ]] function mist.msgBullseye(vars) - if string.lower(vars.ref) == 'red' then - vars.ref = mist.DBs.missionData.bullseye.red - mist.msgBR(vars) - elseif string.lower(vars.ref) == 'blue' then - vars.ref = mist.DBs.missionData.bullseye.blue + if mist.DBs.missionData.bullseye[string.lower(vars.ref)] then + vars.ref = mist.DBs.missionData.bullseye[string.lower(vars.ref)] mist.msgBR(vars) end end @@ -5727,7 +6007,215 @@ do -- mist.demos scope end end +do + --[[ stuff for marker panels + marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. + -- generates Id if not specified or if multiple marks created. + -- makes marks for countries by creating a mark for each client group in the country + -- can create multiple marks if needed for groups and countries. + -- adds marks to table for parsing and removing + -- Uses similar structure as messages. Big differences is it doesn't only mark to groups. + If to All, then mark is for All + if to coa mark is to coa + if to specific units, mark is to group + + + -------- + STUFF TO Check + -------- + If mark added to a group before a client joins slot is synced. + Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see? + + May need to automate process... + ]] + --[[ + local typeBase = { + ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, + ['MiG-21Bis'] = {'Mig-21'}, + ['MiG-15bis'] = {'Mig-15'}, + ['FW-190D9'] = {'FW-190'}, + ['Bf-109K-4'] = {'Bf-109'}, + } + + + local mId = 1337 + + mist.marker = {} + mist.marker.list = {} + local function markSpamFilter(recList, spamBlockOn) + + for id, name in pairs(recList) do + if name == spamBlockOn then + log:info('already on recList') + return recList + end + end + log:info('add to recList') + table.insert(recList, spamBlockOn) + return recList + end + + local function iterate() + mId = mId + 1 + return mId + end + + function mist.marker.add(pos, text, markFor, id) + log:warn('markerFunc') + log:info('Pos: $1, Text: $2, markFor: $3, id: $4', pos, text, markFor, id) + if not id then + + else + + end + local markType = 'all' + local markForTable = {} + if pos then + pos = mist.utils.makeVec3(pos) + end + if text and type(text) ~= string then + text = tostring(text) + else + text = '' + end + + if markFor then + if type(markFor) == 'number' then -- groupId + if mist.DBs.groupsById[markFor] then + markType = 'group' + end + elseif type(markFor) == 'string' then -- groupName + if mist.DBs.groupsByName[markFor] then + markType = 'group' + markFor = mist.DBs.groupsByName[markFor].groupId + end + elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all + markType = 'table' + log:info(markFor) + for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all. + log:info(forIndex) + log:info(forData) + for list, listData in pairs(forData) do + log:info(listData) + forIndex = string.lower(forIndex) + if type(listData) == 'string' then + listData = string.lower(listData) + end + if listData == 'all' then + markType = 'all' + break + elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. + for name, index in pairs (coalition.side) do + if listData == string.lower(name) then + markType = 'coalition' + end + end + elseif (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then + markForTable = markSpamFilter(markForTable, clientData.groupId) + elseif forIndex == 'unittypes' then -- mark to group + -- iterate play units + for clientId, clientData in pairs(mist.DBs.humansById) do + for typeId, typeData in pairs(listData) do + log:info(typeData) + local found = false + if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then + if mist.matchString(typeData, clientData.type) then + found = true + else + -- check other known names for aircraft + end + end + if found == true then + markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info to other function to see if client is already recieving the current message. + end + for clientDataEntry, clientDataVal in pairs(clientData) do + if type(clientDataVal) == 'string' then + + if mist.matchString(list, clientDataVal) == true or list == 'all' then + local sString = typeData + for rName, pTbl in pairs(typeBase) do -- just a quick check to see if the user may have meant something and got the specific type of the unit wrong + for pIndex, pName in pairs(pTbl) do + if mist.stringMatch(sString, pName) then + sString = rName + end + end + end + if mist.stringMatch(sString, clientData.type) then + found = true + markForTable = markSpamFilter(markForTable, clientData.groupId) -- sends info oto other function to see if client is already recieving the current message. + --table.insert(newMsgFor, clientId) + end + end + end + if found == true then -- shouldn't this be elsewhere too? + break + end + end + end + + end + end + end + end + end + else + markType = 'all' + end + + + + + + + if markType ~= 'table' then + local newId = iterate() + local data = {markId = newId, text = text, pos = pos, markType = markType, markFor = markFor} + + -- create marks + if markType == 'coa' then + trigger.action.markToCoalition(newId, text, pos, markFor) + elseif markType == 'group' then + trigger.action.markToGroup(newId, text, pos, markFor) + else + trigger.action.markToAll(iterate(), text, pos) + end + table.insert(mist.marker.list, data) -- add to the DB + else + if #markForTable > 0 then + log:info('iterate') + for i = 1, #markForTable do + local newId = iterate() + local data = {markId = newId, text = text, pos = pos, markFor = markFor} + log:info(data) + table.insert(mist.marker.list, data) + trigger.action.markToGroup(newId, text, pos, markForTable[i]) + end + end + end + + + + end + + function mist.marker.remove(id) + for i, data in pairs(mist.marker.list) do + if id == data.markId then + trigger.action.removeMark(id) + end + end + end + + function mist.marker.get(id) + + end + + function mist.marker.coords(pos, cType, markFor, id) -- wrapper function to just display coordinates of a specific format at location + + + end + ]] +end --- Time conversion functions. -- @section mist.time do -- mist.time scope @@ -5738,7 +6226,7 @@ do -- mist.time scope -- if number or table the time is converted into mil tim function mist.time.convertToSec(timeTable) - timeInSec = 0 + local timeInSec = 0 if timeTable and type(timeTable) == 'number' then timeInSec = timeTable elseif timeTable and type(timeTable) == 'table' and (timeTable.d or timeTable.h or timeTable.m or timeTable.s) then @@ -5965,6 +6453,7 @@ do -- group tasks scope if group then local groupCon = group:getController() if groupCon then + log:warn(misTask) groupCon:setTask(misTask) return true end @@ -5983,7 +6472,7 @@ do -- group tasks scope end for coa_name, coa_data in pairs(env.mission.coalition) do - if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then + if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do for obj_type_name, obj_type_data in pairs(cntry_data) do @@ -5996,6 +6485,11 @@ do -- group tasks scope for point_num, point in pairs(group_data.route.points) do local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey(point.name) + else + routeData.name = point.name + end if not point.point then routeData.x = point.x routeData.y = point.y @@ -6034,7 +6528,7 @@ do -- group tasks scope -- function mist.ground.buildPath() end -- ???? function mist.ground.patrolRoute(vars) - log:info('patrol') + --log:info('patrol') local tempRoute = {} local useRoute = {} local gpData = vars.gpData @@ -6328,24 +6822,28 @@ do -- group tasks scope end -- need to return a Vec3 or Vec2? - function mist.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() + function mist.getRandPointInCircle(p, radius, innerRadius, maxA, minA) + local point = mist.utils.makeVec3(p) + local theta = 2*math.pi*math.random() + local minR = innerRadius or 0 + if maxA and not minA then + theta = math.rad(math.random(0, maxA - math.random())) + elseif maxA and minA and minA < maxA then + theta = math.rad(math.random(minA, maxA) - math.random()) + end local rad = math.random() + math.random() if rad > 1 then rad = 2 - rad end local radMult - if innerRadius and innerRadius <= radius then - radMult = (radius - innerRadius)*rad + innerRadius + if minR and minR <= radius then + --radMult = (radius - innerRadius)*rad + innerRadius + radMult = radius * math.sqrt((minR^2 + (radius^2 - minR^2) * math.random()) / radius^2) else radMult = radius*rad end - if not point.z then --might as well work with vec2/3 - point.z = point.y - end - local rndCoord if radius > 0 then rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z} @@ -6355,12 +6853,39 @@ do -- group tasks scope return rndCoord end - function mist.getRandomPointInZone(zoneName, innerRadius) + function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA) if type(zoneName) == 'string' and type(trigger.misc.getZone(zoneName)) == 'table' then - return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius) + return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius, maxA, minA) end return false end + + function mist.getRandomPointInPoly(zone) + local avg = mist.getAvgPoint(zone) + local radius = 0 + local minR = math.huge + local newCoord = {} + for i = 1, #zone do + if mist.utils.get2DDist(avg, zone[i]) > radius then + radius = mist.utils.get2DDist(avg, zone[i]) + end + if mist.utils.get2DDist(avg, zone[i]) < minR then + minR = mist.utils.get2DDist(avg, zone[i]) + end + end + local lSpawnPos = {} + for j = 1, 100 do + newCoord = mist.getRandPointInCircle(avg, radius) + if mist.pointInPolygon(newCoord, zone) then + break + end + if j == 100 then + newCoord = mist.getRandPointInCircle(avg, 50000) + log:warn("Failed to find point in poly; Giving random point from center of the poly") + end + end + return newCoord + end function mist.groupToRandomPoint(vars) local group = vars.group --Required @@ -6394,20 +6919,20 @@ do -- group tasks scope local offset = {} local posStart = mist.getLeadPos(group) - - offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) - offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) - path[#path + 1] = mist.ground.buildWP(posStart, form, speed) + if posStart then + offset.x = mist.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3) + offset.z = mist.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3) + path[#path + 1] = mist.ground.buildWP(posStart, form, speed) - if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then - path[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed) - path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed) - path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed) - else - path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed) + if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then + path[#path + 1] = mist.ground.buildWP({x = posStart.x + 11, z = posStart.z + 11}, 'off_road', speed) + path[#path + 1] = mist.ground.buildWP(posStart, 'on_road', speed) + path[#path + 1] = mist.ground.buildWP(offset, 'on_road', speed) + else + path[#path + 1] = mist.ground.buildWP({x = posStart.x + 25, z = posStart.z + 25}, form, speed) + end end - path[#path + 1] = mist.ground.buildWP(offset, form, speed) path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed) @@ -6469,7 +6994,7 @@ do -- group tasks scope elseif type(terrainTypes) == 'table' then -- if its a table it does this check for typeId, typeData in pairs(terrainTypes) do for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then + if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeData) then table.insert(typeConverted, constId) end end @@ -6477,7 +7002,8 @@ do -- group tasks scope end for validIndex, validData in pairs(typeConverted) do if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true + log:info('Surface is : $1', validData) + return true end end return false @@ -6538,11 +7064,11 @@ do -- group tasks scope if type(group) == 'string' then -- group name group = Group.getByName(group) end - + local units = group:getUnits() local leader = units[1] - if not Unit.isExist(leader) then -- SHOULD be good, but if there is a bug, this code future-proofs it then. + if Unit.getLife(leader) == 0 or not Unit.isExist(leader) then -- SHOULD be good, but if there is a bug, this code future-proofs it then. local lowestInd = math.huge for ind, unit in pairs(units) do if Unit.isExist(unit) and ind < lowestInd then @@ -6587,7 +7113,6 @@ end -- @tfield[opt] boolean toggle switch the flag to false if required -- conditions are not met. Default: false. -- @tfield[opt] table unitTableDef - --- Logger class. -- @type mist.Logger do -- mist.Logger scope @@ -6659,18 +7184,17 @@ do -- mist.Logger scope -- @usage myLogger = mist.Logger:new("MyScript", "info") -- @treturn mist.Logger function mist.Logger:new(tag, level) - local l = {} - l.tag = tag + local l = {tag = tag} setmetatable(l, self) self.__index = self - self:setLevel(level) + l:setLevel(level) return l end --- Sets the level of verbosity for this logger. -- @tparam[opt] number|string level the log level defines which messages -- will be logged and which will be omitted. Log level 3 beeing the most verbose - -- and 0 disabling all output. This can also be a string. Allowed strings are: + -- and 0 disabling all output. This can also[ be a string. Allowed strings are: -- "none" (0), "error" (1), "warning" (2) and "info" (3). -- @usage myLogger:setLevel("info") -- @usage -- log everything @@ -6760,7 +7284,7 @@ do -- mist.Logger scope end end else - env.error(self.tag .. '|' .. text) + env.error(self.tag .. '|' .. text, mistSettings.errorPopup) end end end @@ -6784,7 +7308,7 @@ do -- mist.Logger scope end end else - env.warning(self.tag .. '|' .. text) + env.warning(self.tag .. '|' .. text, mistSettings.warnPopup) end end end @@ -6808,13 +7332,14 @@ do -- mist.Logger scope end end else - env.info(self.tag .. '|' .. text) + env.info(self.tag .. '|' .. text, mistSettings.infoPopup) end end end end + -- initialize mist mist.init() env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.')) diff --git a/resources/plugins/base/plugin.json b/resources/plugins/base/plugin.json index 97863e88..8d0c50b4 100644 --- a/resources/plugins/base/plugin.json +++ b/resources/plugins/base/plugin.json @@ -5,7 +5,7 @@ "specificOptions": [], "scriptsWorkOrders": [ { - "file": "mist_4_3_74.lua", + "file": "mist_4_4_90.lua", "mnemonic": "mist" }, { diff --git a/resources/plugins/jtacautolase/mist_4_3_74.lua b/resources/plugins/jtacautolase/mist_4_3_74.lua deleted file mode 100644 index ffb822a4..00000000 --- a/resources/plugins/jtacautolase/mist_4_3_74.lua +++ /dev/null @@ -1,6822 +0,0 @@ ---[[-- -MIST Mission Scripting Tools. -## Description: -MIssion Scripting Tools (MIST) is a collection of Lua functions -and databases that is intended to be a supplement to the standard -Lua functions included in the simulator scripting engine. - -MIST functions and databases provide ready-made solutions to many common -scripting tasks and challenges, enabling easier scripting and saving -mission scripters time. The table mist.flagFuncs contains a set of -Lua functions (that are similar to Slmod functions) that do not -require detailed Lua knowledge to use. - -However, the majority of MIST does require knowledge of the Lua language, -and, if you are going to utilize these components of MIST, it is necessary -that you read the Simulator Scripting Engine guide on the official ED wiki. - -## Links: - -ED Forum Thread: