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: - -##Github: - -Development - -Official Releases - -@script MIST -@author Speed -@author Grimes -@author lukrop -]] -mist = {} - --- don't change these -mist.majorVersion = 4 -mist.minorVersion = 3 -mist.build = 74 - --- forward declaration of log shorthand -local log - -do -- the main scope - local coroutines = {} - - local tempSpawnedUnits = {} -- birth events added here - local tempSpawnedGroups = {} - local tempSpawnGroupsCounter = 0 - - local mistAddedObjects = {} -- mist.dynAdd unit data added here - local mistAddedGroups = {} -- mist.dynAdd groupdata added here - local writeGroups = {} - local lastUpdateTime = 0 - - local updateAliveUnitsCounter = 0 - local updateTenthSecond = 0 - - local mistGpId = 7000 - local mistUnitId = 7000 - local mistDynAddIndex = {[' air '] = 0, [' hel '] = 0, [' gnd '] = 0, [' bld '] = 0, [' static '] = 0, [' shp '] = 0} - - local scheduledTasks = {} - local taskId = 0 - local idNum = 0 - - mist.nextGroupId = 1 - mist.nextUnitId = 1 - - local dbLog - - local function initDBs() -- mist.DBs scope - mist.DBs = {} - - mist.DBs.missionData = {} - if env.mission then - - mist.DBs.missionData.startTime = env.mission.start_time - mist.DBs.missionData.theatre = env.mission.theatre - mist.DBs.missionData.version = env.mission.version - mist.DBs.missionData.files = {} - if type(env.mission.resourceCounter) == 'table' then - for fIndex, fData in pairs (env.mission.resourceCounter) do - mist.DBs.missionData.files[#mist.DBs.missionData.files + 1] = mist.utils.deepCopy(fIndex) - 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 - end - - mist.DBs.zonesByName = {} - mist.DBs.zonesByNum = {} - - - if env.mission.triggers and env.mission.triggers.zones then - for zone_ind, zone_data in pairs(env.mission.triggers.zones) do - if type(zone_data) == 'table' then - local zone = mist.utils.deepCopy(zone_data) - zone.point = {} -- point is used by SSE - zone.point.x = zone_data.x - zone.point.y = 0 - zone.point.z = zone_data.y - - mist.DBs.zonesByName[zone_data.name] = zone - mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in - zones_by_num se are different objects.. don't want them linked.]] - end - end - end - - 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 - mist.DBs.units[coa_name] = {} - - -- build nav points DB - mist.DBs.navPoints[coa_name] = {} - if coa_data.nav_points then --navpoints - --mist.debug.writeData (mist.utils.serialize,{'NavPoints',coa_data.nav_points}, 'NavPoints.txt') - for nav_ind, nav_data in pairs(coa_data.nav_points) do - - if type(nav_data) == 'table' then - mist.DBs.navPoints[coa_name][nav_ind] = mist.utils.deepCopy(nav_data) - - mist.DBs.navPoints[coa_name][nav_ind].name = nav_data.callsignStr -- name is a little bit more self-explanatory. - mist.DBs.navPoints[coa_name][nav_ind].point = {} -- point is used by SSE, support it. - mist.DBs.navPoints[coa_name][nav_ind].point.x = nav_data.x - mist.DBs.navPoints[coa_name][nav_ind].point.y = 0 - mist.DBs.navPoints[coa_name][nav_ind].point.z = nav_data.y - end - end - end - if coa_data.country then --there is a country table - for cntry_id, cntry_data in pairs(coa_data.country) do - - local countryName = string.lower(cntry_data.name) - mist.DBs.units[coa_name][countryName] = {} - mist.DBs.units[coa_name][countryName].countryId = cntry_data.id - - if type(cntry_data) == 'table' then --just making sure - - for obj_type_name, obj_type_data in pairs(cntry_data) do - - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check - - local category = obj_type_name - - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - - mist.DBs.units[coa_name][countryName][category] = {} - - for group_num, group_data in pairs(obj_type_data.group) do - - if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group - - mist.DBs.units[coa_name][countryName][category][group_num] = {} - local groupName = group_data.name - if env.mission.version > 7 then - groupName = env.getValueDictByKey(groupName) - end - mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName - mist.DBs.units[coa_name][countryName][category][group_num].groupId = group_data.groupId - mist.DBs.units[coa_name][countryName][category][group_num].category = category - mist.DBs.units[coa_name][countryName][category][group_num].coalition = coa_name - mist.DBs.units[coa_name][countryName][category][group_num].country = countryName - mist.DBs.units[coa_name][countryName][category][group_num].countryId = cntry_data.id - mist.DBs.units[coa_name][countryName][category][group_num].startTime = group_data.start_time - mist.DBs.units[coa_name][countryName][category][group_num].task = group_data.task - mist.DBs.units[coa_name][countryName][category][group_num].hidden = group_data.hidden - - mist.DBs.units[coa_name][countryName][category][group_num].units = {} - - mist.DBs.units[coa_name][countryName][category][group_num].radioSet = group_data.radioSet - mist.DBs.units[coa_name][countryName][category][group_num].uncontrolled = group_data.uncontrolled - mist.DBs.units[coa_name][countryName][category][group_num].frequency = group_data.frequency - mist.DBs.units[coa_name][countryName][category][group_num].modulation = group_data.modulation - - for unit_num, unit_data in pairs(group_data.units) do - local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group - - units_tbl[unit_num] = {} - if env.mission.version > 7 then - units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name) - else - units_tbl[unit_num].unitName = unit_data.name - end - units_tbl[unit_num].type = unit_data.type - units_tbl[unit_num].skill = unit_data.skill --will be nil for statics - units_tbl[unit_num].unitId = unit_data.unitId - units_tbl[unit_num].category = category - units_tbl[unit_num].coalition = coa_name - units_tbl[unit_num].country = countryName - units_tbl[unit_num].countryId = cntry_data.id - units_tbl[unit_num].heading = unit_data.heading - units_tbl[unit_num].playerCanDrive = unit_data.playerCanDrive - units_tbl[unit_num].alt = unit_data.alt - units_tbl[unit_num].alt_type = unit_data.alt_type - units_tbl[unit_num].speed = unit_data.speed - units_tbl[unit_num].livery_id = unit_data.livery_id - if unit_data.point then --ME currently does not work like this, but it might one day - units_tbl[unit_num].point = unit_data.point - else - units_tbl[unit_num].point = {} - units_tbl[unit_num].point.x = unit_data.x - units_tbl[unit_num].point.y = unit_data.y - end - units_tbl[unit_num].x = unit_data.x - units_tbl[unit_num].y = unit_data.y - - units_tbl[unit_num].callsign = unit_data.callsign - units_tbl[unit_num].onboard_num = unit_data.onboard_num - units_tbl[unit_num].hardpoint_racks = unit_data.hardpoint_racks - units_tbl[unit_num].psi = unit_data.psi - - - units_tbl[unit_num].groupName = groupName - units_tbl[unit_num].groupId = group_data.groupId - - if unit_data.AddPropAircraft then - units_tbl[unit_num].AddPropAircraft = unit_data.AddPropAircraft - end - - if category == 'static' then - units_tbl[unit_num].categoryStatic = unit_data.category - units_tbl[unit_num].shape_name = unit_data.shape_name - if unit_data.mass then - units_tbl[unit_num].mass = unit_data.mass - end - - if unit_data.canCargo then - units_tbl[unit_num].canCargo = unit_data.canCargo - end - end - - end --for unit_num, unit_data in pairs(group_data.units) do - end --if group_data and group_data.units then - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --if type(cntry_data) == 'table' then - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - - mist.DBs.unitsByName = {} - mist.DBs.unitsById = {} - mist.DBs.unitsByCat = {} - - mist.DBs.unitsByCat.helicopter = {} -- adding default categories - mist.DBs.unitsByCat.plane = {} - mist.DBs.unitsByCat.ship = {} - mist.DBs.unitsByCat.static = {} - mist.DBs.unitsByCat.vehicle = {} - - mist.DBs.unitsByNum = {} - - mist.DBs.groupsByName = {} - mist.DBs.groupsById = {} - mist.DBs.humansByName = {} - mist.DBs.humansById = {} - - mist.DBs.dynGroupsAdded = {} -- will be filled by mist.dbUpdate from dynamically spawned groups - mist.DBs.activeHumans = {} - - mist.DBs.aliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main. - - mist.DBs.removedAliveUnits = {} -- will be filled in by the "updateAliveUnits" coroutine in mist.main. - - mist.DBs.const = {} - - -- not accessible by SSE, must use static list :-/ - mist.DBs.const.callsigns = { - ['NATO'] = { - ['rules'] = { - ['groupLimit'] = 9, - }, - ['AWACS'] = { - ['Overlord'] = 1, - ['Magic'] = 2, - ['Wizard'] = 3, - ['Focus'] = 4, - ['Darkstar'] = 5, - }, - ['TANKER'] = { - ['Texaco'] = 1, - ['Arco'] = 2, - ['Shell'] = 3, - }, - ['JTAC'] = { - ['Axeman'] = 1, - ['Darknight'] = 2, - ['Warrior'] = 3, - ['Pointer'] = 4, - ['Eyeball'] = 5, - ['Moonbeam'] = 6, - ['Whiplash'] = 7, - ['Finger'] = 8, - ['Pinpoint'] = 9, - ['Ferret'] = 10, - ['Shaba'] = 11, - ['Playboy'] = 12, - ['Hammer'] = 13, - ['Jaguar'] = 14, - ['Deathstar'] = 15, - ['Anvil'] = 16, - ['Firefly'] = 17, - ['Mantis'] = 18, - ['Badger'] = 19, - }, - ['aircraft'] = { - ['Enfield'] = 1, - ['Springfield'] = 2, - ['Uzi'] = 3, - ['Colt'] = 4, - ['Dodge'] = 5, - ['Ford'] = 6, - ['Chevy'] = 7, - ['Pontiac'] = 8, - }, - - ['unique'] = { - ['A10'] = { - ['Hawg'] = 9, - ['Boar'] = 10, - ['Pig'] = 11, - ['Tusk'] = 12, - ['rules'] = { - ['canUseAircraft'] = true, - ['appliesTo'] = { - 'A-10C', - 'A-10A', - }, - }, - }, - }, - }, - } - mist.DBs.const.shapeNames = { - ["Landmine"] = "landmine", - ["FARP CP Blindage"] = "kp_ug", - ["Subsidiary structure C"] = "saray-c", - ["Barracks 2"] = "kazarma2", - ["Small house 2C"] = "dom2c", - ["Military staff"] = "aviashtab", - ["Tech hangar A"] = "ceh_ang_a", - ["Oil derrick"] = "neftevyshka", - ["Tech combine"] = "kombinat", - ["Garage B"] = "garage_b", - ["Airshow_Crowd"] = "Crowd1", - ["Hangar A"] = "angar_a", - ["Repair workshop"] = "tech", - ["Subsidiary structure D"] = "saray-d", - ["FARP Ammo Dump Coating"] = "SetkaKP", - ["Small house 1C area"] = "dom2c-all", - ["Tank 2"] = "airbase_tbilisi_tank_01", - ["Boiler-house A"] = "kotelnaya_a", - ["Workshop A"] = "tec_a", - ["Small werehouse 1"] = "s1", - ["Garage small B"] = "garagh-small-b", - ["Small werehouse 4"] = "s4", - ["Shop"] = "magazin", - ["Subsidiary structure B"] = "saray-b", - ["FARP Fuel Depot"] = "GSM Rus", - ["Coach cargo"] = "wagon-gruz", - ["Electric power box"] = "tr_budka", - ["Tank 3"] = "airbase_tbilisi_tank_02", - ["Red_Flag"] = "H-flag_R", - ["Container red 3"] = "konteiner_red3", - ["Garage A"] = "garage_a", - ["Hangar B"] = "angar_b", - ["Black_Tyre"] = "H-tyre_B", - ["Cafe"] = "stolovaya", - ["Restaurant 1"] = "restoran1", - ["Subsidiary structure A"] = "saray-a", - ["Container white"] = "konteiner_white", - ["Warehouse"] = "sklad", - ["Tank"] = "bak", - ["Railway crossing B"] = "pereezd_small", - ["Subsidiary structure F"] = "saray-f", - ["Farm A"] = "ferma_a", - ["Small werehouse 3"] = "s3", - ["Water tower A"] = "wodokachka_a", - ["Railway station"] = "r_vok_sd", - ["Coach a tank blue"] = "wagon-cisterna_blue", - ["Supermarket A"] = "uniwersam_a", - ["Coach a platform"] = "wagon-platforma", - ["Garage small A"] = "garagh-small-a", - ["TV tower"] = "tele_bash", - ["Comms tower M"] = "tele_bash_m", - ["Small house 1A"] = "domik1a", - ["Farm B"] = "ferma_b", - ["GeneratorF"] = "GeneratorF", - ["Cargo1"] = "ab-212_cargo", - ["Container red 2"] = "konteiner_red2", - ["Subsidiary structure E"] = "saray-e", - ["Coach a passenger"] = "wagon-pass", - ["Black_Tyre_WF"] = "H-tyre_B_WF", - ["Electric locomotive"] = "elektrowoz", - ["Shelter"] = "ukrytie", - ["Coach a tank yellow"] = "wagon-cisterna_yellow", - ["Railway crossing A"] = "pereezd_big", - [".Ammunition depot"] = "SkladC", - ["Small werehouse 2"] = "s2", - ["Windsock"] = "H-Windsock_RW", - ["Shelter B"] = "ukrytie_b", - ["Fuel tank"] = "toplivo-bak", - ["Locomotive"] = "teplowoz", - [".Command Center"] = "ComCenter", - ["Pump station"] = "nasos", - ["Black_Tyre_RF"] = "H-tyre_B_RF", - ["Coach cargo open"] = "wagon-gruz-otkr", - ["Subsidiary structure 3"] = "hozdomik3", - ["FARP Tent"] = "PalatkaB", - ["White_Tyre"] = "H-tyre_W", - ["Subsidiary structure G"] = "saray-g", - ["Container red 1"] = "konteiner_red1", - ["Small house 1B area"] = "domik1b-all", - ["Subsidiary structure 1"] = "hozdomik1", - ["Container brown"] = "konteiner_brown", - ["Small house 1B"] = "domik1b", - ["Subsidiary structure 2"] = "hozdomik2", - ["Chemical tank A"] = "him_bak_a", - ["WC"] = "WC", - ["Small house 1A area"] = "domik1a-all", - ["White_Flag"] = "H-Flag_W", - ["Airshow_Cone"] = "Comp_cone", - } - - - -- create mist.DBs.oldAliveUnits - -- do - -- local intermediate_alive_units = {} -- between 0 and 0.5 secs old - -- local function make_old_alive_units() -- called every 0.5 secs, makes the old_alive_units DB which is just a copy of alive_units that is 0.5 to 1 sec old - -- if intermediate_alive_units then - -- mist.DBs.oldAliveUnits = mist.utils.deepCopy(intermediate_alive_units) - -- end - -- intermediate_alive_units = mist.utils.deepCopy(mist.DBs.aliveUnits) - -- timer.scheduleFunction(make_old_alive_units, nil, timer.getTime() + 0.5) - -- end - - -- make_old_alive_units() - -- end - - --Build DBs - for coa_name, coa_data in pairs(mist.DBs.units) do - for cntry_name, cntry_data in pairs(coa_data) do - for category_name, category_data in pairs(cntry_data) do - if type(category_data) == 'table' then - for group_ind, group_data in pairs(category_data) do - if type(group_data) == 'table' and group_data.units and type(group_data.units) == 'table' and #group_data.units > 0 then -- OCD paradigm programming - mist.DBs.groupsByName[group_data.groupName] = mist.utils.deepCopy(group_data) - mist.DBs.groupsById[group_data.groupId] = mist.utils.deepCopy(group_data) - for unit_ind, unit_data in pairs(group_data.units) do - mist.DBs.unitsByName[unit_data.unitName] = mist.utils.deepCopy(unit_data) - mist.DBs.unitsById[unit_data.unitId] = mist.utils.deepCopy(unit_data) - - 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) - 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 - mist.DBs.humansByName[unit_data.unitName] = mist.utils.deepCopy(unit_data) - mist.DBs.humansById[unit_data.unitId] = mist.utils.deepCopy(unit_data) - --if Unit.getByName(unit_data.unitName) then - -- mist.DBs.activeHumans[unit_data.unitName] = mist.utils.deepCopy(unit_data) - -- mist.DBs.activeHumans[unit_data.unitName].playerName = Unit.getByName(unit_data.unitName):getPlayerName() - --end - end - end - end - end - end - end - end - end - - --DynDBs - mist.DBs.MEunits = mist.utils.deepCopy(mist.DBs.units) - mist.DBs.MEunitsByName = mist.utils.deepCopy(mist.DBs.unitsByName) - mist.DBs.MEunitsById = mist.utils.deepCopy(mist.DBs.unitsById) - mist.DBs.MEunitsByCat = mist.utils.deepCopy(mist.DBs.unitsByCat) - mist.DBs.MEunitsByNum = mist.utils.deepCopy(mist.DBs.unitsByNum) - mist.DBs.MEgroupsByName = mist.utils.deepCopy(mist.DBs.groupsByName) - mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById) - - mist.DBs.deadObjects = {} - - do - local mt = {} - - function mt.__newindex(t, key, val) - 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) - 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') - val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_]) - local pos = Object.getPosition(val.object) - if pos then - val.objectPos = pos.p - end - 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') - val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_]) - local pos = Object.getPosition(val.object) - if pos then - val.objectPos = pos.p - end - 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') - 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') - val.objectData = static - val.objectPos = pos.p - val.objectType = 'static' - static_found = true - break - end - end - if not static_found then - val.objectPos = pos.p - val.objectType = 'building' - end - else - val.objectType = 'unknown' - end - end - rawset(t, key, val) - end - - setmetatable(mist.DBs.deadObjects, mt) - end - - do -- mist unitID funcs - for id, idData in pairs(mist.DBs.unitsById) do - if idData.unitId > mist.nextUnitId then - mist.nextUnitId = mist.utils.deepCopy(idData.unitId) - end - if idData.groupId > mist.nextGroupId then - mist.nextGroupId = mist.utils.deepCopy(idData.groupId) - end - end - end - - - end - - local function updateAliveUnits() -- coroutine function - local lalive_units = mist.DBs.aliveUnits -- local references for faster execution - local lunits = mist.DBs.unitsByNum - local ldeepcopy = mist.utils.deepCopy - local lUnit = Unit - local lremovedAliveUnits = mist.DBs.removedAliveUnits - local updatedUnits = {} - - if #lunits > 0 then - local units_per_run = math.ceil(#lunits/20) - if units_per_run < 5 then - units_per_run = 5 - end - - for i = 1, #lunits do - 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 - local pos = unit:getPosition() - local newtbl = ldeepcopy(lunits[i]) - if pos then - newtbl.pos = pos.p - end - newtbl.unit = unit - --newtbl.rt_id = unit.id_ - lalive_units[unit.id_] = newtbl - updatedUnits[unit.id_] = true - end - end - if i%units_per_run == 0 then - coroutine.yield() - end - end - -- All units updated, remove any "alive" units that were not updated- they are dead! - for unit_id, unit in pairs(lalive_units) do - if not updatedUnits[unit_id] then - lremovedAliveUnits[unit_id] = unit - lalive_units[unit_id] = nil - end - end - end - end - - local function dbUpdate(event, objType) - dbLog:info('dbUpdate') - local newTable = {} - newTable.startTime = 0 - if type(event) == 'string' then -- if name of an object. - local newObject - if Group.getByName(event) then - newObject = Group.getByName(event) - elseif StaticObject.getByName(event) then - 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) - return false - end - - newTable.name = newObject:getName() - newTable.groupId = tonumber(newObject:getID()) - newTable.groupName = newObject:getName() - local unitOneRef - if objType == 'static' then - unitOneRef = newObject - newTable.countryId = tonumber(newObject:getCountry()) - newTable.coalitionId = tonumber(newObject:getCoalition()) - newTable.category = 'static' - else - unitOneRef = newObject:getUnits() - newTable.countryId = tonumber(unitOneRef[1]:getCountry()) - newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) - newTable.category = tonumber(newObject:getCategory()) - end - for countryData, countryId in pairs(country.id) do - if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then - newTable.countryId = countryId - newTable.country = string.lower(countryData) - for coaData, coaId in pairs(coalition.side) do - if coaId == coalition.getCountryCoalition(countryId) then - newTable.coalition = string.lower(coaData) - end - end - end - end - for catData, catId in pairs(Unit.Category) do - if objType == 'group' and Group.getByName(newTable.groupName):isExist() then - if catId == Group.getByName(newTable.groupName):getCategory() then - newTable.category = string.lower(catData) - end - elseif objType == 'static' and StaticObject.getByName(newTable.groupName):isExist() then - if catId == StaticObject.getByName(newTable.groupName):getCategory() then - newTable.category = string.lower(catData) - end - - end - end - local gfound = false - for index, data in pairs(mistAddedGroups) do - if mist.stringMatch(data.name, newTable.groupName) == true then - gfound = true - newTable.task = data.task - newTable.modulation = data.modulation - newTable.uncontrolled = data.uncontrolled - newTable.radioSet = data.radioSet - newTable.hidden = data.hidden - newTable.startTime = data.start_time - mistAddedGroups[index] = nil - end - end - - if gfound == false then - newTable.uncontrolled = false - newTable.hidden = false - end - - newTable.units = {} - if objType == 'group' then - for unitId, unitData in pairs(unitOneRef) do - newTable.units[unitId] = {} - newTable.units[unitId].unitName = unitData:getName() - - newTable.units[unitId].x = mist.utils.round(unitData:getPosition().p.x) - newTable.units[unitId].y = mist.utils.round(unitData:getPosition().p.z) - newTable.units[unitId].point = {} - newTable.units[unitId].point.x = newTable.units[unitId].x - newTable.units[unitId].point.y = newTable.units[unitId].y - newTable.units[unitId].alt = mist.utils.round(unitData:getPosition().p.y) - newTable.units[unitId].speed = mist.vec.mag(unitData:getVelocity()) - - newTable.units[unitId].heading = mist.getHeading(unitData, true) - - newTable.units[unitId].type = unitData:getTypeName() - newTable.units[unitId].unitId = tonumber(unitData:getID()) - - - newTable.units[unitId].groupName = newTable.groupName - newTable.units[unitId].groupId = newTable.groupId - newTable.units[unitId].countryId = newTable.countryId - newTable.units[unitId].coalitionId = newTable.coalitionId - newTable.units[unitId].coalition = newTable.coalition - newTable.units[unitId].country = newTable.country - local found = false - for index, data in pairs(mistAddedObjects) do - if mist.stringMatch(data.name, newTable.units[unitId].unitName) == true then - found = true - newTable.units[unitId].livery_id = data.livery_id - newTable.units[unitId].skill = data.skill - newTable.units[unitId].alt_type = data.alt_type - newTable.units[unitId].callsign = data.callsign - newTable.units[unitId].psi = data.psi - mistAddedObjects[index] = nil - end - if found == false then - newTable.units[unitId].skill = "High" - newTable.units[unitId].alt_type = "BARO" - end - end - - end - else -- its a static - newTable.category = 'static' - newTable.units[1] = {} - newTable.units[1].unitName = newObject:getName() - newTable.units[1].category = 'static' - newTable.units[1].x = mist.utils.round(newObject:getPosition().p.x) - newTable.units[1].y = mist.utils.round(newObject:getPosition().p.z) - newTable.units[1].point = {} - newTable.units[1].point.x = newTable.units[1].x - newTable.units[1].point.y = newTable.units[1].y - newTable.units[1].alt = mist.utils.round(newObject:getPosition().p.y) - newTable.units[1].heading = mist.getHeading(newObject, true) - newTable.units[1].type = newObject:getTypeName() - newTable.units[1].unitId = tonumber(newObject:getID()) - newTable.units[1].groupName = newTable.name - newTable.units[1].groupId = newTable.groupId - newTable.units[1].countryId = newTable.countryId - newTable.units[1].country = newTable.country - newTable.units[1].coalitionId = newTable.coalitionId - newTable.units[1].coalition = newTable.coalition - if newObject:getCategory() == 6 and newObject:getCargoDisplayName() then - local mass = newObject:getCargoDisplayName() - mass = string.gsub(mass, ' ', '') - mass = string.gsub(mass, 'kg', '') - newTable.units[1].mass = tonumber(mass) - newTable.units[1].categoryStatic = 'Cargos' - newTable.units[1].canCargo = true - newTable.units[1].shape_name = 'ab-212_cargo' - end - - ----- search mist added objects for extra data if applicable - for index, data in pairs(mistAddedObjects) do - if mist.stringMatch(data.name, newTable.units[1].unitName) == true then - newTable.units[1].shape_name = data.shape_name -- for statics - newTable.units[1].livery_id = data.livery_id - newTable.units[1].airdromeId = data.airdromeId - newTable.units[1].mass = data.mass - newTable.units[1].canCargo = data.canCargo - newTable.units[1].categoryStatic = data.categoryStatic - newTable.units[1].type = 'cargo1' - mistAddedObjects[index] = nil - end - end - end - end - --mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua') - newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time - --mist.debug.dumpDBs() - --end - dbLog:info('endDbUpdate') - return newTable - end - - --[[DB update code... FRACK. I need to refactor some of it. - - The problem is that the DBs need to account better for shared object names. Needs to write over some data and outright remove other. - - If groupName is used then entire group needs to be rewritten - what to do with old groups units DB entries?. Names cant be assumed to be the same. - - - -- new spawn event check. - -- event handler filters everything into groups: tempSpawnedGroups - -- this function then checks DBs to see if data has changed - ]] - local function checkSpawnedEventsNew() - if tempSpawnGroupsCounter > 0 then - --[[local updatesPerRun = math.ceil(#tempSpawnedGroupsCounter/20) - if updatesPerRun < 5 then - updatesPerRun = 5 - end]] - - dbLog:info('iterate') - for name, gType in pairs(tempSpawnedGroups) do - dbLog: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 - - 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 - end - end - end - - local function updateDBTables() - local i = #writeGroups - - local savesPerRun = math.ceil(i/10) - if savesPerRun < 5 then - savesPerRun = 5 - end - if i > 0 then - dbLog:info('updateDBTables') - local ldeepCopy = mist.utils.deepCopy - for x = 1, i do - dbLog:info(writeGroups[x]) - local newTable = writeGroups[x].data - local updated = writeGroups[x].isUpdated - local mistCategory - if type(newTable.category) == 'string' then - mistCategory = string.lower(newTable.category) - end - - if string.upper(newTable.category) == 'GROUND_UNIT' then - mistCategory = 'vehicle' - newTable.category = mistCategory - elseif string.upper(newTable.category) == 'AIRPLANE' then - mistCategory = 'plane' - newTable.category = mistCategory - elseif string.upper(newTable.category) == 'HELICOPTER' then - mistCategory = 'helicopter' - newTable.category = mistCategory - elseif string.upper(newTable.category) == 'SHIP' then - mistCategory = 'ship' - newTable.category = mistCategory - end - dbLog:info('Update unitsBy') - for newId, newUnitData in pairs(newTable.units) do - dbLog:info(newId) - newUnitData.category = mistCategory - if newUnitData.unitId then - dbLog:info('byId') - mist.DBs.unitsById[tonumber(newUnitData.unitId)] = ldeepCopy(newUnitData) - end - 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') - 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') - 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') - mist.DBs.unitsByNum[i] = ldeepCopy(newUnitData) - break - end - end - - else - 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 - mist.DBs.unitsByName[newUnitData.unitName] = ldeepCopy(newUnitData) - - - end - -- this is a really annoying DB to populate. Gotta create new tables in case its missing - dbLog:info('write mist.DBs.units') - if not mist.DBs.units[newTable.coalition] then - mist.DBs.units[newTable.coalition] = {} - end - - if not mist.DBs.units[newTable.coalition][newTable.country] then - mist.DBs.units[newTable.coalition][(newTable.country)] = {} - mist.DBs.units[newTable.coalition][(newTable.country)].countryId = newTable.countryId - end - if not mist.DBs.units[newTable.coalition][newTable.country][mistCategory] then - mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] = {} - end - - if updated == true then - 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') - mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][i] = ldeepCopy(newTable) - break - end - end - else - mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory][#mist.DBs.units[newTable.coalition][(newTable.country)][mistCategory] + 1] = ldeepCopy(newTable) - end - - - if newTable.groupId then - mist.DBs.groupsById[newTable.groupId] = ldeepCopy(newTable) - end - - mist.DBs.groupsByName[newTable.name] = ldeepCopy(newTable) - mist.DBs.dynGroupsAdded[#mist.DBs.dynGroupsAdded + 1] = ldeepCopy(newTable) - - writeGroups[x] = nil - if x%savesPerRun == 0 then - coroutine.yield() - end - end - if timer.getTime() > lastUpdateTime then - lastUpdateTime = timer.getTime() - end - 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') - - --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 - 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' - tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 - end - - - end - end - - local function doScheduledFunctions() - local i = 1 - while i <= #scheduledTasks do - if not scheduledTasks[i].rep then -- not a repeated process - if scheduledTasks[i].t <= timer.getTime() then - local task = scheduledTasks[i] -- local reference - table.remove(scheduledTasks, i) - local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars))) - if not err then - log:error('Error in scheduled function: $1', errmsg) - end - --task.f(unpack(task.vars, 1, table.maxn(task.vars))) -- do the task, do not increment i - else - i = i + 1 - end - else - if scheduledTasks[i].st and scheduledTasks[i].st <= timer.getTime() then --if a stoptime was specified, and the stop time exceeded - table.remove(scheduledTasks, i) -- stop time exceeded, do not execute, do not increment i - elseif scheduledTasks[i].t <= timer.getTime() then - local task = scheduledTasks[i] -- local reference - task.t = timer.getTime() + task.rep --schedule next run - local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars))) - if not err then - log:error('Error in scheduled function: $1' .. errmsg) - end - --scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task - i = i + 1 - else - i = i + 1 - end - end - end - end - - -- Event handler to start creating the dead_objects table - local function addDeadObject(event) - if event.id == world.event.S_EVENT_DEAD or event.id == world.event.S_EVENT_CRASH then - if event.initiator and event.initiator.id_ and event.initiator.id_ > 0 then - - local id = event.initiator.id_ -- initial ID, could change if there is a duplicate id_ already dead. - local val = {object = event.initiator} -- the new entry in mist.DBs.deadObjects. - - local original_id = id --only for duplicate runtime IDs. - local id_ind = 1 - while mist.DBs.deadObjects[id] do - --log:info('duplicate runtime id of previously dead object id: $1', id) - id = tostring(original_id) .. ' #' .. tostring(id_ind) - id_ind = id_ind + 1 - end - - if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then - --log: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 - val.objectPos = pos.p - end - val.objectType = mist.DBs.aliveUnits[val.object.id_].category - --[[if mist.DBs.activeHumans[Unit.getName(val.object)] then - --trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20) - mist.DBs.activeHumans[Unit.getName(val.object)] = nil - end]] - elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units - --log: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 - val.objectPos = pos.p - end - val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category - - else --attempt to determine if static object... - --log: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... - --log:info('correlated dead static object to position') - val.objectData = static - val.objectPos = pos.p - val.objectType = 'static' - static_found = true - break - end - end - if not static_found then - val.objectPos = pos.p - val.objectType = 'building' - end - else - val.objectType = 'unknown' - end - end - mist.DBs.deadObjects[id] = val - end - end - end - - --[[ - local function addClientsToActive(event) - if event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_BIRTH then - log:info(event) - if Unit.getPlayerName(event.initiator) then - log:info(Unit.getPlayerName(event.initiator)) - local newU = mist.utils.deepCopy(mist.DBs.unitsByName[Unit.getName(event.initiator)]) - newU.playerName = Unit.getPlayerName(event.initiator) - mist.DBs.activeHumans[Unit.getName(event.initiator)] = newU - --trigger.action.outText('added: ' .. Unit.getName(event.initiator), 20) - end - elseif event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and event.initiator then - if mist.DBs.activeHumans[Unit.getName(event.initiator)] then - mist.DBs.activeHumans[Unit.getName(event.initiator)] = nil - -- trigger.action.outText('removed via control: ' .. Unit.getName(event.initiator), 20) - end - end - end - - mist.addEventHandler(addClientsToActive) - ]] - - --- 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') - - log = mist.log -- log shorthand - -- set warning log level, showing only - -- warnings and errors - log:setLevel("warning") - - log:info("initializing databases") - initDBs() - - -- add event handler for group spawns - mist.addEventHandler(groupSpawned) - mist.addEventHandler(addDeadObject) - - -- 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) - return - end - - --- The main function. - -- Run 100 times per second. - -- You shouldn't call this function. - function mist.main() - timer.scheduleFunction(mist.main, {}, timer.getTime() + 0.01) --reschedule first in case of Lua error - - updateTenthSecond = updateTenthSecond + 1 - if updateTenthSecond == 10 then - updateTenthSecond = 0 - - checkSpawnedEventsNew() - - if not coroutines.updateDBTables then - coroutines.updateDBTables = coroutine.create(updateDBTables) - end - - coroutine.resume(coroutines.updateDBTables) - - if coroutine.status(coroutines.updateDBTables) == 'dead' then - coroutines.updateDBTables = nil - end - end - - --updating alive units - updateAliveUnitsCounter = updateAliveUnitsCounter + 1 - if updateAliveUnitsCounter == 5 then - updateAliveUnitsCounter = 0 - - if not coroutines.updateAliveUnits then - coroutines.updateAliveUnits = coroutine.create(updateAliveUnits) - end - - coroutine.resume(coroutines.updateAliveUnits) - - if coroutine.status(coroutines.updateAliveUnits) == 'dead' then - coroutines.updateAliveUnits = nil - end - end - - doScheduledFunctions() - end -- end of mist.main - - --- Returns next unit id. - -- @treturn number next unit id. - function mist.getNextUnitId() - mist.nextUnitId = mist.nextUnitId + 1 - if mist.nextUnitId > 6900 then - mist.nextUnitId = 14000 - end - return 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 - end - return mist.nextGroupId - end - - --- Returns timestamp of last database update. - -- @treturn timestamp of last database update - function mist.getLastDBUpdateTime() - return lastUpdateTime - end - - --- Spawns a static object to the game world. - -- @todo write good docs - -- @tparam table staticObj table containing data needed for the object creation - function mist.dynAddStatic(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 - newObj[entry] = val - end - end - end - --log:info(newObj) - - local cntry = newObj.country - if newObj.countryId then - cntry = newObj.countryId - end - - local newCountry = '' - - for countryId, countryName in pairs(country.name) do - if type(cntry) == 'string' then - cntry = cntry:gsub("%s+", "_") - if tostring(countryName) == string.upper(cntry) then - newCountry = countryName - end - elseif type(cntry) == 'number' then - if countryId == cntry then - newCountry = countryName - end - end - end - - if newCountry == '' then - log:error("Country not found: $1", cntry) - return false - end - - if newObj.clone or not newObj.groupId then - mistGpId = mistGpId + 1 - newObj.groupId = mistGpId - end - - if newObj.clone or not newObj.unitId then - mistUnitId = mistUnitId + 1 - newObj.unitId = mistUnitId - end - - if newObj.clone or not newObj.name then - mistDynAddIndex[' static '] = mistDynAddIndex[' static '] + 1 - newObj.name = (newCountry .. ' static ' .. mistDynAddIndex[' static ']) - end - - if not newObj.dead then - newObj.dead = false - end - - if not newObj.heading then - newObj.heading = math.random(360) - end - - if newObj.categoryStatic then - newObj.category = newObj.categoryStatic - end - if newObj.mass then - newObj.category = 'Cargos' - end - - if newObj.shapeName then - newObj.shape_name = newObj.shapeName - end - - if not newObj.shape_name then - log:info('shape_name not present') - if mist.DBs.const.shapeNames[newObj.type] then - newObj.shape_name = mist.DBs.const.shapeNames[newObj.type] - end - end - - 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') - coalition.addStaticObject(country.id[newCountry], newObj) - - return newObj - end - log:error("Failed to add static object due to missing or incorrect value. X: $1, Y: $2, Type: $3", newObj.x, newObj.y, newObj.type) - return false - end - - --- Spawns a dynamic group into the game world. - -- Same as coalition.add function in SSE. checks the passed data to see if its valid. - -- Will generate groupId, groupName, unitId, and unitName if needed - -- @tparam table newGroup table containting values needed for spawning a group. - function mist.dynAdd(newGroup) - - --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua') - local cntry = newGroup.country - if newGroup.countryId then - cntry = newGroup.countryId - end - - local groupType = newGroup.category - local newCountry = '' - -- validate data - for countryId, countryName in pairs(country.name) do - if type(cntry) == 'string' then - cntry = cntry:gsub("%s+", "_") - if tostring(countryName) == string.upper(cntry) then - newCountry = countryName - end - elseif type(cntry) == 'number' then - if countryId == cntry then - newCountry = countryName - end - end - end - - if newCountry == '' then - log:error("Country not found: $1", cntry) - return false - end - - local newCat = '' - for catName, catId in pairs(Unit.Category) do - if type(groupType) == 'string' then - if tostring(catName) == string.upper(groupType) then - newCat = catName - end - elseif type(groupType) == 'number' then - if catId == groupType then - newCat = catName - end - end - - if catName == 'GROUND_UNIT' and (string.upper(groupType) == 'VEHICLE' or string.upper(groupType) == 'GROUND') then - newCat = 'GROUND_UNIT' - elseif catName == 'AIRPLANE' and string.upper(groupType) == 'PLANE' then - newCat = 'AIRPLANE' - end - end - local typeName - if newCat == 'GROUND_UNIT' then - typeName = ' gnd ' - elseif newCat == 'AIRPLANE' then - typeName = ' air ' - elseif newCat == 'HELICOPTER' then - typeName = ' hel ' - elseif newCat == 'SHIP' then - typeName = ' shp ' - elseif newCat == 'BUILDING' then - typeName = ' bld ' - end - if newGroup.clone or not newGroup.groupId then - mistDynAddIndex[typeName] = mistDynAddIndex[typeName] + 1 - mistGpId = mistGpId + 1 - newGroup.groupId = mistGpId - end - if newGroup.groupName or newGroup.name then - if newGroup.groupName then - newGroup.name = newGroup.groupName - elseif newGroup.name then - newGroup.name = newGroup.name - end - end - - if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then - newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) - end - - if not newGroup.hidden then - newGroup.hidden = false - end - - if not newGroup.visible then - newGroup.visible = false - end - - if (newGroup.start_time and type(newGroup.start_time) ~= 'number') or not newGroup.start_time then - if newGroup.startTime then - newGroup.start_time = mist.utils.round(newGroup.startTime) - else - newGroup.start_time = 0 - end - end - - - for unitIndex, unitData in pairs(newGroup.units) do - local originalName = newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name - if newGroup.clone or not unitData.unitId then - mistUnitId = mistUnitId + 1 - newGroup.units[unitIndex].unitId = mistUnitId - end - if newGroup.units[unitIndex].unitName or newGroup.units[unitIndex].name then - if newGroup.units[unitIndex].unitName then - newGroup.units[unitIndex].name = newGroup.units[unitIndex].unitName - elseif newGroup.units[unitIndex].name then - newGroup.units[unitIndex].name = newGroup.units[unitIndex].name - end - end - if newGroup.clone or not unitData.name then - newGroup.units[unitIndex].name = tostring(newGroup.name .. ' unit' .. unitIndex) - end - - if not unitData.skill then - 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' - end - if not unitData.speed then - if newCat == 'AIRPLANE' then - newGroup.units[unitIndex].speed = 150 - elseif newCat == 'HELICOPTER' then - newGroup.units[unitIndex].speed = 60 - end - end - if not unitData.payload then - newGroup.units[unitIndex].payload = mist.getPayload(originalName) - 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 - newGroup.route = {} - newGroup.route.points = copyRoute - end - end - newGroup.country = newCountry - - - --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua') - - -- sanitize table - newGroup.groupName = nil - newGroup.clone = nil - newGroup.category = nil - newGroup.country = nil - - newGroup.tasks = {} - - for unitIndex, unitData in pairs(newGroup.units) do - newGroup.units[unitIndex].unitName = nil - end - - coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup) - - return newGroup - - end - - --- Schedules a function. - -- Modified Slmod task scheduler, superior to timer.scheduleFunction - -- @tparam function f function to schedule - -- @tparam table vars array containing all parameters passed to the function - -- @tparam number t time in seconds from mission start to schedule the function to. - -- @tparam[opt] number rep time between repetitions of the function - -- @tparam[opt] number st time in seconds from mission start at which the function - -- should stop to be rescheduled. - -- @treturn number scheduled function id. - function mist.scheduleFunction(f, vars, t, rep, st) - --verify correct types - assert(type(f) == 'function', 'variable 1, expected function, got ' .. type(f)) - assert(type(vars) == 'table' or vars == nil, 'variable 2, expected table or nil, got ' .. type(f)) - assert(type(t) == 'number', 'variable 3, expected number, got ' .. type(t)) - assert(type(rep) == 'number' or rep == nil, 'variable 4, expected number or nil, got ' .. type(rep)) - assert(type(st) == 'number' or st == nil, 'variable 5, expected number or nil, got ' .. type(st)) - if not vars then - vars = {} - end - taskId = taskId + 1 - table.insert(scheduledTasks, {f = f, vars = vars, t = t, rep = rep, st = st, id = taskId}) - return taskId - end - - --- Removes a scheduled function. - -- @tparam number id function id - -- @treturn boolean true if function was successfully removed, false otherwise. - function mist.removeFunction(id) - local i = 1 - while i <= #scheduledTasks do - if scheduledTasks[i].id == id then - table.remove(scheduledTasks, i) - else - i = i + 1 - end - end - end - - --- Registers an event handler. - -- @tparam function f function handling event - -- @treturn number id of the event handler - function mist.addEventHandler(f) --id is optional! - local handler = {} - idNum = idNum + 1 - handler.id = idNum - handler.f = f - function handler:onEvent(event) - self.f(event) - end - world.addEventHandler(handler) - return handler.id - end - - --- Removes event handler with given id. - -- @tparam number id event handler id - -- @treturn boolean true on success, false otherwise - function mist.removeEventHandler(id) - for key, handler in pairs(world.eventHandlers) do - if handler.id and handler.id == id then - world.eventHandlers[key] = nil - return true - end - end - return false - end -end - --- Begin common funcs -do - --- Returns MGRS coordinates as string. - -- @tparam string MGRS MGRS coordinates - -- @tparam number acc the accuracy of each easting/northing. - -- Can be: 0, 1, 2, 3, 4, or 5. - function mist.tostringMGRS(MGRS, acc) - if acc == 0 then - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph - else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', mist.utils.round(MGRS.Northing/(10^(5-acc)), 0)) - end - end - - --[[acc: - in DM: decimal point of minutes. - In DMS: decimal point of seconds. - position after the decimal of the least significant digit: - So: - 42.32 - acc of 2. - ]] - function mist.tostringLL(lat, lon, acc, DMS) - - local latHemi, lonHemi - if lat > 0 then - latHemi = 'N' - else - latHemi = 'S' - end - - if lon > 0 then - lonHemi = 'E' - else - lonHemi = 'W' - end - - lat = math.abs(lat) - lon = math.abs(lon) - - local latDeg = math.floor(lat) - local latMin = (lat - latDeg)*60 - - local lonDeg = math.floor(lon) - local lonMin = (lon - lonDeg)*60 - - if DMS then -- degrees, minutes, and seconds. - local oldLatMin = latMin - latMin = math.floor(latMin) - local latSec = mist.utils.round((oldLatMin - latMin)*60, acc) - - local oldLonMin = lonMin - lonMin = math.floor(lonMin) - local lonSec = mist.utils.round((oldLonMin - lonMin)*60, acc) - - if latSec == 60 then - latSec = 0 - latMin = latMin + 1 - end - - if lonSec == 60 then - lonSec = 0 - lonMin = lonMin + 1 - end - - local secFrmtStr -- create the formatting string for the seconds place - if acc <= 0 then -- no decimal place. - secFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi - - else -- degrees, decimal minutes. - latMin = mist.utils.round(latMin, acc) - lonMin = mist.utils.round(lonMin, acc) - - if latMin == 60 then - latMin = 0 - latDeg = latDeg + 1 - end - - if lonMin == 60 then - lonMin = 0 - lonDeg = lonDeg + 1 - end - - local minFrmtStr -- create the formatting string for the minutes place - if acc <= 0 then -- no decimal place. - minFrmtStr = '%02d' - else - local width = 3 + acc -- 01.310 - that's a width of 6, for example. - minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' - end - - return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi - - end - end - - --[[ required: az - radian - required: dist - meters - optional: alt - meters (set to false or nil if you don't want to use it). - optional: metric - set true to get dist and alt in km and m. - precision will always be nearest degree and NM or km.]] - function mist.tostringBR(az, dist, alt, metric) - az = mist.utils.round(mist.utils.toDegree(az), 0) - - if metric then - dist = mist.utils.round(dist/1000, 0) - else - dist = mist.utils.round(mist.utils.metersToNM(dist), 0) - end - - local s = string.format('%03d', az) .. ' for ' .. dist - - if alt then - if metric then - s = s .. ' at ' .. mist.utils.round(alt, 0) - else - s = s .. ' at ' .. mist.utils.round(mist.utils.metersToFeet(alt), 0) - end - end - return s - end - - function mist.getNorthCorrection(gPoint) --gets the correction needed for true north - local point = mist.utils.deepCopy(gPoint) - if not point.z then --Vec2; convert to Vec3 - point.z = point.y - point.y = 0 - end - local lat, lon = coord.LOtoLL(point) - local north_posit = coord.LLtoLO(lat + 1, lon) - return math.atan2(north_posit.z - point.z, north_posit.x - point.x) - end - - --- Returns skill of the given unit. - -- @tparam string unitName unit name - -- @return skill of the unit - function mist.getUnitSkill(unitName) - if mist.DBs.unitsByName[unitName] then - if Unit.getByName(unitName) then - local lunit = Unit.getByName(unitName) - local data = mist.DBs.unitsByName[unitName] - if data.unitName == unitName and data.type == lunit:getTypeName() and data.unitId == tonumber(lunit:getID()) and data.skill then - return data.skill - end - end - end - log:error("Unit not found in DB: $1", unitName) - return false - end - - --- Returns an array containing a group's units positions. - -- e.g. - -- { - -- [1] = {x = 299435.224, y = -1146632.6773}, - -- [2] = {x = 663324.6563, y = 322424.1112} - -- } - -- @tparam number|string groupIdent group id or name - -- @treturn table array containing positions of each group member - function mist.getGroupPoints(groupIdent) - -- search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - if mist.DBs.MEgroupsByName[groupIdent] then - gpId = mist.DBs.MEgroupsByName[groupIdent].groupId - else - log:error("Group not found in mist.DBs.MEgroupsByName: $1", groupIdent) - end - 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 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 - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - for point_num, point in pairs(group_data.route.points) do - if not point.point then - points[point_num] = { x = point.x, y = point.y } - else - points[point_num] = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - end - return points - end - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - end - - --- getUnitAttitude(unit) return values. - -- Yaw, AoA, ClimbAngle - relative to earth reference - -- DOES NOT TAKE INTO ACCOUNT WIND. - -- @table attitude - -- @tfield number Heading in radians, range of 0 to 2*pi, - -- relative to true north. - -- @tfield number Pitch in radians, range of -pi/2 to pi/2 - -- @tfield number Roll in radians, range of 0 to 2*pi, - -- right roll is positive direction. - -- @tfield number Yaw in radians, range of -pi to pi, - -- right yaw is positive direction. - -- @tfield number AoA in radians, range of -pi to pi, - -- rotation of aircraft to the right in comparison to - -- flight direction being positive. - -- @tfield number ClimbAngle in radians, range of -pi/2 to pi/2 - - --- Returns the attitude of a given unit. - -- Will work on any unit, even if not an aircraft. - -- @tparam Unit unit unit whose attitude is returned. - -- @treturn table @{attitude} - function mist.getAttitude(unit) - local unitpos = unit:getPosition() - if unitpos then - - local Heading = math.atan2(unitpos.x.z, unitpos.x.x) - - Heading = Heading + mist.getNorthCorrection(unitpos.p) - - if Heading < 0 then - Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi - end - ---- heading complete.---- - - local Pitch = math.asin(unitpos.x.y) - ---- pitch complete.---- - - -- now get roll: - --maybe not the best way to do it, but it works. - - --first, make a vector that is perpendicular to y and unitpos.x with cross product - local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0}) - - --now, get dot product of of this cross product with unitpos.z - local dp = mist.vec.dp(cp, unitpos.z) - - --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) - local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z))) - - --now, have to get sign of roll. - -- by convention, making right roll positive - -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. - - if unitpos.z.y > 0 then -- left roll, flip the sign of the roll - Roll = -Roll - end - ---- roll complete. ---- - - --now, work on yaw, AoA, climb, and abs velocity - local Yaw - local AoA - local ClimbAngle - - -- get unit velocity - local unitvel = unit:getVelocity() - if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! - local AxialVel = {} --unit velocity transformed into aircraft axes directions - - --transform velocity components in direction of aircraft axes. - AxialVel.x = mist.vec.dp(unitpos.x, unitvel) - AxialVel.y = mist.vec.dp(unitpos.y, unitvel) - AxialVel.z = mist.vec.dp(unitpos.z, unitvel) - - --Yaw is the angle between unitpos.x and the x and z velocities - --define right yaw as positive - Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z})) - - --now set correct direction: - if AxialVel.z > 0 then - Yaw = -Yaw - end - - -- AoA is angle between unitpos.x and the x and y velocities - AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0})) - - --now set correct direction: - if AxialVel.y > 0 then - AoA = -AoA - end - - ClimbAngle = math.asin(unitvel.y/mist.vec.mag(unitvel)) - end - return { Heading = Heading, Pitch = Pitch, Roll = Roll, Yaw = Yaw, AoA = AoA, ClimbAngle = ClimbAngle} - else - log:error("Couldn't get unit's position") - end - end - - --- Returns heading of given unit. - -- @tparam Unit unit unit whose heading is returned. - -- @param rawHeading - -- @treturn number heading of the unit, in range - -- of 0 to 2*pi. - function mist.getHeading(unit, rawHeading) - local unitpos = unit:getPosition() - if unitpos then - local Heading = math.atan2(unitpos.x.z, unitpos.x.x) - if not rawHeading then - Heading = Heading + mist.getNorthCorrection(unitpos.p) - end - if Heading < 0 then - Heading = Heading + 2*math.pi -- put heading in range of 0 to 2*pi - end - return Heading - end - end - - --- Returns given unit's pitch - -- @tparam Unit unit unit whose pitch is returned. - -- @treturn number pitch of given unit - function mist.getPitch(unit) - local unitpos = unit:getPosition() - if unitpos then - return math.asin(unitpos.x.y) - end - end - - --- Returns given unit's roll. - -- @tparam Unit unit unit whose roll is returned. - -- @treturn number roll of given unit - function mist.getRoll(unit) - local unitpos = unit:getPosition() - if unitpos then - -- now get roll: - --maybe not the best way to do it, but it works. - - --first, make a vector that is perpendicular to y and unitpos.x with cross product - local cp = mist.vec.cp(unitpos.x, {x = 0, y = 1, z = 0}) - - --now, get dot product of of this cross product with unitpos.z - local dp = mist.vec.dp(cp, unitpos.z) - - --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) - local Roll = math.acos(dp/(mist.vec.mag(cp)*mist.vec.mag(unitpos.z))) - - --now, have to get sign of roll. - -- by convention, making right roll positive - -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. - - if unitpos.z.y > 0 then -- left roll, flip the sign of the roll - Roll = -Roll - end - return Roll - end - end - - --- Returns given unit's yaw. - -- @tparam Unit unit unit whose yaw is returned. - -- @treturn number yaw of given unit. - function mist.getYaw(unit) - local unitpos = unit:getPosition() - if unitpos then - -- get unit velocity - local unitvel = unit:getVelocity() - if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! - local AxialVel = {} --unit velocity transformed into aircraft axes directions - - --transform velocity components in direction of aircraft axes. - AxialVel.x = mist.vec.dp(unitpos.x, unitvel) - AxialVel.y = mist.vec.dp(unitpos.y, unitvel) - AxialVel.z = mist.vec.dp(unitpos.z, unitvel) - - --Yaw is the angle between unitpos.x and the x and z velocities - --define right yaw as positive - local Yaw = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/mist.vec.mag({x = AxialVel.x, y = 0, z = AxialVel.z})) - - --now set correct direction: - if AxialVel.z > 0 then - Yaw = -Yaw - end - return Yaw - end - end - end - - --- Returns given unit's angle of attack. - -- @tparam Unit unit unit to get AoA from. - -- @treturn number angle of attack of the given unit. - function mist.getAoA(unit) - local unitpos = unit:getPosition() - if unitpos then - local unitvel = unit:getVelocity() - if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! - local AxialVel = {} --unit velocity transformed into aircraft axes directions - - --transform velocity components in direction of aircraft axes. - AxialVel.x = mist.vec.dp(unitpos.x, unitvel) - AxialVel.y = mist.vec.dp(unitpos.y, unitvel) - AxialVel.z = mist.vec.dp(unitpos.z, unitvel) - - -- AoA is angle between unitpos.x and the x and y velocities - local AoA = math.acos(mist.vec.dp({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/mist.vec.mag({x = AxialVel.x, y = AxialVel.y, z = 0})) - - --now set correct direction: - if AxialVel.y > 0 then - AoA = -AoA - end - return AoA - end - end - end - - --- Returns given unit's climb angle. - -- @tparam Unit unit unit to get climb angle from. - -- @treturn number climb angle of given unit. - function mist.getClimbAngle(unit) - local unitpos = unit:getPosition() - if unitpos then - local unitvel = unit:getVelocity() - if mist.vec.mag(unitvel) ~= 0 then --must have non-zero velocity! - return math.asin(unitvel.y/mist.vec.mag(unitvel)) - end - end - end - - --[[-- - Unit name table. - Many Mist functions require tables of unit names, which are known - in Mist as UnitNameTables. These follow a special set of shortcuts - borrowed from Slmod. These shortcuts alleviate the problem of entering - huge lists of unit names by hand, and in many cases, they remove the - need to even know the names of the units in the first place! - - These are the unit table "short-cut" commands: - - Prefixes: - "[-u]" - subtract this unit if its in the table - "[g]" - add this group to the table - "[-g]" - subtract this group from the table - "[c]" - add this country's units - "[-c]" - subtract this country's units if any are in the table - - Stand-alone identifiers - "[all]" - add all units - "[-all]" - subtract all units (not very useful by itself) - "[blue]" - add all blue units - "[-blue]" - subtract all blue units - "[red]" - add all red coalition units - "[-red]" - subtract all red units - - Compound Identifiers: - "[c][helicopter]" - add all of this country's helicopters - "[-c][helicopter]" - subtract all of this country's helicopters - "[c][plane]" - add all of this country's planes - "[-c][plane]" - subtract all of this country's planes - "[c][ship]" - add all of this country's ships - "[-c][ship]" - subtract all of this country's ships - "[c][vehicle]" - add all of this country's vehicles - "[-c][vehicle]" - subtract all of this country's vehicles - - "[all][helicopter]" - add all helicopters - "[-all][helicopter]" - subtract all helicopters - "[all][plane]" - add all planes - "[-all][plane]" - subtract all planes - "[all][ship]" - add all ships - "[-all][ship]" - subtract all ships - "[all][vehicle]" - add all vehicles - "[-all][vehicle]" - subtract all vehicles - - "[blue][helicopter]" - add all blue coalition helicopters - "[-blue][helicopter]" - subtract all blue coalition helicopters - "[blue][plane]" - add all blue coalition planes - "[-blue][plane]" - subtract all blue coalition planes - "[blue][ship]" - add all blue coalition ships - "[-blue][ship]" - subtract all blue coalition ships - "[blue][vehicle]" - add all blue coalition vehicles - "[-blue][vehicle]" - subtract all blue coalition vehicles - - "[red][helicopter]" - add all red coalition helicopters - "[-red][helicopter]" - subtract all red coalition helicopters - "[red][plane]" - add all red coalition planes - "[-red][plane]" - subtract all red coalition planes - "[red][ship]" - add all red coalition ships - "[-red][ship]" - subtract all red coalition ships - "[red][vehicle]" - add all red coalition vehicles - "[-red][vehicle]" - subtract all red coalition vehicles - - Country names to be used in [c] and [-c] short-cuts: - Turkey - Norway - The Netherlands - Spain - 11 - UK - Denmark - USA - Georgia - Germany - Belgium - Canada - France - Israel - Ukraine - Russia - South Ossetia - Abkhazia - Italy - Australia - Austria - Belarus - Bulgaria - Czech Republic - China - Croatia - Finland - Greece - Hungary - India - Iran - Iraq - Japan - Kazakhstan - North Korea - Pakistan - Poland - Romania - Saudi Arabia - Serbia, Slovakia - South Korea - Sweden - Switzerland - Syria - USAF Aggressors - - Do NOT use a '[u]' notation for single units. Single units are referenced - the same way as before: Simply input their names as strings. - - These unit tables are evaluated in order, and you cannot subtract a unit - from a table before it is added. For example: - - {'[blue]', '[-c]Georgia'} - - will evaluate to all of blue coalition except those units owned by the - country named "Georgia"; however: - - {'[-c]Georgia', '[blue]'} - - will evaluate to all of the units in blue coalition, because the addition - of all units owned by blue coalition occurred AFTER the subtraction of all - units owned by Georgia (which actually subtracted nothing at all, since - there were no units in the table when the subtraction occurred). - - More examples: - - {'[blue][plane]', '[-c]Georgia', '[-g]Hawg 1'} - - Evaluates to all blue planes, except those blue units owned by the country - named "Georgia" and the units in the group named "Hawg1". - - - {'[g]arty1', '[g]arty2', '[-u]arty1_AD', '[-u]arty2_AD', 'Shark 11' } - - Evaluates to the unit named "Shark 11", plus all the units in groups named - "arty1" and "arty2" except those that are named "arty1\_AD" and "arty2\_AD". - - @table UnitNameTable - ]] - - --- Returns a table containing unit names. - -- @tparam table tbl sequential strings - -- @treturn table @{UnitNameTable} - function mist.makeUnitTable(tbl) - --Assumption: will be passed a table of strings, sequential - log:info(tbl) - local units_by_name = {} - - local l_munits = mist.DBs.units --local reference for faster execution - for i = 1, #tbl do - local unit = tbl[i] - if unit:sub(1,4) == '[-u]' then --subtract a unit - if units_by_name[unit:sub(5)] then -- 5 to end - units_by_name[unit:sub(5)] = nil --remove - end - elseif unit:sub(1,3) == '[g]' then -- add a group - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(4) then - -- index 4 to end - for unit_ind, unit in pairs(group_tbl.units) do - units_by_name[unit.unitName] = true --add - end - end - end - end - end - end - end - elseif unit:sub(1,4) == '[-g]' then -- subtract a group - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' and group_tbl.groupName == unit:sub(5) then - -- index 5 to end - for unit_ind, unit in pairs(group_tbl.units) do - if units_by_name[unit.unitName] then - units_by_name[unit.unitName] = nil --remove - end - end - end - end - end - end - end - end - elseif unit:sub(1,3) == '[c]' then -- add a country - local category = '' - local country_start = 4 - if unit:sub(4,15) == '[helicopter]' then - category = 'helicopter' - country_start = 16 - elseif unit:sub(4,10) == '[plane]' then - category = 'plane' - country_start = 11 - elseif unit:sub(4,9) == '[ship]' then - category = 'ship' - country_start = 10 - elseif unit:sub(4,12) == '[vehicle]' then - category = 'vehicle' - country_start = 13 - end - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - if country == string.lower(unit:sub(country_start)) then -- match - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - units_by_name[unit.unitName] = true --add - end - end - end - end - end - end - end - end - elseif unit:sub(1,4) == '[-c]' then -- subtract a country - local category = '' - local country_start = 5 - if unit:sub(5,16) == '[helicopter]' then - category = 'helicopter' - country_start = 17 - elseif unit:sub(5,11) == '[plane]' then - category = 'plane' - country_start = 12 - elseif unit:sub(5,10) == '[ship]' then - category = 'ship' - country_start = 11 - elseif unit:sub(5,13) == '[vehicle]' then - category = 'vehicle' - country_start = 14 - end - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - if country == string.lower(unit:sub(country_start)) then -- match - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - if units_by_name[unit.unitName] then - units_by_name[unit.unitName] = nil --remove - end - end - end - end - end - end - end - end - end - elseif unit:sub(1,6) == '[blue]' then -- add blue coalition - local category = '' - if unit:sub(7) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(7) == '[plane]' then - category = 'plane' - elseif unit:sub(7) == '[ship]' then - category = 'ship' - elseif unit:sub(7) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - if coa == 'blue' then - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - units_by_name[unit.unitName] = true --add - end - end - end - end - end - end - end - end - elseif unit:sub(1,7) == '[-blue]' then -- subtract blue coalition - local category = '' - if unit:sub(8) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(8) == '[plane]' then - category = 'plane' - elseif unit:sub(8) == '[ship]' then - category = 'ship' - elseif unit:sub(8) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - if coa == 'blue' then - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - if units_by_name[unit.unitName] then - units_by_name[unit.unitName] = nil --remove - end - end - end - end - end - end - end - end - end - elseif unit:sub(1,5) == '[red]' then -- add red coalition - local category = '' - if unit:sub(6) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(6) == '[plane]' then - category = 'plane' - elseif unit:sub(6) == '[ship]' then - category = 'ship' - elseif unit:sub(6) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - if coa == 'red' then - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - units_by_name[unit.unitName] = true --add - end - end - end - end - end - end - end - end - elseif unit:sub(1,6) == '[-red]' then -- subtract red coalition - local category = '' - if unit:sub(7) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(7) == '[plane]' then - category = 'plane' - elseif unit:sub(7) == '[ship]' then - category = 'ship' - elseif unit:sub(7) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - if coa == 'red' then - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - if units_by_name[unit.unitName] then - units_by_name[unit.unitName] = nil --remove - end - end - end - end - end - end - end - end - end - elseif unit:sub(1,5) == '[all]' then -- add all of a certain category (or all categories) - local category = '' - if unit:sub(6) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(6) == '[plane]' then - category = 'plane' - elseif unit:sub(6) == '[ship]' then - category = 'ship' - elseif unit:sub(6) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - units_by_name[unit.unitName] = true --add - end - end - end - end - end - end - end - elseif unit:sub(1,6) == '[-all]' then -- subtract all of a certain category (or all categories) - local category = '' - if unit:sub(7) == '[helicopter]' then - category = 'helicopter' - elseif unit:sub(7) == '[plane]' then - category = 'plane' - elseif unit:sub(7) == '[ship]' then - category = 'ship' - elseif unit:sub(7) == '[vehicle]' then - category = 'vehicle' - end - for coa, coa_tbl in pairs(l_munits) do - for country, country_table in pairs(coa_tbl) do - for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then - for group_ind, group_tbl in pairs(unit_type_tbl) do - if type(group_tbl) == 'table' then - for unit_ind, unit in pairs(group_tbl.units) do - if units_by_name[unit.unitName] then - units_by_name[unit.unitName] = nil --remove - end - end - end - end - end - end - end - end - else -- just a regular unit - units_by_name[unit] = true --add - end - end - - local units_tbl = {} -- indexed sequentially - for unit_name, val in pairs(units_by_name) do - if val then - units_tbl[#units_tbl + 1] = unit_name -- add all the units to the table - end - end - - - units_tbl.processed = timer.getTime() --add the processed flag - return units_tbl -end - -function mist.getDeadMapObjsInZones(zone_names) - -- zone_names: table of zone names - -- returns: table of dead map objects (indexed numerically) - local map_objs = {} - local zones = {} - for i = 1, #zone_names do - if mist.DBs.zonesByName[zone_names[i]] then - zones[#zones + 1] = mist.DBs.zonesByName[zone_names[i]] - end - end - for obj_id, obj in pairs(mist.DBs.deadObjects) do - if obj.objectType and obj.objectType == 'building' then --dead map object - for i = 1, #zones do - if ((zones[i].point.x - obj.objectPos.x)^2 + (zones[i].point.z - obj.objectPos.z)^2)^0.5 <= zones[i].radius then - map_objs[#map_objs + 1] = mist.utils.deepCopy(obj) - end - end - end - end - return map_objs -end - -function mist.getDeadMapObjsInPolygonZone(zone) - -- zone_names: table of zone names - -- returns: table of dead map objects (indexed numerically) - local map_objs = {} - for obj_id, obj in pairs(mist.DBs.deadObjects) do - if obj.objectType and obj.objectType == 'building' then --dead map object - if mist.pointInPolygon(obj.objectPos, zone) then - map_objs[#map_objs + 1] = mist.utils.deepCopy(obj) - end - end - end - return map_objs -end - -function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm - --[[local type_tbl = { - point = {'table'}, - poly = {'table'}, - maxalt = {'number', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.pointInPolygon', type_tbl, {point, poly, maxalt}) - assert(err, errmsg) - ]] - point = mist.utils.makeVec3(point) - local px = point.x - local pz = point.z - local cn = 0 - local newpoly = mist.utils.deepCopy(poly) - - if not maxalt or (point.y <= maxalt) then - local polysize = #newpoly - newpoly[#newpoly + 1] = newpoly[1] - - newpoly[1] = mist.utils.makeVec3(newpoly[1]) - - for k = 1, polysize do - newpoly[k+1] = mist.utils.makeVec3(newpoly[k+1]) - if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then - local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z) - if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then - cn = cn + 1 - end - end - end - - return cn%2 == 1 - else - return false - end -end - -function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) - local units = {} - - for i = 1, #unit_names do - units[#units + 1] = Unit.getByName(unitNames[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] - end - end - - return inZoneUnits -end - -function mist.getUnitsInZones(unit_names, zone_names, zone_type) - - zone_type = zone_type or 'cylinder' - if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then - zone_type = 'cylinder' - end - if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then - zone_type = 'sphere' - end - - assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type)) - - local units = {} - local zones = {} - - for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) - if unit then - units[#units + 1] = unit - end - end - - - for k = 1, #zone_names do - local zone = trigger.misc.getZone(zone_names[k]) - if zone then - zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z} - end - end - - local in_zone_units = {} - - for units_ind = 1, #units do - for zones_ind = 1, #zones do - if zone_type == 'sphere' then --add land height value for sphere zone type - local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z}) - if alt then - zones[zones_ind].y = alt - end - end - local unit_pos = units[units_ind]:getPosition().p - 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 - elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^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 - end - end - end - end - return in_zone_units -end - -function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_type) - - zone_type = zone_type or 'cylinder' - if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then - zone_type = 'cylinder' - end - if zone_type == 's' or zone_type == 'spherical' or zone_type == 'S' then - zone_type = 'sphere' - end - - assert(zone_type == 'cylinder' or zone_type == 'sphere', 'invalid zone_type: ' .. tostring(zone_type)) - - local units = {} - local zone_units = {} - - for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) - if unit then - units[#units + 1] = unit - end - end - - for k = 1, #zone_unit_names do - local unit = Unit.getByName(zone_unit_names[k]) - if unit then - zone_units[#zone_units + 1] = unit - end - end - - local in_zone_units = {} - - for units_ind = 1, #units do - for zone_units_ind = 1, #zone_units do - local unit_pos = units[units_ind]:getPosition().p - local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p - if unit_pos and zone_unit_pos and units[units_ind]:isActive() == true then - if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - end - end - end - end - return in_zone_units -end - -function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) - log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius) - radius = radius or math.huge - local unit_info1 = {} - local unit_info2 = {} - - -- get the positions all in one step, saves execution time. - for unitset1_ind = 1, #unitset1 do - local unit1 = Unit.getByName(unitset1[unitset1_ind]) - if unit1 and unit1:isActive() == true then - unit_info1[#unit_info1 + 1] = {} - unit_info1[#unit_info1].unit = unit1 - unit_info1[#unit_info1].pos = unit1:getPosition().p - end - end - - for unitset2_ind = 1, #unitset2 do - local unit2 = Unit.getByName(unitset2[unitset2_ind]) - if unit2 and unit2:isActive() == true then - unit_info2[#unit_info2 + 1] = {} - unit_info2[#unit_info2].unit = unit2 - unit_info2[#unit_info2].pos = unit2:getPosition().p - end - end - - local LOS_data = {} - -- now compute los - for unit1_ind = 1, #unit_info1 do - local unit_added = false - for unit2_ind = 1, #unit_info2 do - if radius == math.huge or (mist.vec.mag(mist.vec.sub(unit_info1[unit1_ind].pos, unit_info2[unit2_ind].pos)) < radius) then -- inside radius - local point1 = { x = unit_info1[unit1_ind].pos.x, y = unit_info1[unit1_ind].pos.y + altoffset1, z = unit_info1[unit1_ind].pos.z} - local point2 = { x = unit_info2[unit2_ind].pos.x, y = unit_info2[unit2_ind].pos.y + altoffset2, z = unit_info2[unit2_ind].pos.z} - if land.isVisible(point1, point2) then - if unit_added == false then - unit_added = true - LOS_data[#LOS_data + 1] = {} - LOS_data[#LOS_data].unit = unit_info1[unit1_ind].unit - LOS_data[#LOS_data].vis = {} - LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit - else - LOS_data[#LOS_data].vis[#LOS_data[#LOS_data].vis + 1] = unit_info2[unit2_ind].unit - end - end - end - end - end - - return LOS_data -end - -function mist.getAvgPoint(points) - local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 - for i = 1, #points do - local nPoint = mist.utils.makeVec3(points[i]) - if nPoint.z then - avgX = avgX + nPoint.x - avgY = avgY + nPoint.y - avgZ = avgZ + nPoint.z - totNum = totNum + 1 - end - end - if totNum ~= 0 then - return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum} - end -end - ---Gets the average position of a group of units (by name) -function mist.getAvgPos(unitNames) - local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 - for i = 1, #unitNames do - local unit - if Unit.getByName(unitNames[i]) then - unit = Unit.getByName(unitNames[i]) - elseif StaticObject.getByName(unitNames[i]) then - unit = StaticObject.getByName(unitNames[i]) - end - if unit then - local pos = unit:getPosition().p - if pos then -- you never know O.o - avgX = avgX + pos.x - avgY = avgY + pos.y - avgZ = avgZ + pos.z - totNum = totNum + 1 - end - end - end - if totNum ~= 0 then - return {x = avgX/totNum, y = avgY/totNum, z = avgZ/totNum} - end -end - -function mist.getAvgGroupPos(groupName) - if type(groupName) == 'string' and Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then - groupName = Group.getByName(groupName) - end - local units = {} - for i = 1, #groupName:getSize() do - table.insert(units, groupName.getUnit(i):getName()) - end - - return mist.getAvgPos(units) - -end - ---[[ vars for mist.getMGRSString: -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -]] -function mist.getMGRSString(vars) - local units = vars.units - local acc = vars.acc or 5 - local avgPos = mist.getAvgPos(units) - if avgPos then - return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc) - end -end - ---[[ vars for mist.getLLString -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -]] -function mist.getLLString(vars) - local units = vars.units - local acc = vars.acc or 3 - local DMS = vars.DMS - local avgPos = mist.getAvgPos(units) - if avgPos then - local lat, lon = coord.LOtoLL(avgPos) - return mist.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -]] -function mist.getBRString(vars) - local units = vars.units - local ref = mist.utils.makeVec3(vars.ref, 0) -- turn it into Vec3 if it is not already. - local alt = vars.alt - local metric = vars.metric - local avgPos = mist.getAvgPos(units) - if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = mist.utils.getDir(vec, ref) - local dist = mist.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return mist.tostringBR(dir, dist, alt, metric) - end -end - --- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction. ---[[ vars for mist.getLeadingPos: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -]] -function mist.getLeadingPos(vars) - local units = vars.units - local heading = vars.heading - local radius = vars.radius - if vars.headingDegrees then - heading = mist.utils.toRadian(vars.headingDegrees) - end - - local unitPosTbl = {} - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit and unit:isExist() then - unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p - end - end - if #unitPosTbl > 0 then -- one more more units found. - -- first, find the unit most in the heading direction - local maxPos = -math.huge - - local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = - for i = 1, #unitPosTbl do - local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading) - if (not maxPos) or maxPos < rotatedVec2.x then - maxPos = rotatedVec2.x - maxPosInd = i - end - end - - --now, get all the units around this unit... - local avgPos - if radius then - local maxUnitPos = unitPosTbl[maxPosInd] - local avgx, avgy, avgz, totNum = 0, 0, 0, 0 - for i = 1, #unitPosTbl do - if mist.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then - avgx = avgx + unitPosTbl[i].x - avgy = avgy + unitPosTbl[i].y - avgz = avgz + unitPosTbl[i].z - totNum = totNum + 1 - end - end - avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum} - else - avgPos = unitPosTbl[maxPosInd] - end - - return avgPos - end -end - ---[[ vars for mist.getLeadingMGRSString: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number, 0 to 5. -]] -function mist.getLeadingMGRSString(vars) - local pos = mist.getLeadingPos(vars) - if pos then - local acc = vars.acc or 5 - return mist.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc) - end -end - ---[[ vars for mist.getLeadingLLString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. -]] -function mist.getLeadingLLString(vars) - local pos = mist.getLeadingPos(vars) - if pos then - local acc = vars.acc or 3 - local DMS = vars.DMS - local lat, lon = coord.LOtoLL(pos) - return mist.tostringLL(lat, lon, acc, DMS) - end -end - ---[[ vars for mist.getLeadingBRString: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees -vars.metric - boolean, if true, use km instead of NM. -vars.alt - boolean, if true, include altitude. -vars.ref - vec3/vec2 reference point. -]] -function mist.getLeadingBRString(vars) - local pos = mist.getLeadingPos(vars) - if pos then - local ref = vars.ref - local alt = vars.alt - local metric = vars.metric - - local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z} - local dir = mist.utils.getDir(vec, ref) - local dist = mist.utils.get2DDist(pos, ref) - if alt then - alt = pos.y - end - return mist.tostringBR(dir, dist, alt, metric) - end -end - -end - ---- Group functions. --- @section groups -do -- group functions scope - - --- Check table used for group creation. - -- @tparam table groupData table to check. - -- @treturn boolean true if a group can be spawned using - -- this table, false otherwise. - function mist.groupTableCheck(groupData) - -- return false if country, category - -- or units are missing - if not groupData.country or - not groupData.category or - not groupData.units then - return false - end - -- return false if unitData misses - -- x, y or type - for unitId, unitData in pairs(groupData.units) do - if not unitData.x or - not unitData.y or - not unitData.type then - return false - end - end - -- everything we need is here return true - return true - end - - --- Returns group data table of give group. - function mist.getCurrentGroupData(gpName) - local dbData = mist.getGroupData(gpName) - - if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then - local newGroup = Group.getByName(gpName) - local newData = {} - newData.name = gpName - newData.groupId = tonumber(newGroup:getID()) - newData.category = newGroup:getCategory() - newData.groupName = gpName - newData.hidden = dbData.hidden - - if newData.category == 2 then - newData.category = 'vehicle' - elseif newData.category == 3 then - newData.category = 'ship' - end - - newData.units = {} - local newUnits = newGroup:getUnits() - for unitNum, unitData in pairs(newGroup:getUnits()) do - newData.units[unitNum] = {} - local uName = unitData:getName() - - if mist.DBs.unitsByName[uName] and unitData:getTypeName() == mist.DBs.unitsByName[uName].type and mist.DBs.unitsByName[uName].unitId == tonumber(unitData:getID()) then -- If old data matches most of new data - newData.units[unitNum] = mist.utils.deepCopy(mist.DBs.unitsByName[uName]) - else - newData.units[unitNum].unitId = tonumber(unitData:getID()) - newData.units[unitNum].type = unitData:getTypeName() - newData.units[unitNum].skill = mist.getUnitSkill(uName) - newData.country = string.lower(country.name[unitData:getCountry()]) - newData.units[unitNum].callsign = unitData:getCallsign() - newData.units[unitNum].unitName = uName - end - - newData.units[unitNum].x = unitData:getPosition().p.x - newData.units[unitNum].y = unitData:getPosition().p.z - newData.units[unitNum].point = {x = newData.units[unitNum].x, y = newData.units[unitNum].y} - newData.units[unitNum].heading = mist.getHeading(unitData, true) -- added to DBs - newData.units[unitNum].alt = unitData:getPosition().p.y - newData.units[unitNum].speed = mist.vec.mag(unitData:getVelocity()) - - end - - return newData - elseif StaticObject.getByName(gpName) and StaticObject.getByName(gpName):isExist() == true then - local staticObj = StaticObject.getByName(gpName) - dbData.units[1].x = staticObj:getPosition().p.x - dbData.units[1].y = staticObj:getPosition().p.z - dbData.units[1].alt = staticObj:getPosition().p.y - dbData.units[1].heading = mist.getHeading(staticObj, true) - - return dbData - end - - end - - function mist.getGroupData(gpName) - local found = false - local newData = {} - if mist.DBs.groupsByName[gpName] then - newData = mist.utils.deepCopy(mist.DBs.groupsByName[gpName]) - found = true - end - - if found == false then - for groupName, groupData in pairs(mist.DBs.groupsByName) do - if mist.stringMatch(groupName, gpName) == true then - newData = mist.utils.deepCopy(groupData) - newData.groupName = groupName - found = true - break - end - end - end - - local payloads - if newData.category == 'plane' or newData.category == 'helicopter' then - payloads = mist.getGroupPayload(newData.groupName) - end - if found == true then - --newData.hidden = false -- maybe add this to DBs - - for unitNum, unitData in pairs(newData.units) do - newData.units[unitNum] = {} - - newData.units[unitNum].unitId = unitData.unitId - --newData.units[unitNum].point = unitData.point - newData.units[unitNum].x = unitData.point.x - newData.units[unitNum].y = unitData.point.y - newData.units[unitNum].alt = unitData.alt - newData.units[unitNum].alt_type = unitData.alt_type - newData.units[unitNum].speed = unitData.speed - newData.units[unitNum].type = unitData.type - newData.units[unitNum].skill = unitData.skill - newData.units[unitNum].unitName = unitData.unitName - newData.units[unitNum].heading = unitData.heading -- added to DBs - newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs - - - if newData.category == 'plane' or newData.category == 'helicopter' then - newData.units[unitNum].payload = payloads[unitNum] - newData.units[unitNum].livery_id = unitData.livery_id - newData.units[unitNum].onboard_num = unitData.onboard_num - newData.units[unitNum].callsign = unitData.callsign - newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft - end - if newData.category == 'static' then - newData.units[unitNum].categoryStatic = unitData.categoryStatic - newData.units[unitNum].mass = unitData.mass - newData.units[unitNum].canCargo = unitData.canCargo - newData.units[unitNum].shape_name = unitData.shape_name - end - end - --log:info(newData) - return newData - else - log:error('$1 not found in MIST database', gpName) - return - end - end - - function mist.getPayload(unitIdent) - -- refactor to search by groupId and allow groupId and groupName as inputs - local unitId = unitIdent - if type(unitIdent) == 'string' and not tonumber(unitIdent) then - if mist.DBs.MEunitsByName[unitIdent] then - unitId = mist.DBs.MEunitsByName[unitIdent].unitId - else - log:error("Unit not found in mist.DBs.MEunitsByName: $1", unitIdent) - end - end - local gpId = mist.DBs.MEunitsById[unitId].groupId - - if gpId and unitId 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 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 - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then - for unitIndex, unitData in pairs(group_data.units) do --group index - if unitData.unitId == unitId then - return unitData.payload - end - end - end - end - end - end - end - end - end - end - end - else - log:error('Need string or number. Got: $1', type(unitIdent)) - return false - end - log:warn("Couldn't find payload for unit: $1", unitIdent) - return - end - - function mist.getGroupPayload(groupIdent) - local gpId = groupIdent - if type(groupIdent) == 'string' and not tonumber(groupIdent) then - if mist.DBs.MEgroupsByName[groupIdent] then - gpId = mist.DBs.MEgroupsByName[groupIdent].groupId - else - log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) - end - end - - 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 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 - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then - local payloads = {} - for unitIndex, unitData in pairs(group_data.units) do --group index - payloads[unitIndex] = unitData.payload - end - return payloads - end - end - end - end - end - end - end - end - end - else - log:error('Need string or number. Got: $1', type(groupIdent)) - return false - end - log:warn("Couldn't find payload for group: $1", groupIdent) - return - - end - - function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call - local point = vars.point - - local gpName - if vars.gpName then - gpName = vars.gpName - elseif vars.groupName then - gpName = vars.groupName - else - log:error('Missing field groupName or gpName in variable table') - end - - 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 radius = vars.radius or 0 - local innerRadius = vars.innerRadius - - local route = vars.route - local dbData = false - - local newGroupData - if gpName and not vars.groupData then - if string.lower(action) == 'teleport' or string.lower(action) == 'tele' then - newGroupData = mist.getCurrentGroupData(gpName) - elseif string.lower(action) == 'respawn' then - newGroupData = mist.getGroupData(gpName) - dbData = true - elseif string.lower(action) == 'clone' then - newGroupData = mist.getGroupData(gpName) - newGroupData.clone = 'order66' - dbData = true - else - action = 'tele' - newGroupData = mist.getCurrentGroupData(gpName) - end - else - action = 'tele' - newGroupData = vars.groupData - end - - --log:info('get Randomized Point') - local diff = {x = 0, y = 0} - local newCoord, origCoord - if point then - local valid = false - - 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 - - for i = 1, 100 do - newCoord = mist.getRandPointInCircle(point, radius, innerRadius) - if mist.isTerrainValid(newCoord, validTerrain) then - origCoord = mist.utils.deepCopy(newCoord) - diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)} - valid = true - break - end - end - if valid == false then - log:error('Point supplied in variable table is not a valid coordinate. Valid coords: $1', validTerrain) - return false - end - end - if not newGroupData.country and mist.DBs.groupsByName[newGroupData.groupName].country then - newGroupData.country = mist.DBs.groupsByName[newGroupData.groupName].country - end - if not newGroupData.category and mist.DBs.groupsByName[newGroupData.groupName].category then - newGroupData.category = mist.DBs.groupsByName[newGroupData.groupName].category - end - - 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) - --else - --newCoord = mist.getRandPointInCircle(zone.point, zone.radius) - end - - newGroupData.units[unitNum].x = newCoord.x - newGroupData.units[unitNum].y = newCoord.y - 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 - newGroupData.units[unitNum].alt = point.y - else - if newGroupData.category == 'plane' then - 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) - end - end - end - end - end - - if newGroupData.start_time then - newGroupData.startTime = newGroupData.start_time - end - - if newGroupData.startTime and newGroupData.startTime ~= 0 and dbData == true then - local timeDif = timer.getAbsTime() - timer.getTime0() - if timeDif > newGroupData.startTime then - newGroupData.startTime = 0 - else - newGroupData.startTime = newGroupData.startTime - timeDif - end - - end - - if route then - newGroupData.route = route - end - --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') - if string.lower(newGroupData.category) == 'static' then - --log:info(newGroupData) - return mist.dynAddStatic(newGroupData) - end - return mist.dynAdd(newGroupData) - - end - - function mist.respawnInZone(gpName, zone, disperse, maxDisp) - - if type(gpName) == 'table' and gpName:getName() then - gpName = gpName:getName() - elseif type(gpName) == 'table' and gpName[1]:getName() then - gpName = math.random(#gpName) - else - gpName = tostring(gpName) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - local vars = {} - vars.gpName = gpName - vars.action = 'respawn' - vars.point = zone.point - vars.radius = zone.radius - vars.disperse = disperse - vars.maxDisp = maxDisp - return mist.teleportToPoint(vars) - end - - function mist.cloneInZone(gpName, zone, disperse, maxDisp) - --log:info('cloneInZone') - if type(gpName) == 'table' then - gpName = gpName:getName() - else - gpName = tostring(gpName) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - local vars = {} - vars.gpName = gpName - vars.action = 'clone' - vars.point = zone.point - vars.radius = zone.radius - vars.disperse = disperse - vars.maxDisp = maxDisp - --log:info('do teleport') - return mist.teleportToPoint(vars) - end - - function mist.teleportInZone(gpName, zone, disperse, maxDisp) -- groupName, zoneName or table of Zone Names, keepForm is a boolean - if type(gpName) == 'table' and gpName:getName() then - gpName = gpName:getName() - else - gpName = tostring(gpName) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - local vars = {} - vars.gpName = gpName - vars.action = 'tele' - vars.point = zone.point - vars.radius = zone.radius - vars.disperse = disperse - vars.maxDisp = maxDisp - return mist.teleportToPoint(vars) - end - - function mist.respawnGroup(gpName, task) - local vars = {} - vars.gpName = gpName - vars.action = 'respawn' - if task and type(task) ~= 'number' then - vars.route = mist.getGroupRoute(gpName, 'task') - end - local newGroup = mist.teleportToPoint(vars) - if task and type(task) == 'number' then - local newRoute = mist.getGroupRoute(gpName, 'task') - mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) - end - return newGroup - end - - function mist.cloneGroup(gpName, task) - local vars = {} - vars.gpName = gpName - vars.action = 'clone' - if task and type(task) ~= 'number' then - vars.route = mist.getGroupRoute(gpName, 'task') - end - local newGroup = mist.teleportToPoint(vars) - if task and type(task) == 'number' then - local newRoute = mist.getGroupRoute(gpName, 'task') - mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) - end - return newGroup - end - - function mist.teleportGroup(gpName, task) - local vars = {} - vars.gpName = gpName - vars.action = 'teleport' - if task and type(task) ~= 'number' then - vars.route = mist.getGroupRoute(gpName, 'task') - end - local newGroup = mist.teleportToPoint(vars) - if task and type(task) == 'number' then - local newRoute = mist.getGroupRoute(gpName, 'task') - mist.scheduleFunction(mist.goRoute, {newGroup, newRoute}, timer.getTime() + task) - end - return newGroup - end - - function mist.spawnRandomizedGroup(groupName, vars) -- need to debug - if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then - local gpData = mist.getGroupData(groupName) - gpData.units = mist.randomizeGroupOrder(gpData.units, vars) - gpData.route = mist.getGroupRoute(groupName, 'task') - - mist.dynAdd(gpData) - end - - return true - end - - function mist.randomizeNumTable(vars) - local newTable = {} - - local excludeIndex = {} - local randomTable = {} - - if vars and vars.exclude and type(vars.exclude) == 'table' then - for index, data in pairs(vars.exclude) do - excludeIndex[data] = true - end - end - - local low, hi, size - - if vars.size then - size = vars.size - end - - if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then - low = mist.utils.round(vars.lowerLimit) - else - low = 1 - end - - if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then - hi = mist.utils.round(vars.upperLimit) - else - hi = size - end - - local choices = {} - -- add to exclude list and create list of what to randomize - for i = 1, size do - if not (i >= low and i <= hi) then - - excludeIndex[i] = true - end - if not excludeIndex[i] then - table.insert(choices, i) - else - newTable[i] = i - end - end - - for ind, num in pairs(choices) do - local found = false - local x = 0 - while found == false do - x = mist.random(size) -- get random number from list - local addNew = true - for index, _ in pairs(excludeIndex) do - if index == x then - addNew = false - break - end - end - if addNew == true then - excludeIndex[x] = true - found = true - end - excludeIndex[x] = true - - end - newTable[num] = x - end - --[[ - for i = 1, #newTable do - log:info(newTable[i]) - end - ]] - return newTable - end - - function mist.randomizeGroupOrder(passedUnits, vars) - -- figure out what to exclude, and send data to other func - local units = passedUnits - - if passedUnits.units then - units = passUnits.units - end - - local exclude = {} - local excludeNum = {} - if vars and vars.excludeType and type(vars.excludeType) == 'table' then - exclude = vars.excludeType - end - - if vars and vars.excludeNum and type(vars.excludeNum) == 'table' then - excludeNum = vars.excludeNum - end - - local low, hi - - if vars and vars.lowerLimit and type(vars.lowerLimit) == 'number' then - low = mist.utils.round(vars.lowerLimit) - else - low = 1 - end - - if vars and vars.upperLimit and type(vars.upperLimit) == 'number' then - hi = mist.utils.round(vars.upperLimit) - else - hi = #units - end - - - local excludeNum = {} - for unitIndex, unitData in pairs(units) do - if unitIndex >= low and unitIndex <= hi then -- if within range - local found = false - if #exclude > 0 then - for excludeType, index in pairs(exclude) do -- check if excluded - if mist.stringMatch(excludeType, unitData.type) then -- if excluded - excludeNum[unitIndex] = unitIndex - found = true - end - end - end - else -- unitIndex is either to low, or to high: added to exclude list - excludeNum[unitIndex] = unitId - end - end - - local newGroup = {} - local newOrder = mist.randomizeNumTable({exclude = excludeNum, size = #units}) - - for unitIndex, unitData in pairs(units) do - for i = 1, #newOrder do - if newOrder[i] == unitIndex then - newGroup[i] = mist.utils.deepCopy(units[i]) -- gets all of the unit data - newGroup[i].type = mist.utils.deepCopy(unitData.type) - newGroup[i].skill = mist.utils.deepCopy(unitData.skill) - newGroup[i].unitName = mist.utils.deepCopy(unitData.unitName) - newGroup[i].unitIndex = mist.utils.deepCopy(unitData.unitIndex) -- replaces the units data with a new type - end - end - end - return newGroup - end - - function mist.random(firstNum, secondNum) -- no support for decimals - local lowNum, highNum - if not secondNum then - highNum = firstNum - lowNum = 1 - else - lowNum = firstNum - highNum = secondNum - end - local total = 1 - if math.abs(highNum - lowNum + 1) < 50 then -- if total values is less than 50 - total = math.modf(50/math.abs(highNum - lowNum + 1)) -- make x copies required to be above 50 - end - local choices = {} - for i = 1, total do -- iterate required number of times - for x = lowNum, highNum do -- iterate between the range - choices[#choices +1] = x -- add each entry to a table - end - end - local rtnVal = math.random(#choices) -- will now do a math.random of at least 50 choices - for i = 1, 10 do - rtnVal = math.random(#choices) -- iterate a few times for giggles - end - return choices[rtnVal] - end - - function mist.stringMatch(s1, s2, bool) - local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} - if type(s1) == 'string' and type(s2) == 'string' then - for i , str in pairs(exclude) do - s1 = string.gsub(s1, str, '') - s2 = string.gsub(s2, str, '') - end - if not bool then - s1 = string.lower(s1) - s2 = string.lower(s2) - end - log:info('Comparing: $1 and $2', s1, s2) - if s1 == s2 then - return true - else - return false - end - else - log:error('Either the first or second variable were not a string') - return false - end - end - - mist.matchString = mist.stringMatch -- both commands work because order out type of I - - --[[ scope: -{ - units = {...}, -- unit names. - coa = {...}, -- coa names - countries = {...}, -- country names - CA = {...}, -- looks just like coa. - unitTypes = { red = {}, blue = {}, all = {}, Russia = {},} -} - - -scope examples: - -{ units = { 'Hawg11', 'Hawg12' }, CA = {'blue'} } - -{ countries = {'Georgia'}, unitTypes = {blue = {'A-10C', 'A-10A'}}} - -{ coa = {'all'}} - -{unitTypes = { blue = {'A-10C'}}} -]] -end - ---- Utility functions. --- E.g. conversions between units etc. --- @section mist.utils -do -- mist.util scope - mist.utils = {} - - --- Converts angle in radians to degrees. - -- @param angle angle in radians - -- @return angle in degrees - function mist.utils.toDegree(angle) - return angle*180/math.pi - end - - --- Converts angle in degrees to radians. - -- @param angle angle in degrees - -- @return angle in degrees - function mist.utils.toRadian(angle) - return angle*math.pi/180 - end - - --- Converts meters to nautical miles. - -- @param meters distance in meters - -- @return distance in nautical miles - function mist.utils.metersToNM(meters) - return meters/1852 - end - - --- Converts meters to feet. - -- @param meters distance in meters - -- @return distance in feet - function mist.utils.metersToFeet(meters) - return meters/0.3048 - end - - --- Converts nautical miles to meters. - -- @param nm distance in nautical miles - -- @return distance in meters - function mist.utils.NMToMeters(nm) - return nm*1852 - end - - --- Converts feet to meters. - -- @param feet distance in feet - -- @return distance in meters - function mist.utils.feetToMeters(feet) - return feet*0.3048 - end - - --- Converts meters per second to knots. - -- @param mps speed in m/s - -- @return speed in knots - function mist.utils.mpsToKnots(mps) - return mps*3600/1852 - end - - --- Converts meters per second to kilometers per hour. - -- @param mps speed in m/s - -- @return speed in km/h - function mist.utils.mpsToKmph(mps) - return mps*3.6 - end - - --- Converts knots to meters per second. - -- @param knots speed in knots - -- @return speed in m/s - function mist.utils.knotsToMps(knots) - return knots*1852/3600 - end - - --- Converts kilometers per hour to meters per second. - -- @param kmph speed in km/h - -- @return speed in m/s - function mist.utils.kmphToMps(kmph) - return kmph/3.6 - end - - --- Converts a Vec3 to a Vec2. - -- @tparam Vec3 vec the 3D vector - -- @return vector converted to Vec2 - function mist.utils.makeVec2(vec) - if vec.z then - return {x = vec.x, y = vec.z} - else - return {x = vec.x, y = vec.y} -- it was actually already vec2. - end - end - - --- Converts a Vec2 to a Vec3. - -- @tparam Vec2 vec the 2D vector - -- @param y optional new y axis (altitude) value. If omitted it's 0. - function mist.utils.makeVec3(vec, y) - if not vec.z then - if vec.alt and not y then - y = vec.alt - elseif not y then - y = 0 - end - return {x = vec.x, y = y, z = vec.y} - else - return {x = vec.x, y = vec.y, z = vec.z} -- it was already Vec3, actually. - end - end - - --- Converts a Vec2 to a Vec3 using ground level as altitude. - -- The ground level at the specific point is used as altitude (y-axis) - -- for the new vector. Optionally a offset can be specified. - -- @tparam Vec2 vec the 2D vector - -- @param[opt] offset offset to be applied to the ground level - -- @return new 3D vector - function mist.utils.makeVec3GL(vec, offset) - local adj = offset or 0 - - if not vec.z then - return {x = vec.x, y = (land.getHeight(vec) + adj), z = vec.y} - else - return {x = vec.x, y = (land.getHeight({x = vec.x, y = vec.z}) + adj), z = vec.z} - end - end - - --- Returns the center of a zone as Vec3. - -- @tparam string|table zone trigger zone name or table - -- @treturn Vec3 center of the zone - function mist.utils.zoneToVec3(zone) - local new = {} - if type(zone) == 'table' then - if zone.point then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - elseif zone.x and zone.y and zone.z then - return zone - end - return new - elseif type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - if zone then - new.x = zone.point.x - new.y = zone.point.y - new.z = zone.point.z - return new - end - end - end - - --- Returns heading-error corrected direction. - -- True-north corrected direction from point along vector vec. - -- @tparam Vec3 vec - -- @tparam Vec2 point - -- @return heading-error corrected direction from point. - function mist.utils.getDir(vec, point) - local dir = math.atan2(vec.z, vec.x) - if point then - dir = dir + mist.getNorthCorrection(point) - end - if dir < 0 then - dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi - end - return dir - end - - --- Returns distance in meters between two points. - -- @tparam Vec2|Vec3 point1 first point - -- @tparam Vec2|Vec3 point2 second point - -- @treturn number distance between given points. - function mist.utils.get2DDist(point1, point2) - point1 = mist.utils.makeVec3(point1) - point2 = mist.utils.makeVec3(point2) - return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) - end - - --- Returns distance in meters between two points in 3D space. - -- @tparam Vec3 point1 first point - -- @tparam Vec3 point2 second point - -- @treturn number distancen between given points in 3D space. - function mist.utils.get3DDist(point1, point2) - return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) - end - - --- Creates a waypoint from a vector. - -- @tparam Vec2|Vec3 vec position of the new waypoint - -- @treturn Waypoint a new waypoint to be used inside paths. - function mist.utils.vecToWP(vec) - local newWP = {} - newWP.x = vec.x - newWP.y = vec.y - if vec.z then - newWP.alt = vec.y - newWP.y = vec.z - else - newWP.alt = land.getHeight({x = vec.x, y = vec.y}) - end - return newWP - end - - --- Creates a waypoint from a unit. - -- This function also considers the units speed. - -- The alt_type of this waypoint is set to "BARO". - -- @tparam Unit pUnit Unit whose position and speed will be used. - -- @treturn Waypoint new waypoint. - function mist.utils.unitToWP(pUnit) - local unit = mist.utils.deepCopy(pUnit) - if type(unit) == 'string' then - if Unit.getByName(unit) then - unit = Unit.getByName(unit) - end - end - if unit:isExist() == true then - local new = mist.utils.vecToWP(unit:getPosition().p) - new.speed = mist.vec.mag(unit:getVelocity()) - new.alt_type = "BARO" - - return new - end - log:error("$1 not found or doesn't exist", pUnit) - return false - end - - --- Creates a deep copy of a object. - -- Usually this object is a table. - -- See also: from http://lua-users.org/wiki/CopyTable - -- @param object object to copy - -- @return copy of object - function mist.utils.deepCopy(object) - local lookup_table = {} - local function _copy(object) - if type(object) ~= "table" then - return object - elseif lookup_table[object] then - return lookup_table[object] - end - local new_table = {} - lookup_table[object] = new_table - for index, value in pairs(object) do - new_table[_copy(index)] = _copy(value) - end - return setmetatable(new_table, getmetatable(object)) - end - return _copy(object) - end - - --- Simple rounding function. - -- From http://lua-users.org/wiki/SimpleRound - -- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place - -- @tparam number num number to round - -- @param idp - function mist.utils.round(num, idp) - local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult - end - - --- Rounds all numbers inside a table. - -- @tparam table tbl table in which to round numbers - -- @param idp - function mist.utils.roundTbl(tbl, idp) - for id, val in pairs(tbl) do - if type(val) == 'number' then - tbl[id] = mist.utils.round(val, idp) - end - end - return tbl - end - - --- Executes the given string. - -- borrowed from Slmod - -- @tparam string s string containing LUA code. - -- @treturn boolean true if successfully executed, false otherwise - function mist.utils.dostring(s) - local f, err = loadstring(s) - if f then - return true, f() - else - return false, err - end - end - - --- Checks a table's types. - -- This function checks a tables types against a specifically forged type table. - -- @param fname - -- @tparam table type_tbl - -- @tparam table var_tbl - -- @usage -- specifically forged type table - -- type_tbl = { - -- {'table', 'number'}, - -- 'string', - -- 'number', - -- 'number', - -- {'string','nil'}, - -- {'number', 'nil'} - -- } - -- -- my_tbl index 1 must be a table or a number; - -- -- index 2, a string; index 3, a number; - -- -- index 4, a number; index 5, either a string or nil; - -- -- and index 6, either a number or nil. - -- mist.utils.typeCheck(type_tbl, my_tb) - -- @return true if table passes the check, false otherwise. - function mist.utils.typeCheck(fname, type_tbl, var_tbl) - -- log:info('type check') - for type_key, type_val in pairs(type_tbl) do - -- log:info('type_key: $1 type_val: $2', type_key, type_val) - - --type_key can be a table of accepted keys- so try to find one that is not nil - local type_key_str = '' - local act_key = type_key -- actual key within var_tbl - necessary to use for multiple possible key variables. Initialize to type_key - if type(type_key) == 'table' then - - for i = 1, #type_key do - if i ~= 1 then - type_key_str = type_key_str .. '/' - end - type_key_str = type_key_str .. tostring(type_key[i]) - if var_tbl[type_key[i]] ~= nil then - act_key = type_key[i] -- found a non-nil entry, make act_key now this val. - end - end - else - type_key_str = tostring(type_key) - end - - local err_msg = 'Error in function ' .. fname .. ', parameter "' .. type_key_str .. '", expected: ' - local passed_check = false - - if type(type_tbl[type_key]) == 'table' then - -- log:info('err_msg, before: $1', err_msg) - for j = 1, #type_tbl[type_key] do - - if j == 1 then - err_msg = err_msg .. type_tbl[type_key][j] - else - err_msg = err_msg .. ' or ' .. type_tbl[type_key][j] - end - - if type(var_tbl[act_key]) == type_tbl[type_key][j] then - passed_check = true - end - end - -- log:info('err_msg, after: $1', err_msg) - else - -- log:info('err_msg, before: $1', err_msg) - err_msg = err_msg .. type_tbl[type_key] - -- log:info('err_msg, after: $1', err_msg) - if type(var_tbl[act_key]) == type_tbl[type_key] then - passed_check = true - end - - end - - if not passed_check then - err_msg = err_msg .. ', got ' .. type(var_tbl[act_key]) - return false, err_msg - end - end - return true - end - - --- Serializes the give variable to a string. - -- borrowed from slmod - -- @param var variable to serialize - -- @treturn string variable serialized to string - function mist.utils.basicSerialize(var) - if var == nil then - return "\"\"" - else - if ((type(var) == 'number') or - (type(var) == 'boolean') or - (type(var) == 'function') or - (type(var) == 'table') or - (type(var) == 'userdata') ) then - return tostring(var) - elseif type(var) == 'string' then - var = string.format('%q', var) - return var - end - end -end - ---- Serialize value --- borrowed from slmod (serialize_slmod) --- @param name --- @param value value to serialize --- @param level -function mist.utils.serialize(name, value, level) - --Based on ED's serialize_simple2 - local function basicSerialize(o) - if type(o) == "number" then - return tostring(o) - elseif type(o) == "boolean" then - return tostring(o) - else -- assume it is a string - return mist.utils.basicSerialize(o) - end - end - - local function serializeToTbl(name, value, level) - local var_str_tbl = {} - 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 - table.insert(var_str_tbl, basicSerialize(value) .. ",\n") - elseif type(value) == "table" then - table.insert(var_str_tbl, "\n"..level.."{\n") - - for k,v in pairs(value) do -- serialize its fields - local key - if type(k) == "number" then - key = string.format("[%s]", k) - else - key = string.format("[%q]", k) - end - - table.insert(var_str_tbl, mist.utils.serialize(key, v, level.." ")) - - end - if level == "" then - table.insert(var_str_tbl, level.."} -- end of "..name.."\n") - - else - table.insert(var_str_tbl, level.."}, -- end of "..name.."\n") - - end - else - log:error('Cannot serialize a $1', type(value)) - end - return var_str_tbl - end - - local t_str = serializeToTbl(name, value, level) - - return table.concat(t_str) -end - ---- Serialize value supporting cycles. --- borrowed from slmod (serialize_wcycles) --- @param name --- @param value value to serialize --- @param saved -function mist.utils.serializeWithCycles(name, value, saved) - --mostly straight out of Programming in Lua - local function basicSerialize(o) - if type(o) == "number" then - return tostring(o) - elseif type(o) == "boolean" then - return tostring(o) - else -- assume it is a string - return mist.utils.basicSerialize(o) - end - end - - local t_str = {} - saved = saved or {} -- initial value - if ((type(value) == 'string') or (type(value) == 'number') or (type(value) == 'table') or (type(value) == 'boolean')) then - table.insert(t_str, name .. " = ") - if type(value) == "number" or type(value) == "string" or type(value) == "boolean" then - table.insert(t_str, basicSerialize(value) .. "\n") - else - - if saved[value] then -- value already saved? - table.insert(t_str, saved[value] .. "\n") - else - saved[value] = name -- save name for next time - table.insert(t_str, "{}\n") - for k,v in pairs(value) do -- save its fields - local fieldname = string.format("%s[%s]", name, basicSerialize(k)) - table.insert(t_str, mist.utils.serializeWithCycles(fieldname, v, saved)) - end - end - end - return table.concat(t_str) - else - return "" - end -end - ---- Serialize a table to a single line string. --- serialization of a table all on a single line, no comments, made to replace old get_table_string function --- borrowed from slmod --- @tparam table tbl table to serialize. --- @treturn string string containing serialized table -function mist.utils.oneLineSerialize(tbl) - if type(tbl) == 'table' then --function only works for tables! - - local tbl_str = {} - - tbl_str[#tbl_str + 1] = '{ ' - - for ind,val in pairs(tbl) do -- serialize its fields - if type(ind) == "number" then - tbl_str[#tbl_str + 1] = '[' - tbl_str[#tbl_str + 1] = tostring(ind) - tbl_str[#tbl_str + 1] = '] = ' - else --must be a string - tbl_str[#tbl_str + 1] = '[' - tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind) - tbl_str[#tbl_str + 1] = '] = ' - end - - if ((type(val) == 'number') or (type(val) == 'boolean')) then - tbl_str[#tbl_str + 1] = tostring(val) - tbl_str[#tbl_str + 1] = ', ' - elseif type(val) == 'string' then - tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val) - tbl_str[#tbl_str + 1] = ', ' - elseif type(val) == 'nil' then -- won't ever happen, right? - tbl_str[#tbl_str + 1] = 'nil, ' - elseif type(val) == 'table' then - 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)) - end - - end - tbl_str[#tbl_str + 1] = '}' - return table.concat(tbl_str) - end -end - ---- Returns table in a easy readable string representation. --- this function is not meant for serialization because it uses --- newlines for better readability. --- @param tbl table to show --- @param loc --- @param indent --- @param tableshow_tbls --- @return human readable string representation of given table -function mist.utils.tableShow(tbl, loc, indent, tableshow_tbls) --based on serialize_slmod, this is a _G serialization - tableshow_tbls = tableshow_tbls or {} --create table of tables - loc = loc or "" - indent = indent or "" - if type(tbl) == 'table' then --function only works for tables! - tableshow_tbls[tbl] = loc - - local tbl_str = {} - - tbl_str[#tbl_str + 1] = indent .. '{\n' - - for ind,val in pairs(tbl) do -- serialize its fields - if type(ind) == "number" then - tbl_str[#tbl_str + 1] = indent - tbl_str[#tbl_str + 1] = loc .. '[' - tbl_str[#tbl_str + 1] = tostring(ind) - tbl_str[#tbl_str + 1] = '] = ' - else - tbl_str[#tbl_str + 1] = indent - tbl_str[#tbl_str + 1] = loc .. '[' - tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(ind) - tbl_str[#tbl_str + 1] = '] = ' - end - - if ((type(val) == 'number') or (type(val) == 'boolean')) then - tbl_str[#tbl_str + 1] = tostring(val) - tbl_str[#tbl_str + 1] = ',\n' - elseif type(val) == 'string' then - tbl_str[#tbl_str + 1] = mist.utils.basicSerialize(val) - tbl_str[#tbl_str + 1] = ',\n' - elseif type(val) == 'nil' then -- won't ever happen, right? - tbl_str[#tbl_str + 1] = 'nil,\n' - elseif type(val) == 'table' then - if tableshow_tbls[val] then - tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n' - 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] = ',\n' - end - elseif type(val) == 'function' then - if debug and debug.getinfo then - local fcnname = tostring(val) - local info = debug.getinfo(val, "S") - if info.what == "C" then - tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n' - else - if (string.sub(info.source, 1, 2) == [[./]]) then - tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n' - else - tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n' - end - end - - else - tbl_str[#tbl_str + 1] = 'a function,\n' - end - else - tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. mist.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind) - end - end - - tbl_str[#tbl_str + 1] = indent .. '}' - return table.concat(tbl_str) - end -end -end - ---- Debug functions --- @section mist.debug -do -- mist.debug scope - mist.debug = {} - - --- Dumps the global table _G. - -- This dumps the global table _G to a file in - -- the DCS\Logs directory. - -- This function requires you to disable script sanitization - -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io - -- libraries. - -- @param fname - function mist.debug.dump_G(fname) - if lfs and io then - local fdir = lfs.writedir() .. [[Logs\]] .. fname - local f = io.open(fdir, 'w') - f:write(mist.utils.tableShow(_G)) - f:close() - log:info('Wrote debug data to $1', fdir) - --trigger.action.outText(errmsg, 10) - else - log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua') - --trigger.action.outText(errmsg, 10) - end - end - - --- Write debug data to file. - -- This function requires you to disable script sanitization - -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io - -- libraries. - -- @param fcn - -- @param fcnVars - -- @param fname - function mist.debug.writeData(fcn, fcnVars, fname) - if lfs and io then - local fdir = lfs.writedir() .. [[Logs\]] .. fname - local f = io.open(fdir, 'w') - f:write(fcn(unpack(fcnVars, 1, table.maxn(fcnVars)))) - f:close() - log:info('Wrote debug data to $1', fdir) - local errmsg = 'mist.debug.writeData wrote data to ' .. fdir - trigger.action.outText(errmsg, 10) - else - local errmsg = 'Error: insufficient libraries to run mist.debug.writeData, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua' - log:alert(errmsg) - trigger.action.outText(errmsg, 10) - end - end - - --- Write mist databases to file. - -- This function requires you to disable script sanitization - -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io - -- libraries. - function mist.debug.dumpDBs() - for DBname, DB in pairs(mist.DBs) do - if type(DB) == 'table' and type(DBname) == 'string' then - mist.debug.writeData(mist.utils.serialize, {DBname, DB}, 'mist_DBs_' .. DBname .. '.lua') - end - end - end -end - ---- 3D Vector functions --- @section mist.vec -do -- mist.vec scope - mist.vec = {} - - --- Vector addition. - -- @tparam Vec3 vec1 first vector - -- @tparam Vec3 vec2 second vector - -- @treturn Vec3 new vector, sum of vec1 and vec2. - function mist.vec.add(vec1, vec2) - return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z} - end - - --- Vector substraction. - -- @tparam Vec3 vec1 first vector - -- @tparam Vec3 vec2 second vector - -- @treturn Vec3 new vector, vec2 substracted from vec1. - function mist.vec.sub(vec1, vec2) - return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z} - end - - --- Vector scalar multiplication. - -- @tparam Vec3 vec vector to multiply - -- @tparam number mult scalar multiplicator - -- @treturn Vec3 new vector multiplied with the given scalar - function mist.vec.scalarMult(vec, mult) - return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult} - end - - mist.vec.scalar_mult = mist.vec.scalarMult - - --- Vector dot product. - -- @tparam Vec3 vec1 first vector - -- @tparam Vec3 vec2 second vector - -- @treturn number dot product of given vectors - function mist.vec.dp (vec1, vec2) - return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z - end - - --- Vector cross product. - -- @tparam Vec3 vec1 first vector - -- @tparam Vec3 vec2 second vector - -- @treturn Vec3 new vector, cross product of vec1 and vec2. - function mist.vec.cp(vec1, vec2) - return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x} - end - - --- Vector magnitude - -- @tparam Vec3 vec vector - -- @treturn number magnitude of vector vec - function mist.vec.mag(vec) - return (vec.x^2 + vec.y^2 + vec.z^2)^0.5 - end - - --- Unit vector - -- @tparam Vec3 vec - -- @treturn Vec3 unit vector of vec - function mist.vec.getUnitVec(vec) - local mag = mist.vec.mag(vec) - return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag } - end - - --- Rotate vector. - -- @tparam Vec2 vec2 to rotoate - -- @tparam number theta - -- @return Vec2 rotated vector. - function mist.vec.rotateVec2(vec2, theta) - return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} - end -end - ---- Flag functions. --- The mist "Flag functions" are functions that are similar to Slmod functions --- that detect a game condition and set a flag when that game condition is met. --- --- They are intended to be used by persons with little or no experience in Lua --- programming, but with a good knowledge of the DCS mission editor. --- @section mist.flagFunc -do -- mist.flagFunc scope - mist.flagFunc = {} - - --- Sets a flag if map objects are destroyed inside a zone. - -- Once this function is run, it will start a continuously evaluated process - -- that will set a flag true if map objects (such as bridges, buildings in - -- town, etc.) die (or have died) in a mission editor zone (or set of zones). - -- This will only happen once; once the flag is set true, the process ends. - -- @usage - -- -- Example vars table - -- vars = { - -- zones = { "zone1", "zone2" }, -- can also be a single string - -- flag = 3, -- number of the flag - -- stopflag = 4, -- optional number of the stop flag - -- req_num = 10, -- optional minimum amount of map objects needed to die - -- } - -- mist.flagFuncs.mapobjs_dead_zones(vars) - -- @tparam table vars table containing parameters. - function mist.flagFunc.mapobjs_dead_zones(vars) - --[[vars needs to be: -zones = table or string, -flag = number, -stopflag = number or nil, -req_num = number or nil - -AND used by function, -initial_number - -]] - -- type_tbl - local type_tbl = { - [{'zones', 'zone'}] = {'table', 'string'}, - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_zones', type_tbl, vars) - assert(err, errmsg) - local zones = vars.zones or vars.zone - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local req_num = vars.req_num or vars.reqnum or 1 - local initial_number = vars.initial_number - - if type(zones) == 'string' then - zones = {zones} - end - - if not initial_number then - initial_number = #mist.getDeadMapObjsInZones(zones) - end - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if (#mist.getDeadMapObjsInZones(zones) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - return - else - mist.scheduleFunction(mist.flagFunc.mapobjs_dead_zones, {{zones = zones, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) - end - end - end - - --- Sets a flag if map objects are destroyed inside a polygon. - -- Once this function is run, it will start a continuously evaluated process - -- that will set a flag true if map objects (such as bridges, buildings in - -- town, etc.) die (or have died) in a polygon. - -- This will only happen once; once the flag is set true, the process ends. - -- @usage - -- -- Example vars table - -- vars = { - -- zone = { - -- [1] = mist.DBs.unitsByName['NE corner'].point, - -- [2] = mist.DBs.unitsByName['SE corner'].point, - -- [3] = mist.DBs.unitsByName['SW corner'].point, - -- [4] = mist.DBs.unitsByName['NW corner'].point - -- } - -- flag = 3, -- number of the flag - -- stopflag = 4, -- optional number of the stop flag - -- req_num = 10, -- optional minimum amount of map objects needed to die - -- } - -- mist.flagFuncs.mapobjs_dead_zones(vars) - -- @tparam table vars table containing parameters. - function mist.flagFunc.mapobjs_dead_polygon(vars) - --[[vars needs to be: -zone = table, -flag = number, -stopflag = number or nil, -req_num = number or nil - -AND used by function, -initial_number - -]] - -- type_tbl - local type_tbl = { - [{'zone', 'polyzone'}] = 'table', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.mapobjs_dead_polygon', type_tbl, vars) - assert(err, errmsg) - local zone = vars.zone or vars.polyzone - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local req_num = vars.req_num or vars.reqnum or 1 - local initial_number = vars.initial_number - - if not initial_number then - initial_number = #mist.getDeadMapObjsInPolygonZone(zone) - end - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if (#mist.getDeadMapObjsInPolygonZone(zone) - initial_number) >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - return - else - mist.scheduleFunction(mist.flagFunc.mapobjs_dead_polygon, {{zone = zone, flag = flag, stopflag = stopflag, req_num = req_num, initial_number = initial_number}}, timer.getTime() + 1) - end - end - end - - --- Sets a flag if unit(s) is/are inside a polygon. - -- @tparam table vars @{unitsInPolygonVars} - -- @usage -- set flag 11 to true as soon as any blue vehicles - -- -- are inside the polygon shape created off of the waypoints - -- -- of the group forest1 - -- mist.flagFunc.units_in_polygon { - -- units = {'[blue][vehicle]'}, - -- zone = mist.getGroupPoints('forest1'), - -- flag = 11 - -- } - function mist.flagFunc.units_in_polygon(vars) - --[[vars needs to be: -units = table, -zone = table, -flag = number, -stopflag = number or nil, -maxalt = number or nil, -interval = number or nil, -req_num = number or nil -toggle = boolean or nil -unitTableDef = table or nil -]] - -- type_tbl - local type_tbl = { - [{'units', 'unit'}] = 'table', - [{'zone', 'polyzone'}] = 'table', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'maxalt', 'alt'}] = {'number', 'nil'}, - interval = {'number', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - unitTableDef = {'table', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_polygon', type_tbl, vars) - assert(err, errmsg) - local units = vars.units or vars.unit - local zone = vars.zone or vars.polyzone - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local maxalt = vars.maxalt or vars.alt - local req_num = vars.req_num or vars.reqnum or 1 - local toggle = vars.toggle or nil - local unitTableDef = vars.unitTableDef - - if not units.processed then - unitTableDef = mist.utils.deepCopy(units) - end - - if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts - if unitTableDef then - units = mist.makeUnitTable(unitTableDef) - end - end - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then - local num_in_zone = 0 - for i = 1, #units do - local unit = Unit.getByName(units[i]) - if unit then - local pos = unit:getPosition().p - if mist.pointInPolygon(pos, zone, maxalt) then - num_in_zone = num_in_zone + 1 - if num_in_zone >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - break - end - end - end - end - if toggle and (num_in_zone < req_num) and trigger.misc.getUserFlag(flag) > 0 then - trigger.action.setUserFlag(flag, false) - end - -- do another check in case stopflag was set true by this function - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then - mist.scheduleFunction(mist.flagFunc.units_in_polygon, {{units = units, zone = zone, flag = flag, stopflag = stopflag, interval = interval, req_num = req_num, maxalt = maxalt, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) - end - end - - end - - --- Sets a flag if unit(s) is/are inside a trigger zone. - -- @todo document - function mist.flagFunc.units_in_zones(vars) - --[[vars needs to be: - units = table, - zones = table, - flag = number, - stopflag = number or nil, - zone_type = string or nil, - req_num = number or nil, - interval = number or nil - toggle = boolean or nil - ]] - -- type_tbl - local type_tbl = { - units = 'table', - zones = 'table', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'zone_type', 'zonetype'}] = {'string', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - unitTableDef = {'table', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_zones', type_tbl, vars) - assert(err, errmsg) - local units = vars.units - local zones = vars.zones - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local zone_type = vars.zone_type or vars.zonetype or 'cylinder' - local req_num = vars.req_num or vars.reqnum or 1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - local unitTableDef = vars.unitTableDef - - if not units.processed then - unitTableDef = mist.utils.deepCopy(units) - end - - if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts - if unitTableDef then - units = mist.makeUnitTable(unitTableDef) - end - end - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - - local in_zone_units = mist.getUnitsInZones(units, zones, zone_type) - - if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - elseif #in_zone_units < req_num and toggle then - trigger.action.setUserFlag(flag, false) - end - -- do another check in case stopflag was set true by this function - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.units_in_zones, {{units = units, zones = zones, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef}}, timer.getTime() + interval) - end - end - - end - - --- Sets a flag if unit(s) is/are inside a moving zone. - -- @todo document - function mist.flagFunc.units_in_moving_zones(vars) - --[[vars needs to be: - units = table, - zone_units = table, - radius = number, - flag = number, - stopflag = number or nil, - zone_type = string or nil, - req_num = number or nil, - interval = number or nil - toggle = boolean or nil - ]] - -- type_tbl - local type_tbl = { - units = 'table', - [{'zone_units', 'zoneunits'}] = 'table', - radius = 'number', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'zone_type', 'zonetype'}] = {'string', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - unitTableDef = {'table', 'nil'}, - zUnitTableDef = {'table', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_in_moving_zones', type_tbl, vars) - assert(err, errmsg) - local units = vars.units - local zone_units = vars.zone_units or vars.zoneunits - local radius = vars.radius - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local zone_type = vars.zone_type or vars.zonetype or 'cylinder' - local req_num = vars.req_num or vars.reqnum or 1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - local unitTableDef = vars.unitTableDef - local zUnitTableDef = vars.zUnitTableDef - - if not units.processed then - unitTableDef = mist.utils.deepCopy(units) - end - - if not zone_units.processed then - zUnitTableDef = mist.utils.deepCopy(zone_units) - end - - if (units.processed and units.processed < mist.getLastDBUpdateTime()) or not units.processed then -- run unit table short cuts - if unitTableDef then - units = mist.makeUnitTable(unitTableDef) - end - end - - if (zone_units.processed and zone_units.processed < mist.getLastDBUpdateTime()) or not zone_units.processed then -- run unit table short cuts - if zUnitTableDef then - zone_units = mist.makeUnitTable(zUnitTableDef) - end - - end - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - - local in_zone_units = mist.getUnitsInMovingZones(units, zone_units, radius, zone_type) - - if #in_zone_units >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - elseif #in_zone_units < req_num and toggle then - trigger.action.setUserFlag(flag, false) - end - -- do another check in case stopflag was set true by this function - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.units_in_moving_zones, {{units = units, zone_units = zone_units, radius = radius, flag = flag, stopflag = stopflag, zone_type = zone_type, req_num = req_num, interval = interval, toggle = toggle, unitTableDef = unitTableDef, zUnitTableDef = zUnitTableDef}}, timer.getTime() + interval) - end - end - - end - - --- Sets a flag if units have line of sight to each other. - -- @todo document - function mist.flagFunc.units_LOS(vars) - --[[vars needs to be: -unitset1 = table, -altoffset1 = number, -unitset2 = table, -altoffset2 = number, -flag = number, -stopflag = number or nil, -radius = number or nil, -interval = number or nil, -req_num = number or nil -toggle = boolean or nil -]] - -- type_tbl - local type_tbl = { - [{'unitset1', 'units1'}] = 'table', - [{'altoffset1', 'alt1'}] = 'number', - [{'unitset2', 'units2'}] = 'table', - [{'altoffset2', 'alt2'}] = 'number', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - [{'req_num', 'reqnum'}] = {'number', 'nil'}, - interval = {'number', 'nil'}, - radius = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - unitTableDef1 = {'table', 'nil'}, - unitTableDef2 = {'table', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.units_LOS', type_tbl, vars) - assert(err, errmsg) - local unitset1 = vars.unitset1 or vars.units1 - local altoffset1 = vars.altoffset1 or vars.alt1 - local unitset2 = vars.unitset2 or vars.units2 - local altoffset2 = vars.altoffset2 or vars.alt2 - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local radius = vars.radius or math.huge - local req_num = vars.req_num or vars.reqnum or 1 - local toggle = vars.toggle or nil - local unitTableDef1 = vars.unitTableDef1 - local unitTableDef2 = vars.unitTableDef2 - - if not unitset1.processed then - unitTableDef1 = mist.utils.deepCopy(unitset1) - end - - if not unitset2.processed then - unitTableDef2 = mist.utils.deepCopy(unitset2) - end - - if (unitset1.processed and unitset1.processed < mist.getLastDBUpdateTime()) or not unitset1.processed then -- run unit table short cuts - if unitTableDef1 then - unitset1 = mist.makeUnitTable(unitTableDef1) - end - end - - if (unitset2.processed and unitset2.processed < mist.getLastDBUpdateTime()) or not unitset2.processed then -- run unit table short cuts - if unitTableDef2 then - unitset2 = mist.makeUnitTable(unitTableDef2) - end - end - - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - - local unitLOSdata = mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) - - if #unitLOSdata >= req_num and trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - elseif #unitLOSdata < req_num and toggle then - trigger.action.setUserFlag(flag, false) - end - -- do another check in case stopflag was set true by this function - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.units_LOS, {{unitset1 = unitset1, altoffset1 = altoffset1, unitset2 = unitset2, altoffset2 = altoffset2, flag = flag, stopflag = stopflag, radius = radius, req_num = req_num, interval = interval, toggle = toggle, unitTableDef1 = unitTableDef1, unitTableDef2 = unitTableDef2}}, timer.getTime() + interval) - end - end - end - - --- Sets a flag if group is alive. - -- @todo document - function mist.flagFunc.group_alive(vars) - --[[vars -groupName -flag -toggle -interval -stopFlag - -]] - local type_tbl = { - [{'group', 'groupname', 'gp', 'groupName'}] = 'string', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive', type_tbl, vars) - assert(err, errmsg) - - local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true and #Group.getByName(groupName):getUnits() > 0 then - if trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - end - else - if toggle then - trigger.action.setUserFlag(flag, false) - end - end - end - - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.group_alive, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) - end - - end - - --- Sets a flag if group is dead. - -- @todo document - function mist.flagFunc.group_dead(vars) - local type_tbl = { - [{'group', 'groupname', 'gp', 'groupName'}] = 'string', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_dead', type_tbl, vars) - assert(err, errmsg) - - local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname - local flag = vars.flag - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if (Group.getByName(groupName) and Group.getByName(groupName):isExist() == false) or (Group.getByName(groupName) and #Group.getByName(groupName):getUnits() < 1) or not Group.getByName(groupName) then - if trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - end - else - if toggle then - trigger.action.setUserFlag(flag, false) - end - end - end - - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.group_dead, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle}}, timer.getTime() + interval) - end - end - - --- Sets a flag if less than given percent of group is alive. - -- @todo document - function mist.flagFunc.group_alive_less_than(vars) - local type_tbl = { - [{'group', 'groupname', 'gp', 'groupName'}] = 'string', - percent = 'number', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_less_than', type_tbl, vars) - assert(err, errmsg) - - local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname - local flag = vars.flag - local percent = vars.percent - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then - if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() < percent/100 then - if trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - end - else - if toggle then - trigger.action.setUserFlag(flag, false) - end - end - else - if trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - end - end - end - - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.group_alive_less_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) - end - end - - --- Sets a flag if more than given percent of group is alive. - -- @todo document - function mist.flagFunc.group_alive_more_than(vars) - local type_tbl = { - [{'group', 'groupname', 'gp', 'groupName'}] = 'string', - percent = 'number', - flag = {'number', 'string'}, - [{'stopflag', 'stopFlag'}] = {'number', 'string', 'nil'}, - interval = {'number', 'nil'}, - toggle = {'boolean', 'nil'}, - } - - local err, errmsg = mist.utils.typeCheck('mist.flagFunc.group_alive_more_than', type_tbl, vars) - assert(err, errmsg) - - local groupName = vars.groupName or vars.group or vars.gp or vars.Groupname - local flag = vars.flag - local percent = vars.percent - local stopflag = vars.stopflag or vars.stopFlag or -1 - local interval = vars.interval or 1 - local toggle = vars.toggle or nil - - - if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - if Group.getByName(groupName) and Group.getByName(groupName):isExist() == true then - if Group.getByName(groupName):getSize()/Group.getByName(groupName):getInitialSize() > percent/100 then - if trigger.misc.getUserFlag(flag) == 0 then - trigger.action.setUserFlag(flag, true) - end - else - if toggle and trigger.misc.getUserFlag(flag) == 1 then - trigger.action.setUserFlag(flag, false) - end - end - else --- just in case - if toggle and trigger.misc.getUserFlag(flag) == 1 then - trigger.action.setUserFlag(flag, false) - end - end - end - - if (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == false) then - mist.scheduleFunction(mist.flagFunc.group_alive_more_than, {{groupName = groupName, flag = flag, stopflag = stopflag, interval = interval, toggle = toggle, percent = percent}}, timer.getTime() + interval) - end - end - - mist.flagFunc.mapobjsDeadPolygon = mist.flagFunc.mapobjs_dead_polygon - mist.flagFunc.mapobjsDeadZones = mist.flagFunc.Mapobjs_dead_zones - mist.flagFunc.unitsInZones = mist.flagFunc.units_in_zones - mist.flagFunc.unitsInMovingZones = mist.flagFunc.units_in_moving_zones - mist.flagFunc.unitsInPolygon = mist.flagFunc.units_in_polygon - mist.flagFunc.unitsLOS = mist.flagFunc.units_LOS - mist.flagFunc.groupAlive = mist.flagFunc.group_alive - mist.flagFunc.groupDead = mist.flagFunc.group_dead - mist.flagFunc.groupAliveMoreThan = mist.flagFunc.group_alive_more_than - mist.flagFunc.groupAliveLessThan = mist.flagFunc.group_alive_less_than - -end - ---- Message functions. --- Messaging system --- @section mist.msg -do -- mist.msg scope - local messageList = {} - -- this defines the max refresh rate of the message box it honestly only needs to - -- go faster than this for precision timing stuff (which could be its own function) - local messageDisplayRate = 0.1 - local messageID = 0 - local displayActive = false - local displayFuncId = 0 - - local caSlots = false - local caMSGtoGroup = false - - if env.mission.groundControl then -- just to be sure? - for index, value in pairs(env.mission.groundControl) do - 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 - end - end - elseif type(value) == 'boolean' and value == true then - caSlots = true - break - end - end - end - - local function mistdisplayV5() - --[[thoughts to improve upon - event handler based activeClients table. - display messages only when there is an update - possibly co-routine it. - ]] - end - - local function mistdisplayV4() - local activeClients = {} - - for clientId, clientData in pairs(mist.DBs.humansById) do - if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then - activeClients[clientData.groupId] = clientData.groupName - end - end - - --[[if caSlots == true and caMSGtoGroup == true then - - end]] - - - if #messageList > 0 then - if displayActive == false then - displayActive = true - end - --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') - local msgTableText = {} - local msgTableSound = {} - - for messageId, messageData in pairs(messageList) do - if messageData.displayedFor > messageData.displayTime then - messageData:remove() -- now using the remove/destroy function. - else - if messageData.displayedFor then - messageData.displayedFor = messageData.displayedFor + messageDisplayRate - end - local nextSound = 1000 - local soundIndex = 0 - - if messageData.multSound and #messageData.multSound > 0 then - for index, sData in pairs(messageData.multSound) do - if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played - nextSound = sData.time - soundIndex = index - end - end - if soundIndex ~= 0 then - messageData.multSound[soundIndex].played = true - end - end - - for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants - if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists - if messageData.text then -- text - if not msgTableText[recData] then -- create table entry for text - msgTableText[recData] = {} - msgTableText[recData].text = {} - if recData == 'RED' or recData == 'BLUE' then - msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' - end - msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text - msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor - else -- add to table entry and adjust display time if needed - if recData == 'RED' or recData == 'BLUE' then - msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' - else - msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' - end - msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text - if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then - msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor - else - msgTableText[recData].displayTime = 1 - end - end - end - if soundIndex ~= 0 then - msgTableSound[recData] = messageData.multSound[soundIndex].file - end - end - end - - - end - end - ------- new display - - if caSlots == true and caMSGtoGroup == false then - if msgTableText.RED then - trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, true) - - end - if msgTableText.BLUE then - trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, true) - end - end - - for index, msgData in pairs(msgTableText) do - if type(index) == 'number' then -- its a groupNumber - trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, true) - end - end - --- new audio - if msgTableSound.RED then - trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) - end - if msgTableSound.BLUE then - trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) - end - - - for index, file in pairs(msgTableSound) do - if type(index) == 'number' then -- its a groupNumber - trigger.action.outSoundForGroup(index, file) - end - end - else - mist.removeFunction(displayFuncId) - displayActive = false - end - - end - - 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'}, - } - - --[[function mist.setCAGroupMSG(val) - if type(val) == 'boolean' then - caMSGtoGroup = val - return true - end - return false -end]] - - mist.message = { - - add = function(vars) - local function msgSpamFilter(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 vars = {} - vars.text = 'Hello World' - vars.displayTime = 20 - vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}} - mist.message.add(vars) - - Displays the message for all red coalition players. Players belonging to Ukraine and Georgia, and all A-10Cs on the map - - ]] - - - local new = {} - new.text = vars.text -- The actual message - new.displayTime = vars.displayTime -- How long will the message appear for - new.displayedFor = 0 -- how long the message has been displayed so far - new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text. - new.addedAt = timer.getTime() - new.update = true - - if vars.multSound and vars.multSound[1] then - new.multSound = vars.multSound - else - new.multSound = {} - end - - if vars.sound or vars.fileName then -- converts old sound file system into new multSound format - local sound = vars.sound - if vars.fileName then - sound = vars.fileName - end - new.multSound[#new.multSound+1] = {time = 0.1, file = sound} - end - - if #new.multSound > 0 then - for i, data in pairs(new.multSound) do - data.played = false - end - end - - local newMsgFor = {} -- list of all groups message displays for - for forIndex, forData in pairs(vars.msgFor) do - for list, listData in pairs(forData) do - for clientId, clientData in pairs(mist.DBs.humansById) do - forIndex = string.lower(forIndex) - if type(listData) == 'string' then - listData = string.lower(listData) - end - if (forIndex == 'coa' and (listData == string.lower(clientData.coalition) or listData == 'all')) or (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then -- - newMsgFor = msgSpamFilter(newMsgFor, clientData.groupId) -- so units dont get the same message twice if complex rules are given - --table.insert(newMsgFor, clientId) - elseif forIndex == 'unittypes' then - for typeId, typeData in pairs(listData) do - local found = false - 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 sString == clientData.type then - found = true - newMsgFor = msgSpamFilter(newMsgFor, 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 - for coaData, coaId in pairs(coalition.side) do - if string.lower(forIndex) == 'coa' or string.lower(forIndex) == 'ca' then - if listData == string.lower(coaData) or listData == 'all' then - newMsgFor = msgSpamFilter(newMsgFor, coaData) - end - end - end - end - end - - if #newMsgFor > 0 then - new.msgFor = newMsgFor -- I swear its not confusing - - else - return false - end - - - if vars.name and type(vars.name) == 'string' then - for i = 1, #messageList do - if messageList[i].name then - if messageList[i].name == vars.name then - --log:info('updateMessage') - messageList[i].displayedFor = 0 - messageList[i].addedAt = timer.getTime() - messageList[i].sound = new.sound - messageList[i].text = new.text - messageList[i].msgFor = new.msgFor - messageList[i].multSound = new.multSound - messageList[i].update = true - return messageList[i].messageID - end - end - end - end - - messageID = messageID + 1 - new.messageID = messageID - - --mist.debug.writeData(mist.utils.serialize,{'msg', new}, 'newMsg.lua') - - - messageList[#messageList + 1] = new - - local mt = { __index = mist.message} - setmetatable(new, mt) - - if displayActive == false then - displayActive = true - displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) - end - - return messageID - - end, - - remove = function(self) -- Now a self variable; the former functionality taken up by mist.message.removeById. - for i, msgData in pairs(messageList) do - if messageList[i] == self then - table.remove(messageList, i) - return true --removal successful - end - end - return false -- removal not successful this script fails at life! - end, - - removeById = function(id) -- This function is NOT passed a self variable; it is the remove by id function. - for i, msgData in pairs(messageList) do - if messageList[i].messageID == id then - table.remove(messageList, i) - return true --removal successful - end - end - return false -- removal not successful this script fails at life! - end, - } - - --[[ vars for mist.msgMGRS -vars.units - table of unit names (NOT unitNameTable- maybe this should change). -vars.acc - integer between 0 and 5, inclusive -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] - function mist.msgMGRS(vars) - local units = vars.units - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getMGRSString{units = units, acc = acc} - local newText - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - end - - --[[ vars for mist.msgLL -vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes). -vars.acc - integer, number of numbers after decimal place -vars.DMS - if true, output in degrees, minutes, seconds. Otherwise, output in degrees, minutes. -vars.text - text in the message -vars.displayTime - self explanatory -vars.msgFor - scope -]] - function mist.msgLL(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getLLString{units = units, acc = acc, DMS = DMS} - local newText - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - end - - --[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - vec3 ref point, maybe overload for vec2 as well? -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - function mist.msgBR(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString - local alt = vars.alt - local metric = vars.metric - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getBRString{units = units, ref = ref, alt = alt, metric = metric} - local newText - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - end - - -- basically, just sub-types of mist.msgBR... saves folks the work of getting the ref point. - --[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - string red, blue -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -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 - mist.msgBR(vars) - end - end - - --[[ -vars.units- table of unit names (NOT unitNameTable- maybe this should change). -vars.ref - unit name of reference point -vars.alt - boolean, if used, includes altitude in string -vars.metric - boolean, gives distance in km instead of NM. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - function mist.msgBRA(vars) - if Unit.getByName(vars.ref) and Unit.getByName(vars.ref):isExist() == true then - vars.ref = Unit.getByName(vars.ref):getPosition().p - if not vars.alt then - vars.alt = true - end - mist.msgBR(vars) - end - end - - --[[ vars for mist.msgLeadingMGRS: -vars.units - table of unit names -vars.heading - direction -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number, 0 to 5. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - function mist.msgLeadingMGRS(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc} - local newText - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - - end - - --[[ vars for mist.msgLeadingLL: -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.acc - number of digits after decimal point (can be negative) -vars.DMS - boolean, true if you want DMS. (optional) -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - function mist.msgLeadingLL(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local acc = vars.acc - local DMS = vars.DMS - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS} - local newText - - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - - end - - --[[ -vars.units - table of unit names -vars.heading - direction, number -vars.radius - number -vars.headingDegrees - boolean, switches heading to degrees (optional) -vars.metric - boolean, if true, use km instead of NM. (optional) -vars.alt - boolean, if true, include altitude. (optional) -vars.ref - vec3/vec2 reference point. -vars.text - text of the message -vars.displayTime -vars.msgFor - scope -]] - function mist.msgLeadingBR(vars) - local units = vars.units -- technically, I don't really need to do this, but it helps readability. - local heading = vars.heading - local radius = vars.radius - local headingDegrees = vars.headingDegrees - local metric = vars.metric - local alt = vars.alt - local ref = vars.ref -- vec2/vec3 will be handled in mist.getBRString - local text = vars.text - local displayTime = vars.displayTime - local msgFor = vars.msgFor - - local s = mist.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref} - local newText - - if text then - if string.find(text, '%%s') then -- look for %s - newText = string.format(text, s) -- insert the coordinates into the message - else - -- just append to the end. - newText = text .. s - end - else - newText = s - end - - mist.message.add{ - text = newText, - displayTime = displayTime, - msgFor = msgFor - } - end -end - ---- Demo functions. --- @section mist.demos -do -- mist.demos scope - mist.demos = {} - - function mist.demos.printFlightData(unit) - if unit:isExist() then - local function printData(unit, prevVel, prevE, prevTime) - local angles = mist.getAttitude(unit) - if angles then - local Heading = angles.Heading - local Pitch = angles.Pitch - local Roll = angles.Roll - local Yaw = angles.Yaw - local AoA = angles.AoA - local ClimbAngle = angles.ClimbAngle - - if not Heading then - Heading = 'NA' - else - Heading = string.format('%12.2f', mist.utils.toDegree(Heading)) - end - - if not Pitch then - Pitch = 'NA' - else - Pitch = string.format('%12.2f', mist.utils.toDegree(Pitch)) - end - - if not Roll then - Roll = 'NA' - else - Roll = string.format('%12.2f', mist.utils.toDegree(Roll)) - end - - local AoAplusYaw = 'NA' - if AoA and Yaw then - AoAplusYaw = string.format('%12.2f', mist.utils.toDegree((AoA^2 + Yaw^2)^0.5)) - end - - if not Yaw then - Yaw = 'NA' - else - Yaw = string.format('%12.2f', mist.utils.toDegree(Yaw)) - end - - if not AoA then - AoA = 'NA' - else - AoA = string.format('%12.2f', mist.utils.toDegree(AoA)) - end - - if not ClimbAngle then - ClimbAngle = 'NA' - else - ClimbAngle = string.format('%12.2f', mist.utils.toDegree(ClimbAngle)) - end - local unitPos = unit:getPosition() - local unitVel = unit:getVelocity() - local curTime = timer.getTime() - local absVel = string.format('%12.2f', mist.vec.mag(unitVel)) - - - local unitAcc = 'NA' - local Gs = 'NA' - local axialGs = 'NA' - local transGs = 'NA' - if prevVel and prevTime then - local xAcc = (unitVel.x - prevVel.x)/(curTime - prevTime) - local yAcc = (unitVel.y - prevVel.y)/(curTime - prevTime) - local zAcc = (unitVel.z - prevVel.z)/(curTime - prevTime) - - unitAcc = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc, z = zAcc})) - Gs = string.format('%12.2f', mist.vec.mag({x = xAcc, y = yAcc + 9.81, z = zAcc})/9.81) - axialGs = string.format('%12.2f', mist.vec.dp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x)/9.81) - transGs = string.format('%12.2f', mist.vec.mag(mist.vec.cp({x = xAcc, y = yAcc + 9.81, z = zAcc}, unitPos.x))/9.81) - end - - local E = 0.5*mist.vec.mag(unitVel)^2 + 9.81*unitPos.p.y - - local energy = string.format('%12.2e', E) - - local dEdt = 'NA' - if prevE and prevTime then - dEdt = string.format('%12.2e', (E - prevE)/(curTime - prevTime)) - end - - trigger.action.outText(string.format('%-25s', 'Heading: ') .. Heading .. ' degrees\n' .. string.format('%-25s', 'Roll: ') .. Roll .. ' degrees\n' .. string.format('%-25s', 'Pitch: ') .. Pitch - .. ' degrees\n' .. string.format('%-25s', 'Yaw: ') .. Yaw .. ' degrees\n' .. string.format('%-25s', 'AoA: ') .. AoA .. ' degrees\n' .. string.format('%-25s', 'AoA plus Yaw: ') .. AoAplusYaw .. ' degrees\n' .. string.format('%-25s', 'Climb Angle: ') .. - ClimbAngle .. ' degrees\n' .. string.format('%-25s', 'Absolute Velocity: ') .. absVel .. ' m/s\n' .. string.format('%-25s', 'Absolute Acceleration: ') .. unitAcc ..' m/s^2\n' - .. string.format('%-25s', 'Axial G loading: ') .. axialGs .. ' g\n' .. string.format('%-25s', 'Transverse G loading: ') .. transGs .. ' g\n' .. string.format('%-25s', 'Absolute G loading: ') .. Gs .. ' g\n' .. string.format('%-25s', 'Energy: ') .. energy .. ' J/kg\n' .. string.format('%-25s', 'dE/dt: ') .. dEdt ..' J/(kg*s)', 1) - return unitVel, E, curTime - end - end - - local function frameFinder(unit, prevVel, prevE, prevTime) - if unit:isExist() then - local currVel = unit:getVelocity() - if prevVel and (prevVel.x ~= currVel.x or prevVel.y ~= currVel.y or prevVel.z ~= currVel.z) or (prevTime and (timer.getTime() - prevTime) > 0.25) then - prevVel, prevE, prevTime = printData(unit, prevVel, prevE, prevTime) - end - mist.scheduleFunction(frameFinder, {unit, prevVel, prevE, prevTime}, timer.getTime() + 0.005) -- it can't go this fast, limited to the 100 times a sec check right now. - end - end - - - local curVel = unit:getVelocity() - local curTime = timer.getTime() - local curE = 0.5*mist.vec.mag(curVel)^2 + 9.81*unit:getPosition().p.y - frameFinder(unit, curVel, curE, curTime) - - end - - end - -end - ---- Time conversion functions. --- @section mist.time -do -- mist.time scope - mist.time = {} - -- returns a string for specified military time - -- theTime is optional - -- if present current time in mil time is returned - -- if number or table the time is converted into mil tim - function mist.time.convertToSec(timeTable) - - 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 - if timeTable.d and type(timeTable.d) == 'number' then - timeInSec = timeInSec + (timeTable.d*86400) - end - if timeTable.h and type(timeTable.h) == 'number' then - timeInSec = timeInSec + (timeTable.h*3600) - end - if timeTable.m and type(timeTable.m) == 'number' then - timeInSec = timeInSec + (timeTable.m*60) - end - if timeTable.s and type(timeTable.s) == 'number' then - timeInSec = timeInSec + timeTable.s - end - - end - return timeInSec - end - - function mist.time.getDHMS(timeInSec) - if timeInSec and type(timeInSec) == 'number' then - local tbl = {d = 0, h = 0, m = 0, s = 0} - if timeInSec > 86400 then - while timeInSec > 86400 do - tbl.d = tbl.d + 1 - timeInSec = timeInSec - 86400 - end - end - if timeInSec > 3600 then - while timeInSec > 3600 do - tbl.h = tbl.h + 1 - timeInSec = timeInSec - 3600 - end - end - if timeInSec > 60 then - while timeInSec > 60 do - tbl.m = tbl.m + 1 - timeInSec = timeInSec - 60 - end - end - tbl.s = timeInSec - return tbl - else - log:error("Didn't recieve number") - return - end - end - - function mist.getMilString(theTime) - local timeInSec = 0 - if theTime then - timeInSec = mist.time.convertToSec(theTime) - else - timeInSec = mist.utils.round(timer.getAbsTime(), 0) - end - - local DHMS = mist.time.getDHMS(timeInSec) - - return tostring(string.format('%02d', DHMS.h) .. string.format('%02d',DHMS.m)) - end - - function mist.getClockString(theTime, hour) - local timeInSec = 0 - if theTime then - timeInSec = mist.time.convertToSec(theTime) - else - timeInSec = mist.utils.round(timer.getAbsTime(), 0) - end - local DHMS = mist.time.getDHMS(timeInSec) - if hour then - if DHMS.h > 12 then - DHMS.h = DHMS.h - 12 - return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' PM') - else - return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s) .. ' AM') - end - else - return tostring(string.format('%02d', DHMS.h) .. ':' .. string.format('%02d',DHMS.m) .. ':' .. string.format('%02d',DHMS.s)) - end - end - - -- returns the date in string format - -- both variables optional - -- first val returns with the month as a string - -- 2nd val defins if it should be written the American way or the wrong way. - function mist.time.getDate(convert) - local cal = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -- - local date = {} - - if not env.mission.date then -- Not likely to happen. Resaving mission auto updates this to remove it. - date.d = 0 - date.m = 6 - date.y = 2011 - else - date.d = env.mission.date.Day - date.m = env.mission.date.Month - date.y = env.mission.date.Year - end - local start = 86400 - local timeInSec = mist.utils.round(timer.getAbsTime()) - if convert and type(convert) == 'number' then - timeInSec = convert - end - if timeInSec > 86400 then - while start < timeInSec do - if date.d >= cal[date.m] then - if date.m == 2 and date.d == 28 then -- HOLY COW we can edit years now. Gotta re-add this! - if date.y % 4 == 0 and date.y % 100 == 0 and date.y % 400 ~= 0 or date.y % 4 > 0 then - date.m = date.m + 1 - date.d = 0 - end - --date.d = 29 - else - date.m = date.m + 1 - date.d = 0 - end - end - if date.m == 13 then - date.m = 1 - date.y = date.y + 1 - end - date.d = date.d + 1 - start = start + 86400 - - end - end - return date - end - - function mist.time.relativeToStart(time) - if type(time) == 'number' then - return time - timer.getTime0() - end - end - - function mist.getDateString(rtnType, murica, oTime) -- returns date based on time - local word = {'January', 'Feburary', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' } -- 'etc - local curTime = 0 - if oTime then - curTime = oTime - else - curTime = mist.utils.round(timer.getAbsTime()) - end - local tbl = mist.time.getDate(curTime) - - if rtnType then - if murica then - return tostring(word[tbl.m] .. ' ' .. tbl.d .. ' ' .. tbl.y) - else - return tostring(tbl.d .. ' ' .. word[tbl.m] .. ' ' .. tbl.y) - end - else - if murica then - return tostring(tbl.m .. '.' .. tbl.d .. '.' .. tbl.y) - else - return tostring(tbl.d .. '.' .. tbl.m .. '.' .. tbl.y) - end - end - end - --WIP - function mist.time.milToGame(milString, rtnType) --converts a military time. By default returns the abosolute time that event would occur. With optional value it returns how many seconds from time of call till that time. - local curTime = mist.utils.round(timer.getAbsTime()) - local milTimeInSec = 0 - - if milString and type(milString) == 'string' and string.len(milString) >= 4 then - local hr = tonumber(string.sub(milString, 1, 2)) - local mi = tonumber(string.sub(milString, 3)) - milTimeInSec = milTimeInSec + (mi*60) + (hr*3600) - elseif milString and type(milString) == 'table' and (milString.d or milString.h or milString.m or milString.s) then - milTimeInSec = mist.time.convertToSec(milString) - end - - local startTime = timer.getTime0() - local daysOffset = 0 - if startTime > 86400 then - daysOffset = mist.utils.round(startTime/86400) - if daysOffset > 0 then - milTimeInSec = milTimeInSec *daysOffset - end - end - - if curTime > milTimeInSec then - milTimeInSec = milTimeInSec + 86400 - end - if rtnType then - milTimeInSec = milTimeInSec - startTime - end - return milTimeInSec - end - - -end - ---- Group task functions. --- @section tasks -do -- group tasks scope - mist.ground = {} - mist.fixedWing = {} - mist.heli = {} - mist.air = {} - mist.air.fixedWing = {} - mist.air.heli = {} - - --- Tasks group to follow a route. - -- This sets the mission task for the given group. - -- Any wrapped actions inside the path (like enroute - -- tasks) will be executed. - -- @tparam Group group group to task. - -- @tparam table path containing - -- points defining a route. - function mist.goRoute(group, path) - local misTask = { - id = 'Mission', - params = { - route = { - points = mist.utils.deepCopy(path), - }, - }, - } - if type(group) == 'string' then - group = Group.getByName(group) - end - if group then - local groupCon = group:getController() - if groupCon then - groupCon:setTask(misTask) - return true - end - end - return false - end - - -- same as getGroupPoints but returns speed and formation type along with vec2 of point} - function mist.getGroupRoute(groupIdent, task) - -- refactor to search by groupId and allow groupId and groupName as inputs - local gpId = groupIdent - if mist.DBs.MEgroupsByName[groupIdent] then - gpId = mist.DBs.MEgroupsByName[groupIdent].groupId - else - log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) - 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 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 - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do - if group_data and group_data.groupId == gpId then -- this is the group we are looking for - if group_data.route and group_data.route.points and #group_data.route.points > 0 then - local points = {} - - for point_num, point in pairs(group_data.route.points) do - local routeData = {} - if not point.point then - routeData.x = point.x - routeData.y = point.y - else - routeData.point = point.point --it's possible that the ME could move to the point = Vec2 notation. - end - routeData.form = point.action - routeData.speed = point.speed - routeData.alt = point.alt - routeData.alt_type = point.alt_type - routeData.airdromeId = point.airdromeId - routeData.helipadId = point.helipadId - routeData.type = point.type - routeData.action = point.action - if task then - routeData.task = point.task - end - points[point_num] = routeData - end - - return points - end - log:error('Group route not defined in mission editor for groupId: $1', gpId) - return - end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do - end --for cntry_id, cntry_data in pairs(coa_data.country) do - end --if coa_data.country then --there is a country table - end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then - end --for coa_name, coa_data in pairs(mission.coalition) do - end - - -- function mist.ground.buildPath() end -- ???? - - function mist.ground.patrolRoute(vars) - log:info('patrol') - local tempRoute = {} - local useRoute = {} - local gpData = vars.gpData - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - local useGroupRoute - if not vars.useGroupRoute then - useGroupRoute = vars.gpData - else - useGroupRoute = vars.useGroupRoute - end - local routeProvided = false - if not vars.route then - if useGroupRoute then - tempRoute = mist.getGroupRoute(useGroupRoute) - end - else - useRoute = vars.route - local posStart = mist.getLeadPos(gpData) - useRoute[1] = mist.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed) - routeProvided = true - end - - - local overRideSpeed = vars.speed or 'default' - local pType = vars.pType - local offRoadForm = vars.offRoadForm or 'default' - local onRoadForm = vars.onRoadForm or 'default' - - if routeProvided == false and #tempRoute > 0 then - local posStart = mist.getLeadPos(gpData) - - - useRoute[#useRoute + 1] = mist.ground.buildWP(posStart, offRoadForm, overRideSpeed) - for i = 1, #tempRoute do - local tempForm = tempRoute[i].action - local tempSpeed = tempRoute[i].speed - - if offRoadForm == 'default' then - tempForm = tempRoute[i].action - end - if onRoadForm == 'default' then - onRoadForm = 'On Road' - end - if (string.lower(tempRoute[i].action) == 'on road' or string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then - tempForm = onRoadForm - else - tempForm = offRoadForm - end - - if type(overRideSpeed) == 'number' then - tempSpeed = overRideSpeed - end - - - useRoute[#useRoute + 1] = mist.ground.buildWP(tempRoute[i], tempForm, tempSpeed) - end - - if pType and string.lower(pType) == 'doubleback' then - local curRoute = mist.utils.deepCopy(useRoute) - for i = #curRoute, 2, -1 do - useRoute[#useRoute + 1] = mist.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed) - end - end - - useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP - end - - local cTask3 = {} - local newPatrol = {} - newPatrol.route = useRoute - newPatrol.gpData = gpData:getName() - cTask3[#cTask3 + 1] = 'mist.ground.patrolRoute(' - cTask3[#cTask3 + 1] = mist.utils.oneLineSerialize(newPatrol) - cTask3[#cTask3 + 1] = ')' - cTask3 = table.concat(cTask3) - local tempTask = { - id = 'WrappedAction', - params = { - action = { - id = 'Script', - params = { - command = cTask3, - - }, - }, - }, - } - - useRoute[#useRoute].task = tempTask - log:info(useRoute) - mist.goRoute(gpData, useRoute) - - return - end - - function mist.ground.patrol(gpData, pType, form, speed) - local vars = {} - - if type(gpData) == 'table' and gpData:getName() then - gpData = gpData:getName() - end - - vars.useGroupRoute = gpData - vars.gpData = gpData - vars.pType = pType - vars.offRoadForm = form - vars.speed = speed - - mist.ground.patrolRoute(vars) - - return - end - - -- No longer accepts path - function mist.ground.buildWP(point, overRideForm, overRideSpeed) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - local form, speed - - if point.speed and not overRideSpeed then - wp.speed = point.speed - elseif type(overRideSpeed) == 'number' then - wp.speed = overRideSpeed - else - wp.speed = mist.utils.kmphToMps(20) - end - - if point.form and not overRideForm then - form = point.form - else - form = overRideForm - end - - if not form then - wp.action = 'Cone' - else - form = string.lower(form) - if form == 'off_road' or form == 'off road' then - wp.action = 'Off Road' - elseif form == 'on_road' or form == 'on road' then - wp.action = 'On Road' - elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then - wp.action = 'Rank' - elseif form == 'cone' then - wp.action = 'Cone' - elseif form == 'diamond' then - wp.action = 'Diamond' - elseif form == 'vee' then - wp.action = 'Vee' - elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then - wp.action = 'EchelonL' - elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then - wp.action = 'EchelonR' - else - wp.action = 'Cone' -- if nothing matched - end - end - - wp.type = 'Turning Point' - - return wp - - end - - function mist.fixedWing.buildWP(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 2000 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or altType == 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or altType == 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = mist.utils.kmphToMps(500) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp - end - - function mist.heli.buildWP(point, WPtype, speed, alt, altType) - - local wp = {} - wp.x = point.x - - if point.z then - wp.y = point.z - else - wp.y = point.y - end - - if alt and type(alt) == 'number' then - wp.alt = alt - else - wp.alt = 500 - end - - if altType then - altType = string.lower(altType) - if altType == 'radio' or altType == 'agl' then - wp.alt_type = 'RADIO' - elseif altType == 'baro' or altType == 'asl' then - wp.alt_type = 'BARO' - end - else - wp.alt_type = 'RADIO' - end - - if point.speed then - speed = point.speed - end - - if point.type then - WPtype = point.type - end - - if not speed then - wp.speed = mist.utils.kmphToMps(200) - else - wp.speed = speed - end - - if not WPtype then - wp.action = 'Turning Point' - else - WPtype = string.lower(WPtype) - if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then - wp.action = 'Fly Over Point' - elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then - wp.action = 'Turning Point' - else - wp.action = 'Turning Point' - end - end - - wp.type = 'Turning Point' - return wp - end - - -- need to return a Vec3 or Vec2? - function mist.getRandPointInCircle(point, radius, innerRadius) - local theta = 2*math.pi*math.random() - 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 - 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} - else - rndCoord = {x = point.x, y = point.z} - end - return rndCoord - end - - function mist.getRandomPointInZone(zoneName, innerRadius) - 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) - end - return false - end - - function mist.groupToRandomPoint(vars) - local group = vars.group --Required - local point = vars.point --required - local radius = vars.radius or 0 - local innerRadius = vars.innerRadius - local form = vars.form or 'Cone' - local heading = vars.heading or math.random()*2*math.pi - local headingDegrees = vars.headingDegrees - local speed = vars.speed or mist.utils.kmphToMps(20) - - - local useRoads - if not vars.disableRoads then - useRoads = true - else - useRoads = false - end - - local path = {} - - if headingDegrees then - heading = headingDegrees*math.pi/180 - end - - if heading >= 2*math.pi then - heading = heading - 2*math.pi - end - - local rndCoord = mist.getRandPointInCircle(point, radius, innerRadius) - - 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 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 - - path[#path + 1] = mist.ground.buildWP(offset, form, speed) - path[#path + 1] = mist.ground.buildWP(rndCoord, form, speed) - - mist.goRoute(group, path) - - return - end - - function mist.groupRandomDistSelf(gpData, dist, form, heading, speed) - local pos = mist.getLeadPos(gpData) - local fakeZone = {} - fakeZone.radius = dist or math.random(300, 1000) - fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} - mist.groupToRandomZone(gpData, fakeZone, form, heading, speed) - - return - end - - function mist.groupToRandomZone(gpData, zone, form, heading, speed) - if type(gpData) == 'string' then - gpData = Group.getByName(gpData) - end - - if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) - end - - if speed then - speed = mist.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.radius = zone.radius - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.point = mist.utils.zoneToVec3(zone) - - mist.groupToRandomPoint(vars) - - return - end - - function mist.isTerrainValid(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types - if coord.z then - coord.y = coord.z - end - local typeConverted = {} - - if type(terrainTypes) == 'string' then -- if its a string it does this check - for constId, constData in pairs(land.SurfaceType) do - if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then - table.insert(typeConverted, constId) - end - end - 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 - table.insert(typeConverted, constId) - end - end - end - end - for validIndex, validData in pairs(typeConverted) do - if land.getSurfaceType(coord) == land.SurfaceType[validData] then - return true - end - end - return false - end - - function mist.terrainHeightDiff(coord, searchSize) - local samples = {} - local searchRadius = 5 - if searchSize then - searchRadius = searchSize - end - if type(coord) == 'string' then - coord = mist.utils.zoneToVec3(coord) - end - - coord = mist.utils.makeVec2(coord) - - samples[#samples + 1] = land.getHeight(coord) - for i = 0, 360, 30 do - samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*searchRadius)), y = (coord.y + (math.cos(math.rad(i))*searchRadius))}) - if searchRadius >= 20 then -- if search radius is sorta large, take a sample halfway between center and outer edge - samples[#samples + 1] = land.getHeight({x = (coord.x + (math.sin(math.rad(i))*(searchRadius/2))), y = (coord.y + (math.cos(math.rad(i))*(searchRadius/2)))}) - end - end - local tMax, tMin = 0, 1000000 - for index, height in pairs(samples) do - if height > tMax then - tMax = height - end - if height < tMin then - tMin = height - end - end - return mist.utils.round(tMax - tMin, 2) - end - - function mist.groupToPoint(gpData, point, form, heading, speed, useRoads) - if type(point) == 'string' then - point = trigger.misc.getZone(point) - end - if speed then - speed = mist.utils.kmphToMps(speed) - end - - local vars = {} - vars.group = gpData - vars.form = form - vars.headingDegrees = heading - vars.speed = speed - vars.disableRoads = useRoads - vars.point = mist.utils.zoneToVec3(point) - mist.groupToRandomPoint(vars) - - return - end - - function mist.getLeadPos(group) - 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. - local lowestInd = math.huge - for ind, unit in pairs(units) do - if Unit.isExist(unit) and ind < lowestInd then - lowestInd = ind - return unit:getPosition().p - end - end - end - if leader and Unit.isExist(leader) then -- maybe a little too paranoid now... - return leader:getPosition().p - end - end - -end - ---- Database tables. --- @section mist.DBs - ---- Mission data --- @table mist.DBs.missionData --- @field startTime mission start time --- @field theatre mission theatre/map e.g. Caucasus --- @field version mission version --- @field files mission resources - ---- Tables used as parameters. --- @section varTables - ---- mist.flagFunc.units_in_polygon parameter table. --- @table unitsInPolygonVars --- @tfield table unit name table @{UnitNameTable}. --- @tfield table zone table defining a polygon. --- @tfield number|string flag flag to set to true. --- @tfield[opt] number|string stopflag if set to true the function --- will stop evaluating. --- @tfield[opt] number maxalt maximum altitude (MSL) for the --- polygon. --- @tfield[opt] number req_num minimum number of units that have --- to be in the polygon. --- @tfield[opt] number interval sets the interval for --- checking if units are inside of the polygon in seconds. Default: 1. --- @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 - mist.Logger = {} - - --- parses text and substitutes keywords with values from given array. - -- @param text string containing keywords to substitute with values - -- or a variable. - -- @param ... variables to use for substitution in string. - -- @treturn string new string with keywords substituted or - -- value of variable as string. - local function formatText(text, ...) - if type(text) ~= 'string' then - if type(text) == 'table' then - text = mist.utils.oneLineSerialize(text) - else - text = tostring(text) - end - else - for index,value in ipairs(arg) do - -- TODO: check for getmetatabel(value).__tostring - if type(value) == 'table' then - value = mist.utils.oneLineSerialize(value) - else - value = tostring(value) - end - text = text:gsub('$' .. index, value) - end - end - local fName = nil - local cLine = nil - if debug then - local dInfo = debug.getinfo(3) - fName = dInfo.name - cLine = dInfo.currentline - -- local fsrc = dinfo.short_src - --local fLine = dInfo.linedefined - end - if fName and cLine then - return fName .. '|' .. cLine .. ': ' .. text - elseif cLine then - return cLine .. ': ' .. text - else - return ' ' .. text - end - end - - local function splitText(text) - local tbl = {} - while text:len() > 4000 do - local sub = text:sub(1, 4000) - text = text:sub(4001) - table.insert(tbl, sub) - end - table.insert(tbl, text) - return tbl - end - - --- Creates a new logger. - -- Each logger has it's own tag and log level. - -- @tparam string tag tag which appears at the start of - -- every log line produced by 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: - -- "none" (0), "error" (1), "warning" (2) and "info" (3). - -- @usage myLogger = mist.Logger:new("MyScript") - -- @usage myLogger = mist.Logger:new("MyScript", 2) - -- @usage myLogger = mist.Logger:new("MyScript", "info") - -- @treturn mist.Logger - function mist.Logger:new(tag, level) - local l = {} - l.tag = tag - setmetatable(l, self) - self.__index = self - self: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: - -- "none" (0), "error" (1), "warning" (2) and "info" (3). - -- @usage myLogger:setLevel("info") - -- @usage -- log everything - --myLogger:setLevel(3) - function mist.Logger:setLevel(level) - if not level then - self.level = 2 - else - if type(level) == 'string' then - if level == 'none' or level == 'off' then - self.level = 0 - elseif level == 'error' then - self.level = 1 - elseif level == 'warning' or level == 'warn' then - self.level = 2 - elseif level == 'info' then - self.level = 3 - end - elseif type(level) == 'number' then - self.level = level - else - self.level = 2 - end - end - end - - --- Logs error and shows alert window. - -- This logs an error to the dcs.log and shows a popup window, - -- pausing the simulation. This works always even if logging is - -- disabled by setting a log level of "none" or 0. - -- @tparam string text the text with keywords to substitute. - -- @param ... variables to be used for substitution. - -- @usage myLogger:alert("Shit just hit the fan! WEEEE!!!11") - function mist.Logger:alert(text, ...) - text = formatText(text, unpack(arg)) - if text:len() > 4000 then - local texts = splitText(text) - for i = 1, #texts do - if i == 1 then - env.error(self.tag .. '|' .. texts[i], true) - else - env.error(texts[i]) - end - end - else - env.error(self.tag .. '|' .. text, true) - end - end - - --- Logs a message, disregarding the log level. - -- @tparam string text the text with keywords to substitute. - -- @param ... variables to be used for substitution. - -- @usage myLogger:msg("Always logged!") - function mist.Logger:msg(text, ...) - text = formatText(text, unpack(arg)) - if text:len() > 4000 then - local texts = splitText(text) - for i = 1, #texts do - if i == 1 then - env.info(self.tag .. '|' .. texts[i]) - else - env.info(texts[i]) - end - end - else - env.info(self.tag .. '|' .. text) - end - end - - --- Logs an error. - -- logs a message prefixed with this loggers tag to dcs.log as - -- long as at least the "error" log level (1) is set. - -- @tparam string text the text with keywords to substitute. - -- @param ... variables to be used for substitution. - -- @usage myLogger:error("Just an error!") - -- @usage myLogger:error("Foo is $1 instead of $2", foo, "bar") - function mist.Logger:error(text, ...) - if self.level >= 1 then - text = formatText(text, unpack(arg)) - if text:len() > 4000 then - local texts = splitText(text) - for i = 1, #texts do - if i == 1 then - env.error(self.tag .. '|' .. texts[i]) - else - env.error(texts[i]) - end - end - else - env.error(self.tag .. '|' .. text) - end - end - end - - --- Logs a warning. - -- logs a message prefixed with this loggers tag to dcs.log as - -- long as at least the "warning" log level (2) is set. - -- @tparam string text the text with keywords to substitute. - -- @param ... variables to be used for substitution. - -- @usage myLogger:warn("Mother warned you! Those $1 from the interwebs are $2", {"geeks", 1337}) - function mist.Logger:warn(text, ...) - if self.level >= 2 then - text = formatText(text, unpack(arg)) - if text:len() > 4000 then - local texts = splitText(text) - for i = 1, #texts do - if i == 1 then - env.warning(self.tag .. '|' .. texts[i]) - else - env.warning(texts[i]) - end - end - else - env.warning(self.tag .. '|' .. text) - end - end - end - - --- Logs a info. - -- logs a message prefixed with this loggers tag to dcs.log as - -- long as the highest log level (3) "info" is set. - -- @tparam string text the text with keywords to substitute. - -- @param ... variables to be used for substitution. - -- @see warn - function mist.Logger:info(text, ...) - if self.level >= 3 then - text = formatText(text, unpack(arg)) - if text:len() > 4000 then - local texts = splitText(text) - for i = 1, #texts do - if i == 1 then - env.info(self.tag .. '|' .. texts[i]) - else - env.info(texts[i]) - end - end - else - env.info(self.tag .. '|' .. text) - end - end - end - -end - --- initialize mist -mist.init() -env.info(('Mist version ' .. mist.majorVersion .. '.' .. mist.minorVersion .. '.' .. mist.build .. ' loaded.')) - --- vim: noet:ts=2:sw=2 diff --git a/resources/plugins/jtacautolase/plugin.json b/resources/plugins/jtacautolase/plugin.json index 18e7ca5a..9954aba3 100644 --- a/resources/plugins/jtacautolase/plugin.json +++ b/resources/plugins/jtacautolase/plugin.json @@ -9,10 +9,6 @@ } ], "scriptsWorkOrders": [ - { - "file": "mist_4_3_74.lua", - "mnemonic": "mist" - }, { "file": "JTACAutoLase.lua", "mnemonic": "jtacautolase-script" diff --git a/resources/plugins/skynetiads/skynet-iads-compiled.lua b/resources/plugins/skynetiads/skynet-iads-compiled.lua index 1187e93a..61763cc4 100644 --- a/resources/plugins/skynetiads/skynet-iads-compiled.lua +++ b/resources/plugins/skynetiads/skynet-iads-compiled.lua @@ -1,4 +1,4 @@ -env.info("--- SKYNET VERSION: 1.1.3 | BUILD TIME: 30.09.2020 1816Z ---") +env.info("--- SKYNET VERSION: 1.2.0 | BUILD TIME: 21.11.2020 1159Z ---") do --this file contains the required units per sam type samTypesDB = { @@ -554,8 +554,10 @@ function SkynetIADS:addEarlyWarningRadar(earlyWarningRadarUnitName) ewRadar:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then - self:updateIADSCoverage() + self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end + ewRadar:setActAsEW(true) + ewRadar:setToCorrectAutonomousState() ewRadar:goLive() table.insert(self.earlyWarningRadars, ewRadar) if self:getDebugSettings().addedEWRadar then @@ -623,12 +625,12 @@ function SkynetIADS:addSAMSite(samSiteName) self:setCoalition(samSiteDCS) local samSite = SkynetIADSSamSite:create(samSiteDCS, self) samSite:setupElements() + samSite:goLive() -- for performance improvement, if iads is not scanning no update coverage update needs to be done, will be executed once when iads activates if self.ewRadarScanMistTaskID ~= nil then - self:updateIADSCoverage() + self:buildRadarCoverageForSAMSite(samSite) end samSite:setCachedTargetsMaxAge(self:getCachedTargetsMaxAge()) - samSite:goLive() if samSite:getNatoName() == "UNKNOWN" then self:printOutput("you have added an SAM Site that Skynet IADS can not handle: "..samSite:getDCSName(), true) samSite:cleanUp() @@ -695,47 +697,30 @@ function SkynetIADS:addCommandCenter(commandCenter) self:setCoalition(commandCenter) local comCenter = SkynetIADSCommandCenter:create(commandCenter, self) table.insert(self.commandCenters, comCenter) + -- when IADS is active the radars will be added to the new command center. If it not active this will happen when radar coverage is built + if self.ewRadarScanMistTaskID ~= nil then + self:addRadarsToCommandCenters() + end return comCenter end function SkynetIADS:isCommandCenterUsable() - local hasWorkingCommandCenter = (#self.commandCenters == 0) - for i = 1, #self.commandCenters do - local comCenter = self.commandCenters[i] - if comCenter:isDestroyed() == false and comCenter:hasWorkingPowerSource() then - hasWorkingCommandCenter = true - break - else - hasWorkingCommandCenter = false - end + if #self:getCommandCenters() == 0 then + return true end - return hasWorkingCommandCenter + local usableComCenters = self:getUsableAbstractRadarElemtentsOfTable(self:getCommandCenters()) + return (#usableComCenters > 0) end function SkynetIADS:getCommandCenters() return self.commandCenters end -function SkynetIADS:setSAMSitesToAutonomousMode() - for i= 1, #self.samSites do - samSite = self.samSites[i] - samSite:goAutonomous() - end -end function SkynetIADS.evaluateContacts(self) - if self:isCommandCenterUsable() == false then - if self:getDebugSettings().noWorkingCommmandCenter then - self:printOutput("No Working Command Center") - end - self:setSAMSitesToAutonomousMode() - return - end local ewRadars = self:getUsableEarlyWarningRadars() local samSites = self:getUsableSAMSites() - - -- rewrote this part of the code to keep loops to a minimum --will add SAM Sites acting as EW Rardars to the ewRadars array: for i = 1, #samSites do @@ -761,13 +746,13 @@ function SkynetIADS.evaluateContacts(self) local ewRadar = ewRadars[i] --call go live in case ewRadar had to shut down (HARM attack) ewRadar:goLive() - -- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the sams + -- if an awacs has traveled more than a predeterminded distance we update the autonomous state of the SAMs if getmetatable(ewRadar) == SkynetIADSAWACSRadar and ewRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() then - self:updateAutonomousStatesOfSAMSites() + self:buildRadarCoverageForEarlyWarningRadar(ewRadar) end local ewContacts = ewRadar:getDetectedTargets() if #ewContacts > 0 then - local samSitesUnderCoverage = ewRadar:getSAMSitesInCoveredArea() + local samSitesUnderCoverage = ewRadar:getUsableChildRadars() for j = 1, #samSitesUnderCoverage do local samSiteUnterCoverage = samSitesUnderCoverage[j] -- only if a SAM site is not active we add it to the hash of SAM sites to be iterated later on @@ -817,62 +802,115 @@ function SkynetIADS:cleanAgedTargets() self.contacts = contactsToKeep end -function SkynetIADS:buildSAMSitesInCoveredArea() - local samSites = self:getUsableSAMSites() - for i = 1, #samSites do - local samSite = samSites[i] - samSite:updateSAMSitesInCoveredArea() - end +--TODO unit test this method: +function SkynetIADS:getAbstracRadarElements() + local abstractRadarElements = {} + local ewRadars = self:getEarlyWarningRadars() + local samSites = self:getSAMSites() - local ewRadars = self:getUsableEarlyWarningRadars() for i = 1, #ewRadars do local ewRadar = ewRadars[i] - ewRadar:updateSAMSitesInCoveredArea() + table.insert(abstractRadarElements, ewRadar) end -end - -function SkynetIADS:updateIADSCoverage() - self:buildSAMSitesInCoveredArea() - self:enforceRebuildAutonomousStateOfSAMSites() - --update moose connector with radar group names Skynet is able to use - self:getMooseConnector():update() -end - -function SkynetIADS:updateAutonomousStatesOfSAMSites(deadUnit) - --deat unit is to prevent multiple calls via the event handling of SkynetIADSAbstractElement when a units power source or connection node is destroyed - if deadUnit == nil or self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite ~= deadUnit then - self:updateIADSCoverage() - self.destroyedUnitResponsibleForUpdateAutonomousStateOfSAMSite = deadUnit - end -end - -function SkynetIADS:enforceRebuildAutonomousStateOfSAMSites() - local ewRadars = self:getUsableEarlyWarningRadars() - local samSites = self:getUsableSAMSites() for i = 1, #samSites do local samSite = samSites[i] - if samSite:getActAsEW() then - table.insert(ewRadars, samSite) - end + table.insert(abstractRadarElements, samSite) end + return abstractRadarElements +end + +function SkynetIADS:addRadarsToCommandCenters() + + --we clear any existing radars that may have been added earlier + local comCenters = self:getCommandCenters() + for i = 1, #comCenters do + local comCenter = comCenters[i] + comCenter:clearChildRadars() + end + + -- then we add child radars to the command centers + local abstractRadarElements = self:getAbstracRadarElements() + for i = 1, #abstractRadarElements do + local abstractRadar = abstractRadarElements[i] + self:addSingleRadarToCommandCenters(abstractRadar) + end +end + +function SkynetIADS:addSingleRadarToCommandCenters(abstractRadarElement) + local comCenters = self:getCommandCenters() + for i = 1, #comCenters do + local comCenter = comCenters[i] + comCenter:addChildRadar(abstractRadarElement) + end +end + +-- this method rebuilds the radar coverage of the IADS, a complete rebuild is only required the first time the IADS is activated +-- during runtime it is sufficient to call buildRadarCoverageForSAMSite or buildRadarCoverageForEarlyWarningRadar method that just updates the IADS for one unit, this saves script execution time +function SkynetIADS:buildRadarCoverage() + + --to build the basic radar coverage we use all SAM sites. Checks if SAM site has power or a connection node is done when using the SAM site later on + local samSites = self:getSAMSites() + + --first we clear all child and parent radars that may have been added previously for i = 1, #samSites do local samSite = samSites[i] - local inRange = false - for j = 1, #ewRadars do - if samSite:isInRadarDetectionRangeOf(ewRadars[j]) then - inRange = true + samSite:clearChildRadars() + samSite:clearParentRadars() + end + + local ewRadars = self:getEarlyWarningRadars() + + for i = 1, #ewRadars do + local ewRadar = ewRadars[i] + ewRadar:clearChildRadars() + end + + --then we rebuild the radar coverage + local abstractRadarElements = self:getAbstracRadarElements() + for i = 1, #abstractRadarElements do + local abstract = abstractRadarElements[i] + self:buildRadarCoverageForAbstractRadarElement(abstract) + end + + self:addRadarsToCommandCenters() +end + +function SkynetIADS:buildRadarCoverageForAbstractRadarElement(abstractRadarElement) + local abstractRadarElements = self:getAbstracRadarElements() + for i = 1, #abstractRadarElements do + local aElementToCompare = abstractRadarElements[i] + if aElementToCompare ~= abstractRadarElement then + + if aElementToCompare:isInRadarDetectionRangeOf(abstractRadarElement) then + if getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSSamSite then + abstractRadarElement:addChildRadar(aElementToCompare) + end + if getmetatable(aElementToCompare) == SkynetIADSSamSite and getmetatable(abstractRadarElement) == SkynetIADSEWRadar then + abstractRadarElement:addChildRadar(aElementToCompare) + end + + --EW Radars should not have parent Radars + if getmetatable(aElementToCompare) ~= SkynetIADSEWRadar then + aElementToCompare:addParentRadar(abstractRadarElement) + end end - end - if inRange == false then - samSite:goAutonomous() - else - samSite:resetAutonomousState() + end end end +function SkynetIADS:buildRadarCoverageForSAMSite(samSite) + self:buildRadarCoverageForAbstractRadarElement(samSite) + self:addSingleRadarToCommandCenters(samSite) +end + +function SkynetIADS:buildRadarCoverageForEarlyWarningRadar(ewRadar) + self:buildRadarCoverageForAbstractRadarElement(ewRadar) + self:addSingleRadarToCommandCenters(ewRadar) +end + function SkynetIADS:mergeContact(contact) local existingContact = false for i = 1, #self.contacts do @@ -887,6 +925,7 @@ function SkynetIADS:mergeContact(contact) end end + function SkynetIADS:getContacts() return self.contacts end @@ -909,7 +948,7 @@ function SkynetIADS.activate(self) mist.removeFunction(self.ewRadarScanMistTaskID) mist.removeFunction(self.samSetupMistTaskID) self.ewRadarScanMistTaskID = mist.scheduleFunction(SkynetIADS.evaluateContacts, {self}, 1, self.contactUpdateInterval) - self:updateIADSCoverage() + self:buildRadarCoverage() end function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) @@ -920,9 +959,7 @@ function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) for i = 1, #samSites do local sam = samSites[i] sam:goLive() - --stop harm scan, because this function will shut down point defences - sam:stopScanningForHARMs() - --point defences will go dark after sam:goLive() call on the SAM they are protecting, so we load them and call a separate goLive call here, some SAMs will therefore receive 2 goLive calls + --point defences will go dark after sam:goLive() call on the SAM they are protecting, so we load them by calling a separate goLive call here, point defence SAMs will therefore receive 2 goLive calls -- this should not have a negative impact on performance local pointDefences = sam:getPointDefences() for j = 1, #pointDefences do @@ -930,21 +967,12 @@ function SkynetIADS:setupSAMSitesAndThenActivate(setupTime) pointDefence:goLive() end end - self.samSetupMistTaskID = mist.scheduleFunction(SkynetIADS.postSetupSAMSites, {self}, timer.getTime() + self.samSetupTime) -end - -function SkynetIADS.postSetupSAMSites(self) - local samSites = self:getSAMSites() - for i = 1, #samSites do - local sam = samSites[i] - --turn on the scan again otherwise SAMs that fired a missile while in setup will not turn off anymore - sam:scanForHarms() - end - self:activate() + self.samSetupMistTaskID = mist.scheduleFunction(SkynetIADS.activate, {self}, timer.getTime() + self.samSetupTime) end function SkynetIADS:deactivate() mist.removeFunction(self.ewRadarScanMistTaskID) + mist.removeFunction(self.samSetupMistTaskID) self:deativateSAMSites() self:deactivateEarlyWarningRadars() self:deactivateCommandCenters() @@ -1062,7 +1090,7 @@ function SkynetIADS:printDetailedEarlyWarningRadarStatus() local intactPowerSources = numPowerSources - numDamagedPowerSources local detectedTargets = ewRadar:getDetectedTargets() - local samSitesInCoveredArea = ewRadar:getSAMSitesInCoveredArea() + local samSitesInCoveredArea = ewRadar:getChildRadars() local unitName = "DESTROYED" @@ -1137,7 +1165,7 @@ function SkynetIADS:printDetailedSAMSiteStatus() local detectedTargets = samSite:getDetectedTargets() - local samSitesInCoveredArea = samSite:getSAMSitesInCoveredArea() + local samSitesInCoveredArea = samSite:getChildRadars() env.info("GROUP: "..samSite:getDCSName().." | TYPE: "..samSite:getNatoName()) env.info("ACTIVE: "..tostring(isActive).." | AUTONOMOUS: "..tostring(isAutonomous).." | IS ACTING AS EW: "..tostring(samSite:getActAsEW()).." | DETECTED TARGETS: "..#detectedTargets.." | DEFENDING HARM: "..tostring(samSite:isDefendingHARM()).." | MISSILES IN FLIGHT:"..tostring(samSite:getNumberOfMissilesInFlight())) @@ -1180,29 +1208,29 @@ function SkynetIADS:printSystemStatus() if self:getDebugSettings().IADSStatus then - local numComCenters = #self.commandCenters - local numIntactComCenters = 0 + local numComCenters = #self:getCommandCenters() local numDestroyedComCenters = 0 local numComCentersNoPower = 0 - local numComCentersServingIADS = 0 + local numComCentersNoConnectionNode = 0 + local numIntactComCenters = 0 for i = 1, #self.commandCenters do local commandCenter = self.commandCenters[i] if commandCenter:hasWorkingPowerSource() == false then numComCentersNoPower = numComCentersNoPower + 1 end + if commandCenter:hasActiveConnectionNode() == false then + numComCentersNoConnectionNode = numComCentersNoConnectionNode + 1 + end if commandCenter:isDestroyed() == false then numIntactComCenters = numIntactComCenters + 1 end - if commandCenter:isDestroyed() == false and commandCenter:hasWorkingPowerSource() then - numComCentersServingIADS = numComCentersServingIADS + 1 - end end numDestroyedComCenters = numComCenters - numIntactComCenters - self:printOutput("COMMAND CENTERS: Serving IADS: "..numComCentersServingIADS.." | Total: "..numComCenters.." | Intact: "..numIntactComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPower: "..numComCentersNoPower) - + self:printOutput("COMMAND CENTERS: "..numComCenters.." | Destroyed: "..numDestroyedComCenters.." | NoPowr: "..numComCentersNoPower.." | NoCon: "..numComCentersNoConnectionNode) + local ewNoPower = 0 local ewTotal = #self:getEarlyWarningRadars() local ewNoConnectionNode = 0 @@ -1224,7 +1252,7 @@ function SkynetIADS:printSystemStatus() ewRadarsInactive = ewTotal - ewActive local numEWRadarsDestroyed = #self:getDestroyedEarlyWarningRadars() - self:printOutput("EW: "..ewTotal.." | Act: "..ewActive.." | Inact: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) + self:printOutput("EW: "..ewTotal.." | On: "..ewActive.." | Off: "..ewRadarsInactive.." | Destroyed: "..numEWRadarsDestroyed.." | NoPowr: "..ewNoPower.." | NoCon: "..ewNoConnectionNode) local samSitesInactive = 0 local samSitesActive = 0 @@ -1257,7 +1285,7 @@ function SkynetIADS:printSystemStatus() end samSitesInactive = samSitesTotal - samSitesActive - self:printOutput("SAM: "..samSitesTotal.." | Act: "..samSitesActive.." | Inact: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) + self:printOutput("SAM: "..samSitesTotal.." | On: "..samSitesActive.." | Off: "..samSitesInactive.." | Autonm: "..samSiteAutonomous.." | Raddest: "..samSiteRadarDestroyed.." | NoPowr: "..samSitesNoPower.." | NoCon: "..samSitesNoConnectionNode.." | NoAmmo: "..samSitesOutOfAmmo) end if self:getDebugSettings().contacts then for i = 1, #self.contacts do @@ -1378,19 +1406,17 @@ function SkynetIADSAbstractDCSObjectWrapper:create(dcsObject) setmetatable(instance, self) self.__index = self instance.dcsObject = dcsObject - if dcsObject and dcsObject:isExist() and getmetatable(dcsObject) == Unit then - --we store inital life here, because getLife0() returs a value that is lower that getLife() when no damage has happened... - instance.initialLife = dcsObject:getLife() - end + instance.name = dcsObject:getName() + instance.typeName = dcsObject:getTypeName() return instance end function SkynetIADSAbstractDCSObjectWrapper:getName() - return self.dcsObject:getName() + return self.name end function SkynetIADSAbstractDCSObjectWrapper:getTypeName() - return self.dcsObject:getTypeName() + return self.typeName end function SkynetIADSAbstractDCSObjectWrapper:getPosition() @@ -1405,20 +1431,12 @@ function SkynetIADSAbstractDCSObjectWrapper:isExist() end end -function SkynetIADSAbstractDCSObjectWrapper:getLifePercentage() - if self.dcsObject and self.dcsObject:isExist() then - return self.dcsObject:getLife() / self.initialLife * 100 - else - return 0 - end - -end - function SkynetIADSAbstractDCSObjectWrapper:getDCSRepresentation() return self.dcsObject end end + do SkynetIADSAbstractElement = {} @@ -1451,6 +1469,7 @@ end function SkynetIADSAbstractElement:addPowerSource(powerSource) table.insert(self.powerSources, powerSource) + self:informChildrenOfStateChange() return self end @@ -1460,7 +1479,7 @@ end function SkynetIADSAbstractElement:addConnectionNode(connectionNode) table.insert(self.connectionNodes, connectionNode) - self.iads:updateAutonomousStatesOfSAMSites() + self:informChildrenOfStateChange() return self end @@ -1526,11 +1545,10 @@ function SkynetIADSAbstractElement:onEvent(event) if event.id == world.event.S_EVENT_DEAD then if self:hasWorkingPowerSource() == false or self:isDestroyed() then self:goDark() - self.iads:updateAutonomousStatesOfSAMSites(event.initiator) + self:informChildrenOfStateChange() end if self:hasActiveConnectionNode() == false then - self:goAutonomous() - self.iads:updateAutonomousStatesOfSAMSites(event.initiator) + self:informChildrenOfStateChange() end end if event.id == world.event.S_EVENT_SHOT then @@ -1553,6 +1571,16 @@ function SkynetIADSAbstractElement:goAutonomous() end +--placeholder method, can be implemented by subclasses +function SkynetIADSAbstractElement:setToCorrectAutonomousState() + +end + +--placeholder method, can be implemented by subclasses +function SkynetIADSAbstractElement:informChildrenOfStateChange() + +end + -- helper code for class inheritance function inheritsFrom( baseClass ) @@ -1626,13 +1654,14 @@ function SkynetIADSAbstractRadarElement:create(dcsElementWithRadar, iads) instance.launchers = {} instance.trackingRadars = {} instance.searchRadars = {} - instance.samSitesInCoveredArea = {} + instance.parentRadars = {} + instance.childRadars = {} instance.missilesInFlight = {} instance.pointDefences = {} instance.ingnoreHARMSWhilePointDefencesHaveAmmo = false instance.autonomousBehaviour = SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI instance.goLiveRange = SkynetIADSAbstractRadarElement.GO_LIVE_WHEN_IN_KILL_ZONE - instance.isAutonomous = false + instance.isAutonomous = true instance.harmDetectionChance = 0 instance.minHarmShutdownTime = 0 instance.maxHarmShutDownTime = 0 @@ -1687,21 +1716,108 @@ function SkynetIADSAbstractRadarElement:getPointDefences() return self.pointDefences end - -function SkynetIADSAbstractRadarElement:updateSAMSitesInCoveredArea() - local samSites = self.iads:getUsableSAMSites() - self.samSitesInCoveredArea = {} - for i = 1, #samSites do - local samSite = samSites[i] - if samSite:isInRadarDetectionRangeOf(self) and samSite ~= self then - table.insert(self.samSitesInCoveredArea, samSite) - end - end - return self.samSitesInCoveredArea +function SkynetIADSAbstractRadarElement:addParentRadar(parentRadar) + self:abstractAddRadar(parentRadar, self.parentRadars) + self:informChildrenOfStateChange() end -function SkynetIADSAbstractRadarElement:getSAMSitesInCoveredArea() - return self.samSitesInCoveredArea +function SkynetIADSAbstractRadarElement:getParentRadars() + return self.parentRadars +end + +function SkynetIADSAbstractRadarElement:clearParentRadars() + self.parentRadars = {} +end + +function SkynetIADSAbstractRadarElement:abstractAddRadar(radar, tbl) +local isAdded = false + for i = 1, #tbl do + local child = tbl[i] + if child == radar then + isAdded = true + end + end + if isAdded == false then + table.insert(tbl, radar) + end +end + +function SkynetIADSAbstractRadarElement:addChildRadar(childRadar) + self:abstractAddRadar(childRadar, self.childRadars) +end + +function SkynetIADSAbstractRadarElement:getChildRadars() + return self.childRadars +end + +function SkynetIADSAbstractRadarElement:clearChildRadars() + self.childRadars = {} +end + +--TODO: unit test this method +function SkynetIADSAbstractRadarElement:getUsableChildRadars() + local usableRadars = {} + for i = 1, #self.childRadars do + local childRadar = self.childRadars[i] + if childRadar:hasWorkingPowerSource() and childRadar:hasActiveConnectionNode() then + table.insert(usableRadars, childRadar) + end + end + return usableRadars +end + +function SkynetIADSAbstractRadarElement:informChildrenOfStateChange() + self:setToCorrectAutonomousState() + local children = self:getChildRadars() + for i = 1, #children do + local childRadar = children[i] + childRadar:setToCorrectAutonomousState() + end + self.iads:getMooseConnector():update() +end + +function SkynetIADSAbstractRadarElement:setToCorrectAutonomousState() + local parents = self:getParentRadars() + for i = 1, #parents do + local parent = parents[i] + --of one parent exists that still is connected to the IADS, the SAM site does not have to go autonomous + --instead of isDestroyed() write method, hasWorkingSearchRadars() + if self:hasActiveConnectionNode() and self.iads:isCommandCenterUsable() and parent:hasWorkingPowerSource() and parent:hasActiveConnectionNode() and parent:getActAsEW() == true and parent:isDestroyed() == false then + self:resetAutonomousState() + return + end + end + self:goAutonomous() +end + + +function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode) + if mode ~= nil then + self.autonomousBehaviour = mode + end + return self +end + +function SkynetIADSAbstractRadarElement:getAutonomousBehaviour() + return self.autonomousBehaviour +end + +function SkynetIADSAbstractRadarElement:resetAutonomousState() + self.isAutonomous = false + self:goDark() +end + +function SkynetIADSAbstractRadarElement:goAutonomous() + self.isAutonomous = true + if self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK then + self:goDark() + else + self:goLive() + end +end + +function SkynetIADSAbstractRadarElement:getAutonomousState() + return self.isAutonomous end function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumberOfMissiles) @@ -1717,7 +1833,7 @@ function SkynetIADSAbstractRadarElement:pointDefencesHaveRemainingAmmo(minNumber return returnValue end -function SkynetIADSAbstractElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) +function SkynetIADSAbstractRadarElement:pointDefencesHaveEnoughLaunchers(minNumberOfLaunchers) local numOfLaunchers = 0 for i = 1, #self.pointDefences do local pointDefence = self.pointDefences[i] @@ -1730,7 +1846,7 @@ function SkynetIADSAbstractElement:pointDefencesHaveEnoughLaunchers(minNumberOfL return returnValue end -function SkynetIADSAbstractElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) +function SkynetIADSAbstractRadarElement:setIgnoreHARMSWhilePointDefencesHaveAmmo(state) if state == true or state == false then self.ingnoreHARMSWhilePointDefencesHaveAmmo = state end @@ -1770,7 +1886,14 @@ end function SkynetIADSAbstractRadarElement:setActAsEW(ewState) if ewState == true or ewState == false then + local stateChange = false + if ewState ~= self.actAsEW then + stateChange = true + end self.actAsEW = ewState + if stateChange then + self:informChildrenOfStateChange() + end end if self.actAsEW == true then self:goLive() @@ -1967,7 +2090,6 @@ end function SkynetIADSAbstractRadarElement:goLive() if ( self.aiState == false and self:hasWorkingPowerSource() and self.harmSilenceID == nil) - and ( (self.isAutonomous == false) or (self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DCS_AI ) ) and (self:hasRemainingAmmo() == true ) then if self:isDestroyed() == false then @@ -1976,8 +2098,8 @@ function SkynetIADSAbstractRadarElement:goLive() cont:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) cont:setOption(AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE) self.goLiveTime = timer.getTime() + self.aiState = true end - self.aiState = true self:pointDefencesStopActingAsEW() if self.iads:getDebugSettings().radarWentLive then self.iads:printOutput(self:getDescription().." going live") @@ -1994,21 +2116,9 @@ function SkynetIADSAbstractRadarElement:pointDefencesStopActingAsEW() end -function SkynetIADSAbstractRadarElement:noDamageToRadars() - local radars = self:getRadars() - for i = 1, #radars do - local radar = radars[i] - if radar:getLifePercentage() < 100 then - return false - end - end - return true -end - function SkynetIADSAbstractRadarElement:goDark() - if ( self.aiState == true ) + if (self:hasWorkingPowerSource() == false) or ( self.aiState == true ) and (self.harmSilenceID ~= nil or ( self.harmSilenceID == nil and #self:getDetectedTargets() == 0 and self:hasMissilesInFlight() == false) or ( self.harmSilenceID == nil and #self:getDetectedTargets() > 0 and self:hasMissilesInFlight() == false and self:hasRemainingAmmo() == false ) ) - and ( self.isAutonomous == false or ( self.isAutonomous == true and self.autonomousBehaviour == SkynetIADSAbstractRadarElement.AUTONOMOUS_STATE_DARK ) ) then if self:isDestroyed() == false then local controller = self:getController() @@ -2108,36 +2218,6 @@ function SkynetIADSAbstractRadarElement:getDistanceToUnit(unitPosA, unitPosB) return mist.utils.round(mist.utils.get2DDist(unitPosA, unitPosB, 0)) end -function SkynetIADSAbstractRadarElement:setAutonomousBehaviour(mode) - if mode ~= nil then - self.autonomousBehaviour = mode - end - return self -end - -function SkynetIADSAbstractRadarElement:getAutonomousBehaviour() - return self.autonomousBehaviour -end - -function SkynetIADSAbstractRadarElement:resetAutonomousState() - if self.isAutonomous == true then - self.isAutonomous = false - self:goDark() - end -end - -function SkynetIADSAbstractRadarElement:goAutonomous() - if self.isAutonomous == false then - self.isAutonomous = true - self:goDark() - self:goLive() - end -end - -function SkynetIADSAbstractRadarElement:getAutonomousState() - return self.isAutonomous -end - function SkynetIADSAbstractRadarElement:hasWorkingRadar() local radars = self:getRadars() for i = 1, #radars do @@ -2176,11 +2256,11 @@ function SkynetIADSAbstractRadarElement:scanForHarms() self.harmScanID = mist.scheduleFunction(SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs, {self}, 1, 2) end -function SkynetIADSAbstractElement:isScanningForHARMs() +function SkynetIADSAbstractRadarElement:isScanningForHARMs() return self.harmScanID ~= nil end -function SkynetIADSAbstractElement:isDefendingHARM() +function SkynetIADSAbstractRadarElement:isDefendingHARM() return self.harmSilenceID ~= nil end @@ -2191,7 +2271,7 @@ end function SkynetIADSAbstractRadarElement:goSilentToEvadeHARM(timeToImpact) self:finishHarmDefence(self) - self.objectsIdentifiedAsHarms = {} + --self.objectsIdentifiedAsHarms = {} local harmTime = self:getHarmShutDownTime() if self.iads:getDebugSettings().harmDefence then self.iads:printOutput("HARM DEFENCE: "..self:getDCSName().." shutting down | FOR: "..harmTime.." seconds | TTI: "..timeToImpact) @@ -2281,20 +2361,23 @@ end function SkynetIADSAbstractRadarElement:cleanUpOldObjectsIdentifiedAsHARMS() local validObjects = {} + local validCount = 0 for unitName, unit in pairs(self.objectsIdentifiedAsHarms) do local harm = unit['target'] if harm:getAge() <= self.objectsIdentifiedAsHarmsMaxTargetAge then validObjects[harm:getName()] = {} validObjects[harm:getName()]['target'] = harm validObjects[harm:getName()]['count'] = unit['count'] + validCount = validCount + 1 end - end - self.objectsIdentifiedAsHarms = validObjects - - --stop point defences acting as ew (always on), will occur of activated via shallIgnoreHARMShutdown() in evaluateIfTargetsContainHARMs - if self:getNumberOfObjectsItentifiedAsHARMS() == 0 then + end + --stop point defences acting as ew (always on), will occur if activated via shallIgnoreHARMShutdown() in evaluateIfTargetsContainHARMs + --if in this iteration all harms where cleared we turn of the point defence. But in any other cases we dont turn of point defences, that interferes with other parts of the iads + -- when setting up the iads (letting pds go to read state) + if validCount == 0 and self:getNumberOfObjectsItentifiedAsHARMS() > 0 then self:pointDefencesStopActingAsEW() end + self.objectsIdentifiedAsHarms = validObjects end @@ -2306,7 +2389,7 @@ function SkynetIADSAbstractRadarElement.evaluateIfTargetsContainHARMs(self) self.lastJammerUpdate = 0 end - --we use the regular interval of this method to update to other states: + --we use the regular interval of this method to update to other states: self:updateMissilesInFlight() self:cleanUpOldObjectsIdentifiedAsHARMS() @@ -2374,6 +2457,7 @@ function SkynetIADSAWACSRadar:create(radarUnit, iads) setmetatable(instance, self) self.__index = self instance.lastUpdatePosition = nil + instance.natoName = radarUnit:getTypeName() return instance end @@ -2384,9 +2468,6 @@ function SkynetIADSAWACSRadar:setupElements() table.insert(self.searchRadars, radar) end -function SkynetIADSAWACSRadar:getNatoName() - return self:getDCSRepresentation():getTypeName() -end -- AWACs will not scan for HARMS function SkynetIADSAWACSRadar:scanForHarms() @@ -2394,12 +2475,18 @@ function SkynetIADSAWACSRadar:scanForHarms() end function SkynetIADSAWACSRadar:getMaxAllowedMovementForAutonomousUpdateInNM() - local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget()) - return mist.utils.round(radarRange / 10) + --local radarRange = mist.utils.metersToNM(self.searchRadars[1]:getMaxRangeFindingTarget()) + --return mist.utils.round(radarRange / 10) + --fixed to 10 nm miles to better fit small SAM sites + return 10 end function SkynetIADSAWACSRadar:isUpdateOfAutonomousStateOfSAMSitesRequired() - return self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM() + local isUpdateRequired = self:getDistanceTraveledSinceLastUpdate() > self:getMaxAllowedMovementForAutonomousUpdateInNM() + if isUpdateRequired then + self.lastUpdatePosition = nil + end + return isUpdateRequired end function SkynetIADSAWACSRadar:getDistanceTraveledSinceLastUpdate() @@ -2417,7 +2504,7 @@ end do SkynetIADSCommandCenter = {} -SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractElement) +SkynetIADSCommandCenter = inheritsFrom(SkynetIADSAbstractRadarElement) function SkynetIADSCommandCenter:create(commandCenter, iads) local instance = self:superClass():create(commandCenter, iads) @@ -2427,6 +2514,14 @@ function SkynetIADSCommandCenter:create(commandCenter, iads) return instance end +function SkynetIADSCommandCenter:goDark() + +end + +function SkynetIADSCommandCenter:goLive() + +end + end do @@ -2440,21 +2535,12 @@ function SkynetIADSContact:create(dcsRadarTarget) instance.firstContactTime = timer.getAbsTime() instance.lastTimeSeen = 0 instance.dcsRadarTarget = dcsRadarTarget - instance.name = instance.dcsObject:getName() - instance.typeName = instance.dcsObject:getTypeName() instance.position = instance.dcsObject:getPosition() instance.numOfTimesRefreshed = 0 instance.speed = 0 return instance end -function SkynetIADSContact:getName() - return self.name -end - -function SkynetIADSContact:getTypeName() - return self.typeName -end function SkynetIADSContact:isTypeKnown() return self.dcsRadarTarget.type @@ -2520,6 +2606,17 @@ function SkynetIADSEWRadar:create(radarUnit, iads) return instance end +--an Early Warning Radar has simplified check to detrmine if its autonomous or not +function SkynetIADSEWRadar:setToCorrectAutonomousState() + if self:hasActiveConnectionNode() and self:hasWorkingPowerSource() and self.iads:isCommandCenterUsable() then + self:resetAutonomousState() + self:goLive() + end + if self:hasActiveConnectionNode() == false or self.iads:isCommandCenterUsable() == false then + self:goAutonomous() + end +end + end do diff --git a/resources/stylesheets/style-dcs.css b/resources/stylesheets/style-dcs.css index dab1b553..5cf07d77 100644 --- a/resources/stylesheets/style-dcs.css +++ b/resources/stylesheets/style-dcs.css @@ -48,6 +48,21 @@ QMenuBar::item:pressed { background: #1D2731; } +QToolButton:checked { + border-color: #435466; + background: #14202B; +} + +QToolButton:hover { + background: #536476; +} + +QToolBar::separator { + background:white; + width:1px; + height:1px; +} + QMenu::item:selected { background: #435466; } @@ -504,6 +519,40 @@ QWidget[style="baseMenuHeader"]{ color:white; }*/ -QLabel[style="small"]{ +QLabel[style="small"], QLabel[style="text-xs"]{ font-size: 8px; +} + +QLabel[style="text-sm"]{ + font-size: 10px; +} + +QLabel[style="text-md"] { + font-size: 12px; +} + +QLabel[style="text-xl"] { + font-size: 14px; +} + +QFrame[style="QConditionsWidget"] { + margin: 0px; + border: 0px; + padding: 0px; + background: transparent; +} + +QGroupBox[style="QWeatherWidget"] { + padding: 0px; + margin-left: 0px; + margin-right: 5px; + margin-top: 1ex; + margin-bottom: 5px; +} + +QGroupBox[style="QWeatherWidget"] QLabel[style="text-sm"] { + padding: 0px; + margin: 0px; + font-size: 9px; + line-height: 9px; } \ No newline at end of file diff --git a/resources/syrialandmap.p b/resources/syrialandmap.p index 5cbe1025..658394d6 100644 Binary files a/resources/syrialandmap.p and b/resources/syrialandmap.p differ diff --git a/resources/tools/cau_terrain.miz b/resources/tools/cau_terrain.miz index e151b22d..01b84d44 100644 Binary files a/resources/tools/cau_terrain.miz and b/resources/tools/cau_terrain.miz differ diff --git a/resources/tools/channel_terrain.miz b/resources/tools/channel_terrain.miz index 055cfcae..a968dade 100644 Binary files a/resources/tools/channel_terrain.miz and b/resources/tools/channel_terrain.miz differ diff --git a/resources/tools/generate_frontlines.py b/resources/tools/generate_frontlines.py new file mode 100644 index 00000000..c1346bc9 --- /dev/null +++ b/resources/tools/generate_frontlines.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +import argparse +from pathlib import Path +from typing import List, Tuple, Union, Dict + +from dcs.terrain import Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel +from dcs import Mission + +Terrain = Union[Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel] + +SAVE_PATH = Path("resources/frontlines") + +def validate_miz(file_path: Path) -> bool: + return bool(file_path.suffix == ".miz" and file_path.exists()) + +def validate_airports(airports: Tuple[int], terrain: Terrain): + for airport in airports: + if terrain.airport_by_id(airport) is None: + print(f"Cannot load airport for invalid id {airport}") + +def load_files(files) -> List[Mission]: + missions = [] + for file in files: + if validate_miz(file): + mission = Mission() + mission.load_file(file) + missions.append(mission) + else: + print(f"Error: {file} doesn't look like a valid mission file.") + return missions + +def create_frontline_dict(mission: Mission) -> Dict[str, Dict]: + frontline_dict = {} + for group in mission.country("USA").vehicle_group: + groupname = str(group.name).replace(group.name.id, "").replace(":","") + control_points = groupname.split("|") + frontline_dict[groupname] = { + "points": [(i.position.x, i.position.y) for i in group.points], + "start_cp": int(control_points[0]) + } + return frontline_dict + +def process_missions(missions: List[Mission]) -> None: + for mission in missions: + frontline_dict = create_frontline_dict(mission) + write_json(frontline_dict, mission.terrain.name.lower()) + +def write_json(frontline_dict: Dict[str, Dict], terrain_name: str) -> None: + with open(SAVE_PATH.joinpath(terrain_name + ".json"), "w") as file: + json.dump(frontline_dict, file) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Process a miz file to create json descriptions of multi-segment frontlines" + ) + parser.add_argument( + "files", + metavar="N", + type=Path, + nargs="+", + help="A list of space separated .miz files to extract frontlines from", + ) + + args = parser.parse_args() + missions = load_files(args.files) + process_missions(missions) + # frontline_dict = create_frontline_dict(missions[0]) + + print("Done") + + + diff --git a/resources/tools/generate_landmap.py b/resources/tools/generate_landmap.py index 1cdf2931..1812ac4b 100644 --- a/resources/tools/generate_landmap.py +++ b/resources/tools/generate_landmap.py @@ -1,6 +1,7 @@ import pickle from dcs.mission import Mission +from shapely import geometry for terrain in ["cau", "nev", "syria", "channel", "normandy", "gulf"]: print("Terrain " + terrain) @@ -15,16 +16,16 @@ for terrain in ["cau", "nev", "syria", "channel", "normandy", "gulf"]: if terrain == "cau" and inclusion_zones: # legacy - exclusion_zones.append(zone) + exclusion_zones.append(geometry.Polygon(zone)) else: if plane_group.units[0].type == "F-15C": - exclusion_zones.append(zone) + exclusion_zones.append(geometry.Polygon(zone)) else: - inclusion_zones.append(zone) + inclusion_zones.append(geometry.Polygon(zone)) for ship_group in m.country("USA").ship_group: zone = [(x.position.x, x.position.y) for x in ship_group.points] - seas_zones.append(zone) + seas_zones.append(geometry.Polygon(zone)) with open("../{}landmap.p".format(terrain), "wb") as f: print(len(inclusion_zones), len(exclusion_zones), len(seas_zones)) diff --git a/resources/tools/generate_loadout_check.py b/resources/tools/generate_loadout_check.py deleted file mode 100644 index 21a6e997..00000000 --- a/resources/tools/generate_loadout_check.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import sys -import dcs - -from game import db -from gen.aircraft import AircraftConflictGenerator - -dcs.planes.FlyingType.payload_dirs = [os.path.join(os.path.dirname(os.path.realpath(__file__)), "..\\payloads")] - -mis = dcs.Mission(dcs.terrain.PersianGulf()) -pos = dcs.terrain.PersianGulf().khasab().position -airgen = AircraftConflictGenerator(mis, None, None, None) - -for t, uts in db.UNIT_BY_TASK.items(): - if t != dcs.task.CAP and t != dcs.task.CAS: - continue - - pos.y = dcs.terrain.PersianGulf().khasab().position.x - for t in t == dcs.task.CAP and [dcs.task.CAP, dcs.task.Escort] or [t]: - pos.x += 10000 - for ut in uts: - pos.y += 5000 - ctr = mis.country([v["country"] for k, v in db.FACTIONS.items() if ut in v.units][0]) - - g = mis.flight_group_inflight( - country=ctr, - name="{} - {}".format(t.name, ut), - aircraft_type=ut, - position=pos, - altitude=10000 - ) - g.task = t.name - airgen._setup_group(g, t, 0, {}) - -mis.save("loadout_test.miz") diff --git a/resources/tools/gulf_terrain.miz b/resources/tools/gulf_terrain.miz index 6713c833..36cbb941 100644 Binary files a/resources/tools/gulf_terrain.miz and b/resources/tools/gulf_terrain.miz differ diff --git a/resources/tools/normandy_terrain.miz b/resources/tools/normandy_terrain.miz index e848ee4b..9dbc18d4 100644 Binary files a/resources/tools/normandy_terrain.miz and b/resources/tools/normandy_terrain.miz differ diff --git a/resources/ui/conditions/timeofday/dawn.png b/resources/ui/conditions/timeofday/dawn.png new file mode 100644 index 00000000..f2be3bc0 Binary files /dev/null and b/resources/ui/conditions/timeofday/dawn.png differ diff --git a/resources/ui/conditions/timeofday/day.png b/resources/ui/conditions/timeofday/day.png new file mode 100644 index 00000000..f215867e Binary files /dev/null and b/resources/ui/conditions/timeofday/day.png differ diff --git a/resources/ui/conditions/timeofday/dusk.png b/resources/ui/conditions/timeofday/dusk.png new file mode 100644 index 00000000..152f98e1 Binary files /dev/null and b/resources/ui/conditions/timeofday/dusk.png differ diff --git a/resources/ui/conditions/timeofday/night.png b/resources/ui/conditions/timeofday/night.png new file mode 100644 index 00000000..3ff11ed9 Binary files /dev/null and b/resources/ui/conditions/timeofday/night.png differ diff --git a/resources/ui/conditions/weather/day-clear.png b/resources/ui/conditions/weather/day-clear.png new file mode 100644 index 00000000..f215867e Binary files /dev/null and b/resources/ui/conditions/weather/day-clear.png differ diff --git a/resources/ui/conditions/weather/day-cloudy-fog.png b/resources/ui/conditions/weather/day-cloudy-fog.png new file mode 100644 index 00000000..ebee0024 Binary files /dev/null and b/resources/ui/conditions/weather/day-cloudy-fog.png differ diff --git a/resources/ui/conditions/weather/day-fog.png b/resources/ui/conditions/weather/day-fog.png new file mode 100644 index 00000000..c2868d7b Binary files /dev/null and b/resources/ui/conditions/weather/day-fog.png differ diff --git a/resources/ui/conditions/weather/day-partly-cloudy.png b/resources/ui/conditions/weather/day-partly-cloudy.png new file mode 100644 index 00000000..6eb07751 Binary files /dev/null and b/resources/ui/conditions/weather/day-partly-cloudy.png differ diff --git a/resources/ui/conditions/weather/day-rain.png b/resources/ui/conditions/weather/day-rain.png new file mode 100644 index 00000000..f79a5289 Binary files /dev/null and b/resources/ui/conditions/weather/day-rain.png differ diff --git a/resources/ui/conditions/weather/day-thunderstorm.png b/resources/ui/conditions/weather/day-thunderstorm.png new file mode 100644 index 00000000..61fcc700 Binary files /dev/null and b/resources/ui/conditions/weather/day-thunderstorm.png differ diff --git a/resources/ui/conditions/weather/day-totally-cloud.png b/resources/ui/conditions/weather/day-totally-cloud.png new file mode 100644 index 00000000..e56dcf90 Binary files /dev/null and b/resources/ui/conditions/weather/day-totally-cloud.png differ diff --git a/resources/ui/conditions/weather/night-clear.png b/resources/ui/conditions/weather/night-clear.png new file mode 100644 index 00000000..eb4da38e Binary files /dev/null and b/resources/ui/conditions/weather/night-clear.png differ diff --git a/resources/ui/conditions/weather/night-cloudy-fog.png b/resources/ui/conditions/weather/night-cloudy-fog.png new file mode 100644 index 00000000..ebee0024 Binary files /dev/null and b/resources/ui/conditions/weather/night-cloudy-fog.png differ diff --git a/resources/ui/conditions/weather/night-fog.png b/resources/ui/conditions/weather/night-fog.png new file mode 100644 index 00000000..bab41451 Binary files /dev/null and b/resources/ui/conditions/weather/night-fog.png differ diff --git a/resources/ui/conditions/weather/night-partly-cloudy.png b/resources/ui/conditions/weather/night-partly-cloudy.png new file mode 100644 index 00000000..9c442a00 Binary files /dev/null and b/resources/ui/conditions/weather/night-partly-cloudy.png differ diff --git a/resources/ui/conditions/weather/night-rain.png b/resources/ui/conditions/weather/night-rain.png new file mode 100644 index 00000000..ac2adbd1 Binary files /dev/null and b/resources/ui/conditions/weather/night-rain.png differ diff --git a/resources/ui/conditions/weather/night-thundersotrm.png b/resources/ui/conditions/weather/night-thundersotrm.png new file mode 100644 index 00000000..7c65921d Binary files /dev/null and b/resources/ui/conditions/weather/night-thundersotrm.png differ diff --git a/resources/ui/conditions/weather/night-totally-cloud.png b/resources/ui/conditions/weather/night-totally-cloud.png new file mode 100644 index 00000000..e56dcf90 Binary files /dev/null and b/resources/ui/conditions/weather/night-totally-cloud.png differ diff --git a/resources/ui/conditions/weather/winds.png b/resources/ui/conditions/weather/winds.png new file mode 100644 index 00000000..06afc80d Binary files /dev/null and b/resources/ui/conditions/weather/winds.png differ diff --git a/resources/ui/daytime/dawn.png b/resources/ui/daytime/dawn.png deleted file mode 100644 index 9e7304ae..00000000 Binary files a/resources/ui/daytime/dawn.png and /dev/null differ diff --git a/resources/ui/daytime/day.png b/resources/ui/daytime/day.png deleted file mode 100644 index 96956e7c..00000000 Binary files a/resources/ui/daytime/day.png and /dev/null differ diff --git a/resources/ui/daytime/dusk.png b/resources/ui/daytime/dusk.png deleted file mode 100644 index 64245a95..00000000 Binary files a/resources/ui/daytime/dusk.png and /dev/null differ diff --git a/resources/ui/daytime/night.png b/resources/ui/daytime/night.png deleted file mode 100644 index b78f084a..00000000 Binary files a/resources/ui/daytime/night.png and /dev/null differ diff --git a/resources/ui/daytime/original/dawn.png b/resources/ui/daytime/original/dawn.png deleted file mode 100644 index 4abc3dc1..00000000 Binary files a/resources/ui/daytime/original/dawn.png and /dev/null differ diff --git a/resources/ui/daytime/original/day.png b/resources/ui/daytime/original/day.png deleted file mode 100644 index 2dc7c886..00000000 Binary files a/resources/ui/daytime/original/day.png and /dev/null differ diff --git a/resources/ui/daytime/original/dusk.png b/resources/ui/daytime/original/dusk.png deleted file mode 100644 index 8a36e461..00000000 Binary files a/resources/ui/daytime/original/dusk.png and /dev/null differ diff --git a/resources/ui/daytime/original/night.png b/resources/ui/daytime/original/night.png deleted file mode 100644 index 361d9d20..00000000 Binary files a/resources/ui/daytime/original/night.png and /dev/null differ diff --git a/resources/ui/ground_assets/missile.png b/resources/ui/ground_assets/missile.png new file mode 100644 index 00000000..29c706fa Binary files /dev/null and b/resources/ui/ground_assets/missile.png differ diff --git a/resources/ui/ground_assets/missile_blue.png b/resources/ui/ground_assets/missile_blue.png new file mode 100644 index 00000000..0e341502 Binary files /dev/null and b/resources/ui/ground_assets/missile_blue.png differ diff --git a/resources/ui/misc/all-flight-paths.png b/resources/ui/misc/all-flight-paths.png new file mode 100644 index 00000000..1e0bae20 Binary files /dev/null and b/resources/ui/misc/all-flight-paths.png differ diff --git a/resources/ui/misc/blue-sam.png b/resources/ui/misc/blue-sam.png new file mode 100644 index 00000000..4be43662 Binary files /dev/null and b/resources/ui/misc/blue-sam.png differ diff --git a/resources/ui/misc/dark/discord.png b/resources/ui/misc/dark/discord.png new file mode 100644 index 00000000..97739171 Binary files /dev/null and b/resources/ui/misc/dark/discord.png differ diff --git a/resources/ui/misc/dark/github.png b/resources/ui/misc/dark/github.png new file mode 100644 index 00000000..84b8dd84 Binary files /dev/null and b/resources/ui/misc/dark/github.png differ diff --git a/resources/ui/misc/detection-sam.png b/resources/ui/misc/detection-sam.png new file mode 100644 index 00000000..033ce349 Binary files /dev/null and b/resources/ui/misc/detection-sam.png differ diff --git a/resources/ui/misc/discord.png b/resources/ui/misc/discord.png new file mode 100644 index 00000000..59371f02 Binary files /dev/null and b/resources/ui/misc/discord.png differ diff --git a/resources/ui/misc/flight-path.png b/resources/ui/misc/flight-path.png new file mode 100644 index 00000000..5e43c639 Binary files /dev/null and b/resources/ui/misc/flight-path.png differ diff --git a/resources/ui/misc/github.png b/resources/ui/misc/github.png new file mode 100644 index 00000000..2668dabd Binary files /dev/null and b/resources/ui/misc/github.png differ diff --git a/resources/ui/misc/hide-flight-path.png b/resources/ui/misc/hide-flight-path.png new file mode 100644 index 00000000..94eb9731 Binary files /dev/null and b/resources/ui/misc/hide-flight-path.png differ diff --git a/resources/ui/misc/light/arrows-h.png b/resources/ui/misc/light/arrows-h.png new file mode 100644 index 00000000..40ccd57e Binary files /dev/null and b/resources/ui/misc/light/arrows-h.png differ diff --git a/resources/ui/misc/light/bullseye.png b/resources/ui/misc/light/bullseye.png new file mode 100644 index 00000000..7fc4519d Binary files /dev/null and b/resources/ui/misc/light/bullseye.png differ diff --git a/resources/ui/misc/light/circle-o-notch.png b/resources/ui/misc/light/circle-o-notch.png new file mode 100644 index 00000000..81290dd0 Binary files /dev/null and b/resources/ui/misc/light/circle-o-notch.png differ diff --git a/resources/ui/misc/light/circle-o.png b/resources/ui/misc/light/circle-o.png new file mode 100644 index 00000000..6bbc1a9e Binary files /dev/null and b/resources/ui/misc/light/circle-o.png differ diff --git a/resources/ui/misc/light/circle-thin.png b/resources/ui/misc/light/circle-thin.png new file mode 100644 index 00000000..e04e126a Binary files /dev/null and b/resources/ui/misc/light/circle-thin.png differ diff --git a/resources/ui/misc/light/circle.png b/resources/ui/misc/light/circle.png new file mode 100644 index 00000000..5ed342b2 Binary files /dev/null and b/resources/ui/misc/light/circle.png differ diff --git a/resources/ui/misc/light/discord.png b/resources/ui/misc/light/discord.png new file mode 100644 index 00000000..59371f02 Binary files /dev/null and b/resources/ui/misc/light/discord.png differ diff --git a/resources/ui/misc/light/dot-circle-o.png b/resources/ui/misc/light/dot-circle-o.png new file mode 100644 index 00000000..7dfd0f51 Binary files /dev/null and b/resources/ui/misc/light/dot-circle-o.png differ diff --git a/resources/ui/misc/light/eraser.png b/resources/ui/misc/light/eraser.png new file mode 100644 index 00000000..730e55ff Binary files /dev/null and b/resources/ui/misc/light/eraser.png differ diff --git a/resources/ui/misc/light/fighter-jet.png b/resources/ui/misc/light/fighter-jet.png new file mode 100644 index 00000000..0bee2732 Binary files /dev/null and b/resources/ui/misc/light/fighter-jet.png differ diff --git a/resources/ui/misc/light/flag.png b/resources/ui/misc/light/flag.png new file mode 100644 index 00000000..6f0a38fd Binary files /dev/null and b/resources/ui/misc/light/flag.png differ diff --git a/resources/ui/misc/light/github.png b/resources/ui/misc/light/github.png new file mode 100644 index 00000000..2668dabd Binary files /dev/null and b/resources/ui/misc/light/github.png differ diff --git a/resources/ui/misc/light/industry.png b/resources/ui/misc/light/industry.png new file mode 100644 index 00000000..cc37442b Binary files /dev/null and b/resources/ui/misc/light/industry.png differ diff --git a/resources/ui/misc/light/info.png b/resources/ui/misc/light/info.png new file mode 100644 index 00000000..67e9088f Binary files /dev/null and b/resources/ui/misc/light/info.png differ diff --git a/resources/ui/misc/light/map-marker.png b/resources/ui/misc/light/map-marker.png new file mode 100644 index 00000000..0f71dfa0 Binary files /dev/null and b/resources/ui/misc/light/map-marker.png differ diff --git a/resources/ui/misc/light/map-o.png b/resources/ui/misc/light/map-o.png new file mode 100644 index 00000000..9474954e Binary files /dev/null and b/resources/ui/misc/light/map-o.png differ diff --git a/resources/ui/misc/light/map.png b/resources/ui/misc/light/map.png new file mode 100644 index 00000000..b140a323 Binary files /dev/null and b/resources/ui/misc/light/map.png differ diff --git a/resources/ui/misc/light/object-group.png b/resources/ui/misc/light/object-group.png new file mode 100644 index 00000000..65ff1573 Binary files /dev/null and b/resources/ui/misc/light/object-group.png differ diff --git a/resources/ui/misc/light/object-ungroup.png b/resources/ui/misc/light/object-ungroup.png new file mode 100644 index 00000000..ddc9fe8d Binary files /dev/null and b/resources/ui/misc/light/object-ungroup.png differ diff --git a/resources/ui/misc/light/plane.png b/resources/ui/misc/light/plane.png new file mode 100644 index 00000000..2c492f64 Binary files /dev/null and b/resources/ui/misc/light/plane.png differ diff --git a/resources/ui/misc/medium/discord.png b/resources/ui/misc/medium/discord.png new file mode 100644 index 00000000..d2a43e0a Binary files /dev/null and b/resources/ui/misc/medium/discord.png differ diff --git a/resources/ui/misc/medium/github.png b/resources/ui/misc/medium/github.png new file mode 100644 index 00000000..9e82b032 Binary files /dev/null and b/resources/ui/misc/medium/github.png differ diff --git a/resources/ui/misc/red-sam.png b/resources/ui/misc/red-sam.png new file mode 100644 index 00000000..bdfd28db Binary files /dev/null and b/resources/ui/misc/red-sam.png differ diff --git a/resources/ui/templates/campaigntemplate_EN.j2 b/resources/ui/templates/campaigntemplate_EN.j2 index 88a210fb..5d721dcc 100644 --- a/resources/ui/templates/campaigntemplate_EN.j2 +++ b/resources/ui/templates/campaigntemplate_EN.j2 @@ -1,7 +1,3 @@ Author(s): {{ campaign.authors }} -
-
-Number of control points: {{ campaign.theater.controlpoints|length }} -
{{ campaign.description|safe }} diff --git a/resources/ui/templates/campaigntemplate_FR.j2 b/resources/ui/templates/campaigntemplate_FR.j2 new file mode 100644 index 00000000..e71616f7 --- /dev/null +++ b/resources/ui/templates/campaigntemplate_FR.j2 @@ -0,0 +1,3 @@ +Auteur(s) : {{ campaign.authors }} + +{{ campaign.description|safe }} diff --git a/resources/ui/templates/factiontemplate_EN.j2 b/resources/ui/templates/factiontemplate_EN.j2 index b1711b42..92e37d85 100644 --- a/resources/ui/templates/factiontemplate_EN.j2 +++ b/resources/ui/templates/factiontemplate_EN.j2 @@ -41,18 +41,10 @@
-Air defenses : +Air defenses:
    -{% for sam in faction.sams %} -
  • {{sam}}
  • +{% for air_defense in faction.air_defenses %} +
  • {{air_defense}}
  • {% endfor %}

- -Short range air defenses : -
    -{% for shorad in faction.shorads %} -
  • {{shorad}}
  • -{% endfor %} -
-
\ No newline at end of file diff --git a/resources/ui/templates/factiontemplate_FR.j2 b/resources/ui/templates/factiontemplate_FR.j2 new file mode 100644 index 00000000..90639c99 --- /dev/null +++ b/resources/ui/templates/factiontemplate_FR.j2 @@ -0,0 +1,50 @@ +{{ faction.description|safe }} +
+ +Auteur(s): {{ faction.authors }} +

+ + +Aéronefs disponibles : +
    +{% for aircraft in faction.aircrafts %} +
  • {{aircraft.id}}
  • +{% endfor %} +
+
+ +Véhicules disponibles : +
    +{% for vehicle in faction.frontline_units %} +
  • +{% if vehicle.name is not none %} + {{ vehicle.name }} +{% else %} + {{ vehicle.id }} +{%endif %} +
  • +{% endfor %} +
+
+ +Pièces d'artillerie : +
    +{% for arty in faction.artillery_units %} +
  • +{% if arty.name is not none %} + {{ arty.name }} +{% else %} + {{ arty.id }} +{%endif %} +
  • +{% endfor %} +
+
+ +Défense Sol-Air: +
    +{% for air_defense in faction.air_defenses %} +
  • {{air_defense}}
  • +{% endfor %} +
+
diff --git a/resources/ui/units/aircrafts/P-47D-30_24.jpg b/resources/ui/units/aircrafts/P-47D-30_24.jpg index b71e5342..870dca1a 100644 Binary files a/resources/ui/units/aircrafts/P-47D-30_24.jpg and b/resources/ui/units/aircrafts/P-47D-30_24.jpg differ diff --git a/resources/ui/units/aircrafts/P-47D-30bl1_24.jpg b/resources/ui/units/aircrafts/P-47D-30bl1_24.jpg new file mode 100644 index 00000000..870dca1a Binary files /dev/null and b/resources/ui/units/aircrafts/P-47D-30bl1_24.jpg differ diff --git a/resources/ui/units/aircrafts/P-47D-40_24.jpg b/resources/ui/units/aircrafts/P-47D-40_24.jpg new file mode 100644 index 00000000..870dca1a Binary files /dev/null and b/resources/ui/units/aircrafts/P-47D-40_24.jpg differ diff --git a/tests/test_factions.py b/tests/test_factions.py index c5de3e94..f802dbf0 100644 --- a/tests/test_factions.py +++ b/tests/test_factions.py @@ -71,9 +71,9 @@ class TestFactionLoader(unittest.TestCase): self.assertIn(Infantry.Infantry_M4, faction.infantry_units) self.assertIn(Infantry.Soldier_M249, faction.infantry_units) - self.assertIn("AvengerGenerator", faction.shorads) + self.assertIn("AvengerGenerator", faction.air_defenses) - self.assertIn("HawkGenerator", faction.sams) + self.assertIn("HawkGenerator", faction.air_defenses) self.assertIn(CVN_74_John_C__Stennis, faction.aircraft_carrier) self.assertIn(LHA_1_Tarawa, faction.helicopter_carrier) diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py deleted file mode 100644 index 373eb959..00000000 --- a/theater/conflicttheater.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING - -from dcs.mapping import Point -from dcs.terrain import ( - caucasus, - nevada, - normandy, - persiangulf, - syria, - thechannel, -) -from dcs.terrain.terrain import Terrain - -from .controlpoint import ControlPoint -from .landmap import Landmap, load_landmap, poly_contains - -if TYPE_CHECKING: - from . import FrontLine - -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 - -""" -ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ] -COAST_NS_E = [45, 90, 135, ] -COAST_EW_N = [315, 0, 45, ] -COAST_NSEW_E = [225, 270, 315, ] -COAST_NSEW_W = [45, 90, 135, ] - -COAST_NS_W = [225, 270, 315, ] -COAST_EW_S = [135, 180, 225, ] -""" - -LAND = [0, 45, 90, 135, 180, 225, 270, 315, ] - -COAST_V_E = [0, 45, 90, 135, 180] -COAST_V_W = [180, 225, 270, 315, 0] - -COAST_A_W = [315, 0, 45, 135, 180, 225, 270] -COAST_A_E = [0, 45, 90, 135, 180, 225, 315] - -COAST_H_N = [270, 315, 0, 45, 90] -COAST_H_S = [90, 135, 180, 225, 270] - -COAST_DL_E = [45, 90, 135, 180, 225] -COAST_DL_W = [225, 270, 315, 0, 45] -COAST_DR_E = [315, 0, 45, 90, 135] -COAST_DR_W = [135, 180, 225, 315] - - -class ConflictTheater: - terrain: Terrain - - reference_points: Dict[Tuple[float, float], Tuple[float, float]] - overview_image: str - landmap: Optional[Landmap] - """ - land_poly = None # type: Polygon - """ - daytime_map: Dict[str, Tuple[int, int]] - - def __init__(self): - self.controlpoints: List[ControlPoint] = [] - """ - 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)) - """ - - 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 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]: - from . import FrontLine # Circular import that needs to be resolved. - 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) - - def enemy_points(self) -> List[ControlPoint]: - return [point for point in self.controlpoints if not point.captured] - - def add_json_cp(self, theater, p: dict) -> ControlPoint: - - if p["type"] == "airbase": - - airbase = theater.terrain.airports[p["id"]].__class__ - - if "radials" in p.keys(): - radials = p["radials"] - else: - radials = LAND - - 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 = ControlPoint.from_airport(airbase, radials, size, importance) - elif p["type"] == "carrier": - cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"]) - else: - cp = ControlPoint.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(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() - 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 = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), - (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } - - 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 = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - 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 = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), - (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } - 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 = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), - (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} - 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 = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), - (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} - 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 = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), - (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} - landmap = load_landmap("resources\\syrialandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } diff --git a/theater/controlpoint.py b/theater/controlpoint.py deleted file mode 100644 index f7514b71..00000000 --- a/theater/controlpoint.py +++ /dev/null @@ -1,239 +0,0 @@ -from __future__ import annotations - -import itertools -import re -from typing import Dict, List, TYPE_CHECKING -from enum import Enum - -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 - -from game import db -from gen.ground_forces.combat_stance import CombatStance -from .base import Base -from .missiontarget import MissionTarget -from .theatergroundobject import ( - BaseDefenseGroundObject, - SamGroundObject, - TheaterGroundObject, -) - -if TYPE_CHECKING: - from game import Game - - -class ControlPointType(Enum): - AIRBASE = 0 # An airbase with slots for everything - AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible) - LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier) - FARP = 4 # A FARP, with slots for helicopters - FOB = 5 # A FOB (ground units only) - - -class ControlPoint(MissionTarget): - - position = None # type: Point - name = None # type: str - - captured = False - has_frontline = True - frontline_offset = 0.0 - - alt = 0 - - def __init__(self, id: int, name: str, position: Point, - at: db.StartingPosition, radials: List[int], size: int, - importance: float, has_frontline=True, - cptype=ControlPointType.AIRBASE): - super().__init__(" ".join(re.split(r" |-", name)[:2]), position) - self.id = id - self.full_name = name - self.at = at - self.connected_objectives: List[TheaterGroundObject] = [] - self.base_defenses: List[BaseDefenseGroundObject] = [] - - self.size = size - self.importance = importance - self.captured = False - self.captured_invert = False - self.has_frontline = has_frontline - self.radials = radials - self.connected_points: List[ControlPoint] = [] - self.base: Base = Base() - self.cptype = cptype - self.stances: Dict[int, CombatStance] = {} - self.airport = None - - @property - def ground_objects(self) -> List[TheaterGroundObject]: - return list( - itertools.chain(self.connected_objectives, self.base_defenses)) - - @classmethod - def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): - assert airport - obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE) - obj.airport = airport() - return obj - - @classmethod - def carrier(cls, name: str, at: Point, id: int): - import theater.conflicttheater - cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, - has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) - return cp - - @classmethod - def lha(cls, name: str, at: Point, id: int): - import theater.conflicttheater - cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, - has_frontline=False, cptype=ControlPointType.LHA_GROUP) - return cp - - @property - def heading(self): - if self.cptype == ControlPointType.AIRBASE: - return self.airport.runways[0].heading - elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]: - return 0 # TODO compute heading - else: - return 0 - - 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 self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] - - @property - def is_fleet(self): - """ - :return: Whether this control point is a boat (mobile) - """ - return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] - - @property - def is_lha(self): - """ - :return: Whether this control point is an LHA - """ - return self.cptype in [ControlPointType.LHA_GROUP] - - @property - def sea_radials(self) -> List[int]: - # TODO: fix imports - all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ] - result = [] - for r in all_radials: - if r not in self.radials: - result.append(r) - return result - - @property - def available_aircraft_slots(self): - """ - :return: The maximum number of aircraft that can be stored in this control point - """ - if self.cptype == ControlPointType.AIRBASE: - return len(self.airport.parking_slots) - elif self.is_lha: - return 20 - elif self.is_carrier: - return 90 - else: - return 0 - - def connect(self, to): - self.connected_points.append(to) - self.stances[to.id] = CombatStance.DEFENSIVE - - def has_runway(self): - """ - Check whether this control point can have aircraft taking off or landing. - :return: - """ - if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] : - 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 - elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]: - return True - else: - return True - - 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 - - def is_connected(self, to) -> bool: - return to in self.connected_points - - def find_radial(self, heading: int, ignored_radial: int = None) -> int: - closest_radial = 0 - closest_radial_delta = 360 - for radial in [x for x in self.radials if x != ignored_radial]: - delta = abs(radial - heading) - if delta < closest_radial_delta: - closest_radial = radial - closest_radial_delta = delta - - return closest_radial - - 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 - - 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 = {} - - # Handle cyclic dependency. - from .start_generator import BaseDefenseGenerator - self.base_defenses = [] - BaseDefenseGenerator(game, self).generate() diff --git a/theater/frontline.py b/theater/frontline.py deleted file mode 100644 index c70b3417..00000000 --- a/theater/frontline.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Battlefield front lines.""" -from typing import Tuple - -from dcs.mapping import Point -from . import ControlPoint, MissionTarget - -# TODO: Dedup by moving everything to using this class. -FRONTLINE_MIN_CP_DISTANCE = 5000 - - -def compute_position(control_point_a: ControlPoint, - control_point_b: ControlPoint) -> Point: - a = control_point_a.position - b = control_point_b.position - attack_heading = a.heading_between_point(b) - attack_distance = a.distance_to_point(b) - middle_point = a.point_from_heading(attack_heading, attack_distance / 2) - - strength_delta = float(control_point_a.base.strength - - control_point_b.base.strength) - position = middle_point.point_from_heading(attack_heading, - strength_delta * - attack_distance / 2 - - FRONTLINE_MIN_CP_DISTANCE) - return position - - -class FrontLine(MissionTarget): - """Defines a front line location between two control points. - - Front lines are the area where ground combat happens. - """ - - def __init__(self, control_point_a: ControlPoint, - control_point_b: ControlPoint) -> None: - super().__init__(f"Front line {control_point_a}/{control_point_b}", - compute_position(control_point_a, control_point_b)) - self.control_point_a = control_point_a - self.control_point_b = control_point_b - - @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 diff --git a/theater/landmap.py b/theater/landmap.py deleted file mode 100644 index 0f7395c8..00000000 --- a/theater/landmap.py +++ /dev/null @@ -1,41 +0,0 @@ -import pickle -from typing import Collection, Optional, Tuple -import logging - -Zone = Collection[Tuple[float, float]] -Landmap = Tuple[Collection[Zone], Collection[Zone], Collection[Zone]] - - -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): - n = len(poly) - inside = False - xints = 0.0 - p1x, p1y = poly[0] - for i in range(n+1): - p2x, p2y = poly[i % n] - if y > min(p1y, p2y): - if y <= max(p1y, p2y): - if x <= max(p1x, p2x): - if p1y != p2y: - xints = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x - if p1x == p2x or x <= xints: - inside = not inside - p1x, p1y = p2x, p2y - return inside - -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/theater/missiontarget.py b/theater/missiontarget.py deleted file mode 100644 index fb4da0f3..00000000 --- a/theater/missiontarget.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -from dcs.mapping import Point - - -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) diff --git a/theater/start_generator.py b/theater/start_generator.py deleted file mode 100644 index eb0252c4..00000000 --- a/theater/start_generator.py +++ /dev/null @@ -1,612 +0,0 @@ -from __future__ import annotations - -import logging -import math -import pickle -import random -from typing import Any, Dict, List, Optional - -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.settings import Settings -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 PresetLocationFinder -from gen.locations.preset_locations import PresetLocation -from gen.missiles.missiles_group_generator import generate_missile_group -from gen.sam.sam_group_generator import ( - generate_anti_air_group, - generate_ewr_group, generate_shorad_group, -) -from theater import ( - ConflictTheater, - ControlPoint, - ControlPointType, - TheaterGroundObject, -) -from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW -from theater.theatergroundobject import ( - EwrGroundObject, SamGroundObject, BuildingGroundObject, CarrierGroundObject, - LhaGroundObject, - MissileSiteGroundObject, ShipGroundObject, -) - -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, -} - - -class GameGenerator: - def __init__(self, player: str, enemy: str, theater: ConflictTheater, - settings: Settings, start_date, starting_budget: int, - multiplier: float, midgame: bool) -> None: - self.player = player - self.enemy = enemy - self.theater = theater - self.settings = settings - self.start_date = start_date - self.starting_budget = starting_budget - self.multiplier = multiplier - self.midgame = midgame - - def generate(self) -> Game: - # Reset name generator - namegen.reset() - self.prepare_theater() - self.populate_red_airbases() - - game = Game(player_name=self.player, - enemy_name=self.enemy, - theater=self.theater, - start_date=self.start_date, - settings=self.settings) - - GroundObjectGenerator(game).generate() - game.budget = self.starting_budget - game.settings.multiplier = self.multiplier - game.settings.sams = True - game.settings.version = VERSION - return game - - def prepare_theater(self) -> None: - to_remove = [] - - # Auto-capture half the bases if midgame. - if self.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: - no_carrier = self.settings.do_not_generate_carrier - no_lha = self.settings.do_not_generate_lha - if cp.cptype is ControlPointType.AIRCRAFT_CARRIER_GROUP and \ - no_carrier: - to_remove.append(cp) - elif cp.cptype is ControlPointType.LHA_GROUP and no_lha: - to_remove.append(cp) - - if self.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.midgame and self.settings.inverted: - for i, cp in enumerate(reversed(self.theater.controlpoints)): - if i > len(self.theater.controlpoints): - break - else: - cp.captured = True - - def populate_red_airbases(self) -> None: - for control_point in self.theater.enemy_points(): - if control_point.captured: - continue - self.populate_red_airbase(control_point) - - def populate_red_airbase(self, control_point: ControlPoint) -> None: - # Force reset cp on generation - control_point.base.aircraft = {} - control_point.base.armor = {} - control_point.base.aa = {} - control_point.base.commision_points = {} - control_point.base.strength = 1 - - for task in [PinpointStrike, CAP, CAS, AirDefence]: - if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW: - raise ValueError( - f"CP importance must be between {IMPORTANCE_LOW} and " - f"{IMPORTANCE_HIGH}, is {control_point.importance}") - - importance_factor = ((control_point.importance - IMPORTANCE_LOW) / - (IMPORTANCE_HIGH - IMPORTANCE_LOW)) - # noinspection PyTypeChecker - unit_types = db.choose_units(task, importance_factor, UNIT_VARIETY, - self.enemy) - if not unit_types: - continue - - count_log = math.log(control_point.importance + 0.01, - UNIT_COUNT_IMPORTANCE_LOG) - count = max( - COUNT_BY_TASK[task] * self.multiplier * (1 + count_log), 1 - ) - - count_per_type = max(int(float(count) / len(unit_types)), 1) - for unit_type in unit_types: - control_point.base.commision_units({unit_type: count_per_type}) - - -class ControlPointGroundObjectGenerator: - def __init__(self, game: Game, control_point: ControlPoint) -> None: - self.game = game - self.control_point = control_point - self.preset_locations = PresetLocationFinder.compute_possible_locations(game.theater.terrain.name, control_point.full_name) - - @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.game.settings.do_not_generate_player_navy - if self.control_point.captured and skip_player_navy: - return - - skip_enemy_navy = self.game.settings.do_not_generate_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 = find_location(False, self.control_point.position, - self.game.theater, 5000, 40000, [], False) - if point is None: - logging.error( - f"Could not find point for {self.control_point}'s navy") - 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) - - def pick_preset_location(self, offshore=False) -> Optional[PresetLocation]: - """ - Return a preset location if any is setup and still available for this point - @:param offshore Whether this should be an offshore location - @:return The preset location if found; None if it couldn't be found - """ - if offshore: - if len(self.preset_locations.offshore_locations) > 0: - location = random.choice(self.preset_locations.offshore_locations) - self.preset_locations.offshore_locations.remove(location) - logging.info("Picked a preset offshore location") - return location - else: - if len(self.preset_locations.ashore_locations) > 0: - location = random.choice(self.preset_locations.ashore_locations) - self.preset_locations.ashore_locations.remove(location) - logging.info("Picked a preset ashore location") - return location - logging.info("No preset location found") - return None - - -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 - - @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() - for i in range(random.randint(3, 6)): - self.generate_base_defense(i) - - def generate_ewr(self) -> None: - position = self._find_location() - if position is None: - logging.error("Could not find position for " - f"{self.control_point} EWR") - 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_name) - if group is None: - return - - g.groups = [group] - self.control_point.base_defenses.append(g) - - def generate_base_defense(self, index: int) -> None: - position = self._find_location() - if position is None: - logging.error("Could not find position for " - f"{self.control_point} base defense") - return - - group_id = self.game.next_group_id() - - g = SamGroundObject(namegen.random_objective_name(), group_id, - position, self.control_point, for_airbase=True) - - generate_airbase_defense_group(index, g, self.faction_name, - self.game) - self.control_point.base_defenses.append(g) - - def _find_location(self) -> Optional[Point]: - position = find_location(True, self.control_point.position, - self.game.theater, 400, 3200, [], True) - - # Retry once, searching a bit further (On some big airbase, 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. - # (I know it's not good for performance, but this is only done on campaign generation) - # TODO : Make the whole process less stupid with preset possible positions for each airbase - if position is None: - position = find_location(True, self.control_point.position, - self.game.theater, 3200, 4800, [], True) - return position - - -class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): - def __init__(self, game: Game, control_point: ControlPoint, - templates: GroundObjectTemplates) -> None: - super().__init__(game, 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.""" - 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: - self.generate_aa_site() - else: - self.generate_ground_point() - - 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())) - - offshore = category == "oil" - - # Pick from preset locations - location = self.pick_preset_location(offshore) - - # Else try the old algorithm - if location is None: - point = find_location(not offshore, - self.control_point.position, - self.game.theater, 10000, 40000, - self.control_point.ground_objects) - else: - point = location.position - - if point is None: - logging.error( - f"Could not find point for {obj_name} at {self.control_point}") - 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: - obj_name = namegen.random_objective_name() - - # Pick from preset locations - location = self.pick_preset_location(False) - - # If no preset location, then try the old algorithm - if location is None: - position = find_location(True, self.control_point.position, - self.game.theater, 10000, 40000, - self.control_point.ground_objects) - else: - position = location.position - - if position is None: - logging.error( - f"Could not find point for {obj_name} at {self.control_point}") - return - - 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_name) - if group is not None: - 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: - - # Pick from preset locations - location = self.pick_preset_location(False) - - # If no preset location, then try the old algorithm - if location is None: - position = find_location(True, self.control_point.position, - self.game.theater, 2500, 40000, - [], False) - else: - position = location.position - - - if position is None: - logging.info( - f"Could not find point for {self.control_point} missile site") - 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 GroundObjectGenerator: - def __init__(self, game: Game) -> None: - self.game = game - 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, control_point) - elif control_point.cptype == ControlPointType.LHA_GROUP: - generator = LhaGroundObjectGenerator(self.game, control_point) - else: - generator = AirbaseGroundObjectGenerator(self.game, control_point, - self.templates) - return generator.generate() - - -def generate_airbase_defense_group(airbase_defense_group_id: int, - ground_obj: SamGroundObject, faction: str, - game: Game) -> None: - if airbase_defense_group_id == 0: - group = generate_armor_group(faction, game, ground_obj) - elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0: - group = generate_anti_air_group(game, ground_obj, faction) - elif random.randint(0, 2) == 1: - group = generate_shorad_group(game, ground_obj, faction) - else: - group = generate_armor_group(faction, game, ground_obj) - - ground_obj.groups = [] - if group is not None: - ground_obj.groups.append(group) - - -# TODO: https://stackoverflow.com/a/19482012/632035 -# A lot of the time spent on mission generation is spent in this function since -# just randomly guess up to 1800 times and often fail. This is particularly -# problematic while trying to find placement for navies in Nevada. -def find_location(on_ground: bool, near: Point, theater: ConflictTheater, - min_range: int, max_range: int, - others: List[TheaterGroundObject], - is_base_defense: bool = False) -> Optional[Point]: - """ - Find a valid ground object location - :param on_ground: Whether it should be on ground or on sea (True = on - ground) - :param near: Point - :param theater: Theater object - :param min_range: Minimal range from point - :param max_range: Max range from point - :param others: Other already existing ground objects - :param is_base_defense: True if the location is for base defense. - :return: - """ - point = None - for _ in range(300): - - # Check if on land or sea - p = near.random_point_within(max_range, min_range) - if on_ground and theater.is_on_land(p): - point = p - elif not on_ground and theater.is_in_sea(p): - point = p - - if point: - for angle in range(0, 360, 45): - p = point.point_from_heading(angle, 2500) - if on_ground and not theater.is_on_land(p): - point = None - break - elif not on_ground and not theater.is_in_sea(p): - point = None - break - if point: - for other in others: - if other.position.distance_to_point(point) < 10000: - point = None - break - - if point: - for control_point in theater.controlpoints: - if is_base_defense: - break - if control_point.position != near: - if point is None: - break - if control_point.position.distance_to_point(point) < 30000: - point = None - break - for ground_obj in control_point.ground_objects: - if ground_obj.position.distance_to_point(point) < 10000: - point = None - break - - if point: - return point - return None diff --git a/theater/weatherforecast.py b/theater/weatherforecast.py deleted file mode 100644 index 65ed7351..00000000 --- a/theater/weatherforecast.py +++ /dev/null @@ -1,4 +0,0 @@ - - -class WeatherForecast: - pass