diff --git a/changelog.md b/changelog.md index 0f60b72f..c0ecb509 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,7 @@ -# 2.1.3 +# 2.1.4 + +## Fixes : +* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames. ## Features/Improvements : * **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions diff --git a/game/data/cap_capabilities_db.py b/game/data/cap_capabilities_db.py index eb367238..438a0f80 100644 --- a/game/data/cap_capabilities_db.py +++ b/game/data/cap_capabilities_db.py @@ -1,4 +1,22 @@ -from dcs.planes import * +from dcs.planes import ( + Bf_109K_4, + C_101CC, + FW_190A8, + FW_190D9, + F_5E_3, + F_86F_Sabre, + I_16, + L_39ZA, + MiG_15bis, + MiG_19P, + MiG_21Bis, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) + from pydcs_extensions.a4ec.a4ec import A_4E_C """ diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 866ae897..d81c5484 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -1,95 +1,101 @@ +from dataclasses import dataclass + from game.utils import nm_to_meter, feet_to_meter -MODERN_DOCTRINE = { - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": True, - "STRIKE": True, - "ANTISHIP": True, - }, +@dataclass(frozen=True) +class Doctrine: + cas: bool + cap: bool + sead: bool + strike: bool + antiship: bool - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, + strike_max_range: int + sead_max_range: int - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, + rendezvous_altitude: int + join_distance: int + split_distance: int + ingress_egress_distance: int + ingress_altitude: int + egress_altitude: int - "INGRESS_EGRESS_DISTANCE": nm_to_meter(45), - "INGRESS_ALT": feet_to_meter(20000), - "EGRESS_ALT": feet_to_meter(20000), - "PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), + min_patrol_altitude: int + max_patrol_altitude: int + pattern_altitude: int - "CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)), + cap_min_track_length: int + cap_max_track_length: int + cap_min_distance_from_cp: int + cap_max_distance_from_cp: int - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, -} -COLDWAR_DOCTRINE = { +MODERN_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=True, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + rendezvous_altitude=feet_to_meter(25000), + join_distance=nm_to_meter(20), + split_distance=nm_to_meter(20), + ingress_egress_distance=nm_to_meter(45), + ingress_altitude=feet_to_meter(20000), + egress_altitude=feet_to_meter(20000), + min_patrol_altitude=feet_to_meter(15000), + max_patrol_altitude=feet_to_meter(33000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(15), + cap_max_track_length=nm_to_meter(40), + cap_min_distance_from_cp=nm_to_meter(10), + cap_max_distance_from_cp=nm_to_meter(40), +) - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": True, - "STRIKE": True, - "ANTISHIP": True, - }, +COLDWAR_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=True, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + rendezvous_altitude=feet_to_meter(22000), + join_distance=nm_to_meter(10), + split_distance=nm_to_meter(10), + ingress_egress_distance=nm_to_meter(30), + ingress_altitude=feet_to_meter(18000), + egress_altitude=feet_to_meter(18000), + min_patrol_altitude=feet_to_meter(10000), + max_patrol_altitude=feet_to_meter(24000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(12), + cap_max_track_length=nm_to_meter(24), + cap_min_distance_from_cp=nm_to_meter(8), + cap_max_distance_from_cp=nm_to_meter(25), +) - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, - - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, - - "INGRESS_EGRESS_DISTANCE": nm_to_meter(30), - "INGRESS_ALT": feet_to_meter(18000), - "EGRESS_ALT": feet_to_meter(18000), - "PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), - - "CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)), - - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, -} - -WWII_DOCTRINE = { - - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": False, - "STRIKE": True, - "ANTISHIP": True, - }, - - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, - - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, - - "INGRESS_EGRESS_DISTANCE": nm_to_meter(7), - "INGRESS_ALT": feet_to_meter(8000), - "EGRESS_ALT": feet_to_meter(8000), - "PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), - - "CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)), - - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, - -} +WWII_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=False, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + join_distance=nm_to_meter(5), + split_distance=nm_to_meter(5), + rendezvous_altitude=feet_to_meter(10000), + ingress_egress_distance=nm_to_meter(7), + ingress_altitude=feet_to_meter(8000), + egress_altitude=feet_to_meter(8000), + min_patrol_altitude=feet_to_meter(4000), + max_patrol_altitude=feet_to_meter(15000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(8), + cap_max_track_length=nm_to_meter(18), + cap_min_distance_from_cp=nm_to_meter(0), + cap_max_distance_from_cp=nm_to_meter(5), +) diff --git a/game/data/radar_db.py b/game/data/radar_db.py index c3c9e25a..4e90d56c 100644 --- a/game/data/radar_db.py +++ b/game/data/radar_db.py @@ -1,5 +1,26 @@ +from dcs.ships import ( + CGN_1144_2_Pyotr_Velikiy, + CG_1164_Moskva, + CVN_70_Carl_Vinson, + CVN_71_Theodore_Roosevelt, + CVN_72_Abraham_Lincoln, + CVN_73_George_Washington, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + CV_1143_5_Admiral_Kuznetsov_2017, + FFG_11540_Neustrashimy, + FFL_1124_4_Grisha, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + Type_052B_Destroyer, + Type_052C_Destroyer, + Type_054A_Frigate, + USS_Arleigh_Burke_IIa, +) from dcs.vehicles import AirDefence -from dcs.ships import * UNITS_WITH_RADAR = [ diff --git a/game/db.py b/game/db.py index f2b94b54..97c3c4fc 100644 --- a/game/db.py +++ b/game/db.py @@ -1,34 +1,177 @@ -import typing -import enum from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple, Type, Union -from dcs.countries import get_by_id, country_dict -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * - -from dcs.task import * -from dcs.unit import * -from dcs.unittype import * -from dcs.unitgroup import * +from dcs.countries import country_dict +from dcs.helicopters import ( + AH_1W, + AH_64A, + AH_64D, + HelicopterType, + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, + OH_58D, + SA342L, + SA342M, + SA342Minigun, + SA342Mistral, + UH_1H, + UH_60A, + helicopter_map, +) +from dcs.mapping import Point +# mypy can't resolve these if they're wildcard imports for some reason. +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + A_20G, + A_50, + An_26B, + An_30M, + B_17G, + B_1B, + B_52H, + Bf_109K_4, + C_101CC, + C_130, + E_3A, + FA_18C_hornet, + FW_190A8, + FW_190D9, + F_14B, + F_15C, + F_15E, + F_16A, + F_16C_50, + F_4E, + F_5E_3, + F_86F_Sabre, + F_A_18C, + IL_76MD, + IL_78M, + JF_17, + J_11A, + Ju_88A4, + KC130, + KC_135, + KJ_2000, + L_39C, + L_39ZA, + MQ_9_Reaper, + M_2000C, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_27K, + MiG_29A, + MiG_29G, + MiG_29S, + MiG_31, + Mirage_2000_5, + P_47D_30, + P_47D_30bl1, + P_47D_40, + P_51D, + P_51D_30_NA, + PlaneType, + RQ_1A_Predator, + S_3B_Tanker, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_27, + Su_30, + Su_33, + Su_34, + Tornado_GR4, + Tornado_IDS, + WingLoong_I, + Yak_40, + plane_map, +) +from dcs.ships import ( + Armed_speedboat, + Bulk_cargo_ship_Yakushev, + CVN_71_Theodore_Roosevelt, + CVN_72_Abraham_Lincoln, + CVN_73_George_Washington, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + CV_1143_5_Admiral_Kuznetsov_2017, + Dry_cargo_ship_Ivanov, + LHA_1_Tarawa, + Tanker_Elnya_160, + ship_map, +) +from dcs.task import ( + AWACS, + AntishipStrike, + CAP, + CAS, + CargoTransportation, + Embarking, + Escort, + GroundAttack, + Intercept, + MainTask, + Nothing, + PinpointStrike, + Reconnaissance, + Refueling, + SEAD, + Task, + Transport, +) +from dcs.terrain.terrain import Airport +from dcs.unit import Ship, Unit, Vehicle +from dcs.unitgroup import ShipGroup, StaticGroup +from dcs.unittype import FlyingType, ShipType, UnitType, VehicleType +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Carriage, + Infantry, + Unarmed, + vehicle_map, +) +import pydcs_extensions.frenchpack.frenchpack as frenchpack from game.factions.australia_2005 import Australia_2005 from game.factions.bluefor_coldwar import BLUEFOR_COLDWAR from game.factions.bluefor_coldwar_a4 import BLUEFOR_COLDWAR_A4 from game.factions.bluefor_coldwar_mods import BLUEFOR_COLDWAR_MODS +from game.factions.bluefor_modern import BLUEFOR_MODERN from game.factions.canada_2005 import Canada_2005 from game.factions.china_2010 import China_2010 from game.factions.france_1995 import France_1995 from game.factions.france_2005 import France_2005 from game.factions.france_modded import France_2005_Modded +from game.factions.germany_1944 import Germany_1944 from game.factions.germany_1944_easy import Germany_1944_Easy from game.factions.germany_1990 import Germany_1990 +from game.factions.india_2010 import India_2010 from game.factions.insurgent import Insurgent from game.factions.insurgent_modded import Insurgent_modded from game.factions.iran_2015 import Iran_2015 from game.factions.israel_1948 import Israel_1948 -from game.factions.israel_1973 import Israel_1973, Israel_1973_NO_WW2_UNITS, Israel_1982 +from game.factions.israel_1973 import ( + Israel_1973, + Israel_1973_NO_WW2_UNITS, + Israel_1982, +) from game.factions.israel_2000 import Israel_2000 from game.factions.italy_1990 import Italy_1990 from game.factions.italy_1990_mb339 import Italy_1990_MB339 @@ -37,35 +180,41 @@ from game.factions.libya_2011 import Libya_2011 from game.factions.netherlands_1990 import Netherlands_1990 from game.factions.north_korea_2000 import NorthKorea_2000 from game.factions.pakistan_2015 import Pakistan_2015 -from game.factions.private_miltary_companies import PMC_WESTERN_B, PMC_RUSSIAN, PMC_WESTERN_A -from game.factions.russia_1975 import Russia_1975 -from game.factions.germany_1944 import Germany_1944 -from game.factions.india_2010 import India_2010 +from game.factions.private_miltary_companies import ( + PMC_RUSSIAN, + PMC_WESTERN_A, + PMC_WESTERN_B, +) from game.factions.russia_1955 import Russia_1955 from game.factions.russia_1965 import Russia_1965 +from game.factions.russia_1975 import Russia_1975 from game.factions.russia_1990 import Russia_1990 from game.factions.russia_2010 import Russia_2010 from game.factions.spain_1990 import Spain_1990 from game.factions.sweden_1990 import Sweden_1990 -from game.factions.syria import Syria_2011, Syria_1967, Syria_1967_WW2_Weapons, Syria_1973, Arab_Armies_1948, Syria_1982 +from game.factions.syria import ( + Arab_Armies_1948, + Syria_1967, + Syria_1967_WW2_Weapons, + Syria_1973, + Syria_1982, + Syria_2011, +) from game.factions.turkey_2005 import Turkey_2005 from game.factions.uae_2005 import UAE_2005 from game.factions.uk_1944 import UK_1944 from game.factions.uk_1990 import UnitedKingdom_1990 from game.factions.ukraine_2010 import Ukraine_2010 from game.factions.us_aggressors import US_Aggressors -from game.factions.usa_1944 import USA_1944, ALLIES_1944 +from game.factions.usa_1944 import ALLIES_1944, USA_1944 from game.factions.usa_1955 import USA_1955 from game.factions.usa_1960 import USA_1960 from game.factions.usa_1965 import USA_1965 from game.factions.usa_1990 import USA_1990 from game.factions.usa_2005 import USA_2005 -from game.factions.bluefor_modern import BLUEFOR_MODERN - # PATCH pydcs data with MODS from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN -import pydcs_extensions.frenchpack.frenchpack as frenchpack from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M plane_map["A-4E-C"] = A_4E_C @@ -793,13 +942,13 @@ SAM_CONVERT = { """ Units that will always be spawned in the air """ -TAKEOFF_BAN = [ +TAKEOFF_BAN: List[Type[FlyingType]] = [ ] """ Units that will be always spawned in the air if launched from the carrier """ -CARRIER_TAKEOFF_BAN = [ +CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [ Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned ] @@ -807,7 +956,7 @@ CARRIER_TAKEOFF_BAN = [ Units separated by country. country : DCS Country name """ -FACTIONS = { +FACTIONS: Dict[str, Dict[str, Any]] = { "Bluefor Modern": BLUEFOR_MODERN, "Bluefor Cold War 1970s": BLUEFOR_COLDWAR, @@ -934,10 +1083,11 @@ COMMON_OVERRIDE = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP", } -PLANE_PAYLOAD_OVERRIDES = { +PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { FA_18C_hornet: { CAP: "CAP HEAVY", @@ -946,7 +1096,8 @@ PLANE_PAYLOAD_OVERRIDES = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP HEAVY", }, F_A_18C: { CAP: "CAP HEAVY", @@ -955,7 +1106,8 @@ PLANE_PAYLOAD_OVERRIDES = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP HEAVY", }, A_10A: COMMON_OVERRIDE, A_10C: COMMON_OVERRIDE, @@ -1134,17 +1286,17 @@ LHA_CAPABLE = [ ---------- END OF CONFIGURATION SECTION """ -UnitsDict = typing.Dict[UnitType, int] -PlaneDict = typing.Dict[FlyingType, int] -HeliDict = typing.Dict[HelicopterType, int] -ArmorDict = typing.Dict[VehicleType, int] -ShipDict = typing.Dict[ShipType, int] -AirDefenseDict = typing.Dict[AirDefence, int] +UnitsDict = Dict[UnitType, int] +PlaneDict = Dict[FlyingType, int] +HeliDict = Dict[HelicopterType, int] +ArmorDict = Dict[VehicleType, int] +ShipDict = Dict[ShipType, int] +AirDefenseDict = Dict[AirDefence, int] -AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]] -TaskForceDict = typing.Dict[typing.Type[Task], AssignedUnitsDict] +AssignedUnitsDict = Dict[Type[UnitType], Tuple[int, int]] +TaskForceDict = Dict[Type[MainTask], AssignedUnitsDict] -StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]] +StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point] def upgrade_to_supercarrier(unit, name: str): @@ -1162,7 +1314,7 @@ def upgrade_to_supercarrier(unit, name: str): else: return unit -def unit_task(unit: UnitType) -> Task: +def unit_task(unit: UnitType) -> Optional[Task]: for task, units in UNIT_BY_TASK.items(): if unit in units: return task @@ -1173,10 +1325,10 @@ def unit_task(unit: UnitType) -> Task: print(unit.name + " cause issue") return None -def find_unittype(for_task: Task, country_name: str) -> typing.List[UnitType]: +def find_unittype(for_task: Task, country_name: str) -> List[UnitType]: return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name]["units"]] -def find_infantry(country_name: str) -> typing.List[UnitType]: +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, @@ -1199,7 +1351,7 @@ def unit_type_name(unit_type) -> str: 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) -> UnitType: +def unit_type_from_name(name: str) -> Optional[UnitType]: if name in vehicle_map: return vehicle_map[name] elif name in plane_map: @@ -1232,7 +1384,7 @@ def task_name(task) -> str: return task.name -def choose_units(for_task: Task, factor: float, count: int, country: str) -> typing.Collection[UnitType]: +def choose_units(for_task: Task, factor: float, count: int, country: str) -> List[UnitType]: suitable_unittypes = find_unittype(for_task, country) suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()] suitable_unittypes.sort(key=lambda x: PRICES[x]) @@ -1258,7 +1410,7 @@ def unitdict_merge(a: UnitsDict, b: UnitsDict) -> UnitsDict: def unitdict_split(unit_dict: UnitsDict, count: int): - buffer_dict = {} + buffer_dict: Dict[UnitType, int] = {} for unit_type, unit_count in unit_dict.items(): for _ in range(unit_count): unitdict_append(buffer_dict, unit_type, 1) @@ -1281,7 +1433,7 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict return {} -def assigned_units_split(fd: AssignedUnitsDict) -> typing.Tuple[PlaneDict, PlaneDict]: +def assigned_units_split(fd: AssignedUnitsDict) -> Tuple[PlaneDict, PlaneDict]: return {k: v1 for k, (v1, v2) in fd.items()}, {k: v2 for k, (v1, v2) in fd.items()}, @@ -1290,7 +1442,7 @@ def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict: def assignedunits_split_to_count(dict: AssignedUnitsDict, count: int): - buffer_dict = {} + buffer_dict: Dict[Type[UnitType], Tuple[int, int]] = {} for unit_type, (unit_count, client_count) in dict.items(): for _ in range(unit_count): new_count, new_client_count = buffer_dict.get(unit_type, (0, 0)) diff --git a/userdata/debriefing.py b/game/debriefing.py similarity index 100% rename from userdata/debriefing.py rename to game/debriefing.py diff --git a/game/event/event.py b/game/event/event.py index 8146deb3..0af3852c 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -1,24 +1,24 @@ -import typing +from __future__ import annotations + import logging +import math +from typing import Dict, List, Optional, Type, TYPE_CHECKING -from dcs.action import Coalition -from dcs.unittype import UnitType -from dcs.task import * -from dcs.vehicles import AirDefence +from dcs.mapping import Point +from dcs.task import Task from dcs.unittype import UnitType -from game import * +from game import db, persistency +from game.debriefing import Debriefing from game.infos.information import Information -from theater import * +from game.operation.operation import Operation from gen.environmentgen import EnvironmentSettings -from gen.conflictgen import Conflict -from game.db import assigned_units_from, unitdict_from +from gen.ground_forces.combat_stance import CombatStance +from theater import ControlPoint from theater.start_generator import generate_airbase_defense_group -from userdata.debriefing import Debriefing -from userdata import persistency - -import game.db as db +if TYPE_CHECKING: + from ..game import Game DIFFICULTY_LOG_BASE = 1.1 EVENT_DEPARTURE_MAX_DISTANCE = 340000 @@ -28,6 +28,7 @@ MINOR_DEFEAT_INFLUENCE = 0.1 DEFEAT_INFLUENCE = 0.3 STRONG_DEFEAT_INFLUENCE = 0.5 + class Event: silent = False informational = False @@ -37,7 +38,6 @@ class Event: game = None # type: Game location = None # type: Point from_cp = None # type: ControlPoint - departure_cp = None # type: ControlPoint to_cp = None # type: ControlPoint operation = None # type: Operation @@ -47,7 +47,7 @@ class Event: def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): self.game = game - self.departure_cp = None + self.departure_cp: Optional[ControlPoint] = None self.from_cp = from_cp self.to_cp = target_cp self.location = location @@ -59,14 +59,14 @@ class Event: return self.attacker_name == self.game.player_name @property - def enemy_cp(self) -> ControlPoint: + 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) -> typing.Collection[typing.Type[Task]]: + def tasks(self) -> List[Type[Task]]: return [] @property @@ -91,18 +91,6 @@ class Event: def is_successfull(self, debriefing: Debriefing) -> bool: return self.operation.is_successfull(debriefing) - def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict): - if self.is_player_attacking: - self.departure_cp = cp - else: - self.to_cp = cp - - def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict): - if self.is_player_attacking: - self.departure_cp = cp - else: - self.to_cp = cp - def generate(self): self.operation.is_awacs_enabled = self.is_awacs_enabled self.operation.ca_slots = self.ca_slots @@ -253,7 +241,7 @@ class Event: for enemy_cp in enemy_cps: print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name) - delta = 0 + 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] @@ -376,7 +364,6 @@ class Event: class UnitsDeliveryEvent(Event): informational = True - units = None # type: typing.Dict[UnitType, int] def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): super(UnitsDeliveryEvent, self).__init__(game=game, @@ -386,12 +373,12 @@ class UnitsDeliveryEvent(Event): attacker_name=attacker_name, defender_name=defender_name) - self.units = {} + self.units: Dict[UnitType, int] = {} def __str__(self): return "Pending delivery to {}".format(self.to_cp) - def deliver(self, units: typing.Dict[UnitType, int]): + def deliver(self, units: Dict[UnitType, int]): for k, v in units.items(): self.units[k] = self.units.get(k, 0) + v diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index e548440f..0046526d 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -1,12 +1,17 @@ -from game.event import * +from typing import List, Type + +from dcs.task import CAP, CAS, Task + +from game import db from game.operation.frontlineattack import FrontlineAttackOperation -from userdata.debriefing import Debriefing +from .event import Event +from ..debriefing import Debriefing class FrontlineAttackEvent(Event): @property - def tasks(self) -> typing.Collection[typing.Type[Task]]: + def tasks(self) -> List[Type[Task]]: if self.is_player_attacking: return [CAS, CAP] else: @@ -34,6 +39,7 @@ class FrontlineAttackEvent(Event): 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, @@ -41,13 +47,3 @@ class FrontlineAttackEvent(Event): departure_cp=self.departure_cp, to_cp=self.to_cp) self.operation = op - - def player_defending(self, flights: db.TaskForceDict): - 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/australia_2005.py b/game/factions/australia_2005.py index df4972e6..c9cfb320 100644 --- a/game/factions/australia_2005.py +++ b/game/factions/australia_2005.py @@ -1,7 +1,27 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Australia_2005 = { "country": "Australia", diff --git a/game/factions/bluefor_coldwar.py b/game/factions/bluefor_coldwar.py index 5db15d73..c241bbae 100644 --- a/game/factions/bluefor_coldwar.py +++ b/game/factions/bluefor_coldwar.py @@ -1,7 +1,30 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) BLUEFOR_COLDWAR = { "country": "Combined Joint Task Forces Blue", diff --git a/game/factions/bluefor_coldwar_a4.py b/game/factions/bluefor_coldwar_a4.py index 74983134..ce6cf016 100644 --- a/game/factions/bluefor_coldwar_a4.py +++ b/game/factions/bluefor_coldwar_a4.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C diff --git a/game/factions/bluefor_coldwar_mods.py b/game/factions/bluefor_coldwar_mods.py index aece4e46..a395fc48 100644 --- a/game/factions/bluefor_coldwar_mods.py +++ b/game/factions/bluefor_coldwar_mods.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/bluefor_modern.py b/game/factions/bluefor_modern.py index 8db0ad89..9f97827b 100644 --- a/game/factions/bluefor_modern.py +++ b/game/factions/bluefor_modern.py @@ -1,7 +1,45 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + Ka_50, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_16C_50, + F_5E_3, + JF_17, + KC130, + KC_135, + M_2000C, + Su_25T, + Su_27, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) BLUEFOR_MODERN = { "country": "Combined Joint Task Forces Blue", diff --git a/game/factions/canada_2005.py b/game/factions/canada_2005.py index ea4497ca..7a664709 100644 --- a/game/factions/canada_2005.py +++ b/game/factions/canada_2005.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Canada_2005 = { "country": "Canada", diff --git a/game/factions/china_2010.py b/game/factions/china_2010.py index 0d98d1b9..577fb33d 100644 --- a/game/factions/china_2010.py +++ b/game/factions/china_2010.py @@ -1,7 +1,38 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_28N, + Mi_8MT, +) +from dcs.planes import ( + An_26B, + An_30M, + IL_76MD, + IL_78M, + JF_17, + J_11A, + KJ_2000, + MiG_21Bis, + Su_30, + Su_33, + WingLoong_I, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, + Type_052B_Destroyer, + Type_052C_Destroyer, + Type_054A_Frigate, + Type_071_Amphibious_Transport_Dock, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) China_2010 = { "country": "China", diff --git a/game/factions/france_1995.py b/game/factions/france_1995.py index acf56495..a14f24e5 100644 --- a/game/factions/france_1995.py +++ b/game/factions/france_1995.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) France_1995 = { "country": "France", diff --git a/game/factions/france_2005.py b/game/factions/france_2005.py index b2f4b87a..28c00ae4 100644 --- a/game/factions/france_2005.py +++ b/game/factions/france_2005.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) France_2005 = { "country": "France", diff --git a/game/factions/france_modded.py b/game/factions/france_modded.py index ad0f7de5..8283d090 100644 --- a/game/factions/france_modded.py +++ b/game/factions/france_modded.py @@ -1,10 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) import pydcs_extensions.frenchpack.frenchpack as frenchpack -from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S +from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M France_2005_Modded = { "country": "France", diff --git a/game/factions/germany_1944.py b/game/factions/germany_1944.py index 9123b350..c63f88f4 100644 --- a/game/factions/germany_1944.py +++ b/game/factions/germany_1944.py @@ -1,5 +1,16 @@ -from dcs.planes import * -from dcs.vehicles import * +from dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_GERMANY_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/germany_1944_easy.py b/game/factions/germany_1944_easy.py index b79d45f0..8be93457 100644 --- a/game/factions/germany_1944_easy.py +++ b/game/factions/germany_1944_easy.py @@ -1,5 +1,16 @@ -from dcs.planes import * -from dcs.vehicles import * +from dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_GERMANY_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/germany_1990.py b/game/factions/germany_1990.py index ae1f0668..54432ef4 100644 --- a/game/factions/germany_1990.py +++ b/game/factions/germany_1990.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_4E, + KC130, + KC_135, + MiG_29G, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Germany_1990 = { "country": "Germany", diff --git a/game/factions/india_2010.py b/game/factions/india_2010.py index 2dc756ec..756faa23 100644 --- a/game/factions/india_2010.py +++ b/game/factions/india_2010.py @@ -1,7 +1,32 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + Mi_8MT, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + MiG_21Bis, + MiG_27K, + MiG_29S, + Mirage_2000_5, + Su_30, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + FSG_1241_1MP_Molniya, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) India_2010 = { "country": "India", diff --git a/game/factions/insurgent.py b/game/factions/insurgent.py index d94603c6..66ae3459 100644 --- a/game/factions/insurgent.py +++ b/game/factions/insurgent.py @@ -1,7 +1,14 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Insurgent = { "country": "Insurgents", diff --git a/game/factions/insurgent_modded.py b/game/factions/insurgent_modded.py index 39e1e174..b19b4344 100644 --- a/game/factions/insurgent_modded.py +++ b/game/factions/insurgent_modded.py @@ -1,8 +1,21 @@ -from dcs.ships import * -from dcs.vehicles import * +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) -from pydcs_extensions.frenchpack.frenchpack import DIM__TOYOTA_BLUE, DIM__TOYOTA_DESERT, DIM__TOYOTA_GREEN, \ - DIM__KAMIKAZE +from pydcs_extensions.frenchpack.frenchpack import ( + DIM__KAMIKAZE, + DIM__TOYOTA_BLUE, + DIM__TOYOTA_DESERT, + DIM__TOYOTA_GREEN, +) Insurgent_modded = { "country": "Insurgents", diff --git a/game/factions/iran_2015.py b/game/factions/iran_2015.py index 56751a2c..53f20dd6 100644 --- a/game/factions/iran_2015.py +++ b/game/factions/iran_2015.py @@ -1,7 +1,35 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_28N, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + F_14B, + F_4E, + F_5E_3, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_29A, + Su_17M4, + Su_24M, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Iran_2015 = { "country": "Iran", diff --git a/game/factions/israel_1948.py b/game/factions/israel_1948.py index ffa57fcb..bc3b615c 100644 --- a/game/factions/israel_1948.py +++ b/game/factions/israel_1948.py @@ -1,6 +1,20 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + B_17G, + Bf_109K_4, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Israel_1948 = { "country": "Israel", diff --git a/game/factions/israel_1973.py b/game/factions/israel_1973.py index 00624ec9..3bfa5d15 100644 --- a/game/factions/israel_1973.py +++ b/game/factions/israel_1973.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_16A, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C diff --git a/game/factions/israel_2000.py b/game/factions/israel_2000.py index 6c0db9c0..d87460f9 100644 --- a/game/factions/israel_2000.py +++ b/game/factions/israel_2000.py @@ -1,7 +1,29 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_15E, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Israel_2000 = { "country": "Israel", diff --git a/game/factions/italy_1990.py b/game/factions/italy_1990.py index 2c2175a8..267cd611 100644 --- a/game/factions/italy_1990.py +++ b/game/factions/italy_1990.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + KC_135, + S_3B_Tanker, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Italy_1990 = { "country": "Italy", diff --git a/game/factions/italy_1990_mb339.py b/game/factions/italy_1990_mb339.py index 92307749..9d594817 100644 --- a/game/factions/italy_1990_mb339.py +++ b/game/factions/italy_1990_mb339.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + KC_135, + S_3B_Tanker, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/japan_2005.py b/game/factions/japan_2005.py index c9657348..d33800ff 100644 --- a/game/factions/japan_2005.py +++ b/game/factions/japan_2005.py @@ -1,7 +1,24 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import LHA_1_Tarawa, Ticonderoga_class, USS_Arleigh_Burke_IIa +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Japan_2005 = { "country": "Japan", diff --git a/game/factions/libya_2011.py b/game/factions/libya_2011.py index 688b4877..4de5b42c 100644 --- a/game/factions/libya_2011.py +++ b/game/factions/libya_2011.py @@ -1,6 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_23MLD, + Su_17M4, + Su_24M, + Yak_40, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Libya_2011 = { "country": "Libya", diff --git a/game/factions/netherlands_1990.py b/game/factions/netherlands_1990.py index b32fe7d0..48c916bd 100644 --- a/game/factions/netherlands_1990.py +++ b/game/factions/netherlands_1990.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Netherlands_1990 = { "country": "The Netherlands", diff --git a/game/factions/north_korea_2000.py b/game/factions/north_korea_2000.py index dd588f92..15d1f587 100644 --- a/game/factions/north_korea_2000.py +++ b/game/factions/north_korea_2000.py @@ -1,7 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_29A, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) NorthKorea_2000 = { "country": "North Korea", diff --git a/game/factions/pakistan_2015.py b/game/factions/pakistan_2015.py index 67bfa2aa..8d7c6190 100644 --- a/game/factions/pakistan_2015.py +++ b/game/factions/pakistan_2015.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + E_3A, + F_16C_50, + IL_78M, + JF_17, + MiG_19P, + MiG_21Bis, + WingLoong_I, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Pakistan_2015 = { "country": "Pakistan", diff --git a/game/factions/private_miltary_companies.py b/game/factions/private_miltary_companies.py index 4f2860ca..23fbcbdd 100644 --- a/game/factions/private_miltary_companies.py +++ b/game/factions/private_miltary_companies.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_8MT, + OH_58D, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_101CC, + L_39C, + L_39ZA, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/russia_1955.py b/game/factions/russia_1955.py index dcaeec68..5730bd9d 100644 --- a/game/factions/russia_1955.py +++ b/game/factions/russia_1955.py @@ -1,6 +1,11 @@ -from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40 -from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160 -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.planes import An_26B, An_30M, IL_76MD, IL_78M, MiG_15bis, Yak_40 +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1955 = { "country": "Russia", diff --git a/game/factions/russia_1965.py b/game/factions/russia_1965.py index bc5762c6..9d88d251 100644 --- a/game/factions/russia_1965.py +++ b/game/factions/russia_1965.py @@ -1,7 +1,22 @@ from dcs.helicopters import Mi_8MT -from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50 -from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160 -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_15bis, + MiG_19P, + MiG_21Bis, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1965 = { "country": "Russia", diff --git a/game/factions/russia_1975.py b/game/factions/russia_1975.py index db6a50ae..b8a75437 100644 --- a/game/factions/russia_1975.py +++ b/game/factions/russia_1975.py @@ -1,8 +1,31 @@ -from dcs.helicopters import Mi_8MT, Mi_24V -from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \ - Yak_40, A_50 -from dcs.ships import * -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29A, + Su_17M4, + Su_24M, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CGN_1144_2_Pyotr_Velikiy, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1975 = { "country": "Russia", diff --git a/game/factions/russia_1990.py b/game/factions/russia_1990.py index ee2758a5..747024a8 100644 --- a/game/factions/russia_1990.py +++ b/game/factions/russia_1990.py @@ -1,7 +1,39 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_23MLD, + MiG_25PD, + MiG_29A, + MiG_29S, + MiG_31, + Su_24M, + Su_25, + Su_27, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Russia_1990 = { "country": "Russia", diff --git a/game/factions/russia_2010.py b/game/factions/russia_2010.py index cc45f062..13adefb6 100644 --- a/game/factions/russia_2010.py +++ b/game/factions/russia_2010.py @@ -1,7 +1,42 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_29S, + MiG_31, + Su_24M, + Su_25, + Su_25T, + Su_27, + Su_30, + Su_33, + Su_34, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Russia_2010 = { "country": "Russia", diff --git a/game/factions/spain_1990.py b/game/factions/spain_1990.py index f016cb8a..246484a4 100644 --- a/game/factions/spain_1990.py +++ b/game/factions/spain_1990.py @@ -1,6 +1,26 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + AV8BNA, + C_101CC, + C_130, + E_3A, + FA_18C_hornet, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Spain_1990 = { "country": "Spain", diff --git a/game/factions/sweden_1990.py b/game/factions/sweden_1990.py index ebc754f9..058f1478 100644 --- a/game/factions/sweden_1990.py +++ b/game/factions/sweden_1990.py @@ -1,7 +1,21 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + AJS37, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Sweden_1990 = { "country": "Sweden", diff --git a/game/factions/syria.py b/game/factions/syria.py index de082320..f7017e86 100644 --- a/game/factions/syria.py +++ b/game/factions/syria.py @@ -1,6 +1,35 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, + SA342L, + SA342M, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29S, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Yak_40, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Syria_2011 = { "country": "Syria", diff --git a/game/factions/turkey_2005.py b/game/factions/turkey_2005.py index 02b0500d..be68334c 100644 --- a/game/factions/turkey_2005.py +++ b/game/factions/turkey_2005.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Turkey_2005 = { "country": "Turkey", diff --git a/game/factions/uae_2005.py b/game/factions/uae_2005.py index cc412f06..d6240332 100644 --- a/game/factions/uae_2005.py +++ b/game/factions/uae_2005.py @@ -1,7 +1,27 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, + WingLoong_I, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) UAE_2005 = { "country": "United Arab Emirates", diff --git a/game/factions/uk_1944.py b/game/factions/uk_1944.py index 7798d714..2620fc86 100644 --- a/game/factions/uk_1944.py +++ b/game/factions/uk_1944.py @@ -1,6 +1,19 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + A_20G, + B_17G, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from game.data.building_data import WW2_ALLIES_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/uk_1990.py b/game/factions/uk_1990.py index f2bcc57a..4855059d 100644 --- a/game/factions/uk_1990.py +++ b/game/factions/uk_1990.py @@ -1,7 +1,29 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + SA342M, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + F_4E, + KC130, + KC_135, + Tornado_GR4, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) UnitedKingdom_1990 = { "country": "UK", diff --git a/game/factions/ukraine_2010.py b/game/factions/ukraine_2010.py index cd5149b6..de030137 100644 --- a/game/factions/ukraine_2010.py +++ b/game/factions/ukraine_2010.py @@ -1,7 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_29S, + Su_24M, + Su_25, + Su_25T, + Su_27, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Ukraine_2010 = { "country": "Ukraine", diff --git a/game/factions/us_aggressors.py b/game/factions/us_aggressors.py index ab4e6ffd..650c09cd 100644 --- a/game/factions/us_aggressors.py +++ b/game/factions/us_aggressors.py @@ -1,7 +1,36 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + Ka_50, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + F_15C, + F_16C_50, + F_5E_3, + KC130, + KC_135, + Su_27, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) US_Aggressors = { "country": "USAF Aggressors", diff --git a/game/factions/usa_1944.py b/game/factions/usa_1944.py index 29e07372..7b99bd42 100644 --- a/game/factions/usa_1944.py +++ b/game/factions/usa_1944.py @@ -1,6 +1,20 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + A_20G, + B_17G, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_ALLIES_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/usa_1955.py b/game/factions/usa_1955.py index efa0af47..1943544b 100644 --- a/game/factions/usa_1955.py +++ b/game/factions/usa_1955.py @@ -1,7 +1,22 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.planes import ( + C_130, + E_3A, + F_86F_Sabre, + KC130, + KC_135, + P_51D, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1955 = { "country": "USA", diff --git a/game/factions/usa_1960.py b/game/factions/usa_1960.py index e2b64bd1..ee162d04 100644 --- a/game/factions/usa_1960.py +++ b/game/factions/usa_1960.py @@ -1,7 +1,25 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_86F_Sabre, + KC130, + KC_135, + P_51D, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1960 = { "country": "USA", diff --git a/game/factions/usa_1965.py b/game/factions/usa_1965.py index 59dc651a..cba61391 100644 --- a/game/factions/usa_1965.py +++ b/game/factions/usa_1965.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + B_52H, + C_130, + E_3A, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1965 = { "country": "USA", diff --git a/game/factions/usa_1990.py b/game/factions/usa_1990.py index df2b37df..4014237a 100644 --- a/game/factions/usa_1990.py +++ b/game/factions/usa_1990.py @@ -1,7 +1,34 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + A_10A, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_15E, + F_16C_50, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1990 = { "country": "USA", diff --git a/game/factions/usa_2005.py b/game/factions/usa_2005.py index d6b63a58..5b9e03ca 100644 --- a/game/factions/usa_2005.py +++ b/game/factions/usa_2005.py @@ -1,7 +1,36 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + A_10C, + A_10C_2, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_15E, + F_16C_50, + KC130, + KC_135, + MQ_9_Reaper, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) USA_2005 = { "country": "USA", diff --git a/game/game.py b/game/game.py index 385261b8..57fabfd6 100644 --- a/game/game.py +++ b/game/game.py @@ -1,10 +1,32 @@ +import logging +import math +import random +import sys from datetime import datetime, timedelta +from typing import Any, Dict, List -from game.db import REWARDS, PLAYER_BUDGET_BASE, sys +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.vehicles import AirDefence + +from game import db +from game.db import PLAYER_BUDGET_BASE, REWARDS +from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats -from gen.flights.ai_flight_planner import FlightPlanner +from gen.ato import AirTaskingOrder +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 .event import * +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 .infos.information import Information from .settings import Settings COMMISION_UNIT_VARIETY = 4 @@ -45,20 +67,11 @@ PLAYER_BUDGET_IMPORTANCE_LOG = 2 class Game: - settings = None # type: Settings - budget = PLAYER_BUDGET_INITIAL - events = None # type: typing.List[Event] - pending_transfers = None # type: typing.Dict[] - ignored_cps = None # type: typing.Collection[ControlPoint] - turn = 0 - game_stats: GameStats = None - - current_unit_id = 0 - current_group_id = 0 - - def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, settings): + def __init__(self, player_name: str, enemy_name: str, + theater: ConflictTheater, start_date: datetime, + settings: Settings): self.settings = settings - self.events = [] + self.events: List[Event] = [] self.theater = theater self.player_name = player_name self.player_country = db.FACTIONS[player_name]["country"] @@ -68,17 +81,25 @@ class Game: self.date = datetime(start_date.year, start_date.month, start_date.day) self.game_stats = GameStats() self.game_stats.update(self) - self.planners = {} - self.ground_planners = {} + self.ground_planners: Dict[int, GroundPlanner] = {} self.informations = [] self.informations.append(Information("Game Start", "-" * 40, 0)) self.__culling_points = self.compute_conflicts_position() - self.__frontlineData = [] - self.__destroyed_units = [] - self.jtacs = [] + self.__destroyed_units: List[str] = [] self.savepath = "" + self.budget = PLAYER_BUDGET_INITIAL + self.current_unit_id = 0 + self.current_group_id = 0 + + self.blue_ato = AirTaskingOrder() + self.red_ato = AirTaskingOrder() + + self.aircraft_inventory = GlobalAircraftInventory( + self.theater.controlpoints + ) self.sanitize_sides() + self.on_load() def sanitize_sides(self): @@ -95,11 +116,11 @@ class Game: self.enemy_country = "Russia" @property - def player_faction(self): + def player_faction(self) -> Dict[str, Any]: return db.FACTIONS[self.player_name] @property - def enemy_faction(self): + def enemy_faction(self) -> Dict[str, Any]: return db.FACTIONS[self.enemy_name] def _roll(self, prob, mult): @@ -116,7 +137,7 @@ class Game: for player_cp, enemy_cp in self.theater.conflicts(True): self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp) - def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]: + def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]: importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW) if for_task == AirDefence and not self.settings.sams: @@ -190,12 +211,14 @@ class Game: def is_player_attack(self, event): if isinstance(event, Event): - return event.attacker_name == self.player_name + return event and event.attacker_name and event.attacker_name == self.player_name else: - return event.name == self.player_name + return event and event.name and event.name == self.player_name - def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): + def on_load(self) -> None: + ObjectiveDistanceCache.set_theater(self.theater) + def pass_turn(self, no_action=False): logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.turn = self.turn + 1 @@ -219,26 +242,24 @@ class Game: if not cp.is_carrier and not cp.is_lha: cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY) - self.ignored_cps = [] - if ignored_cps: - self.ignored_cps = ignored_cps - - self.events = [] # type: typing.List[Event] + self.events = [] self._generate_events() # Update statistics self.game_stats.update(self) + self.aircraft_inventory.reset() + for cp in self.theater.controlpoints: + self.aircraft_inventory.set_from_control_point(cp) + # Plan flights & combat for next turn self.__culling_points = self.compute_conflicts_position() - self.planners = {} 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() for cp in self.theater.controlpoints: - if cp.has_runway(): - planner = FlightPlanner(cp, self) - planner.plan_flights() - self.planners[cp.id] = planner - if cp.has_frontline: gplanner = GroundPlanner(cp, self) gplanner.plan_groundwar() @@ -408,10 +429,10 @@ class Game: return 1 def get_player_coalition(self): - return dcs.action.Coalition.Blue + return Coalition.Blue def get_enemy_coalition(self): - return dcs.action.Coalition.Red + return Coalition.Red def get_player_color(self): return "blue" diff --git a/game/inventory.py b/game/inventory.py new file mode 100644 index 00000000..5ef68b04 --- /dev/null +++ b/game/inventory.py @@ -0,0 +1,130 @@ +"""Inventory management APIs.""" +from collections import defaultdict +from typing import Dict, Iterable, Iterator, Set, Tuple + +from dcs.unittype import UnitType + +from gen.flights.flight import Flight +from theater import ControlPoint + + +class ControlPointAircraftInventory: + """Aircraft inventory for a single control point.""" + + def __init__(self, control_point: ControlPoint) -> None: + self.control_point = control_point + self.inventory: Dict[UnitType, int] = defaultdict(int) + + def add_aircraft(self, aircraft: UnitType, count: int) -> None: + """Adds aircraft to the inventory. + + Args: + aircraft: The type of aircraft to add. + count: The number of aircraft to add. + """ + self.inventory[aircraft] += count + + def remove_aircraft(self, aircraft: UnitType, count: int) -> None: + """Removes aircraft from the inventory. + + Args: + aircraft: The type of aircraft to remove. + count: The number of aircraft to remove. + + Raises: + ValueError: The control point cannot fulfill the requested number of + aircraft. + """ + available = self.inventory[aircraft] + if available < count: + raise ValueError( + f"Cannot remove {count} {aircraft.id} from " + f"{self.control_point.name}. Only have {available}." + ) + self.inventory[aircraft] -= count + + def available(self, aircraft: UnitType) -> int: + """Returns the number of available aircraft of the given type. + + Args: + aircraft: The type of aircraft to query. + """ + return self.inventory[aircraft] + + @property + def types_available(self) -> Iterator[UnitType]: + """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]]: + """Iterates over all available aircraft types, including amounts.""" + for aircraft, count in self.inventory.items(): + if count > 0: + yield aircraft, count + + @property + def total_available(self) -> int: + """Returns the total number of aircraft available.""" + # TODO: Remove? + # This probably isn't actually useful. It's used by the AI flight + # planner to determine how many flights of a given type it should + # allocate, but it should probably be making that decision based on the + # number of aircraft available to perform a particular role. + return sum(self.inventory.values()) + + def clear(self) -> None: + """Clears all aircraft from the inventory.""" + self.inventory.clear() + + +class GlobalAircraftInventory: + """Game-wide aircraft inventory.""" + def __init__(self, control_points: Iterable[ControlPoint]) -> None: + self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = { + cp: ControlPointAircraftInventory(cp) for cp in control_points + } + + def reset(self) -> None: + """Clears all control points and their inventories.""" + for inventory in self.inventories.values(): + inventory.clear() + + def set_from_control_point(self, control_point: ControlPoint) -> None: + """Set the control point's aircraft inventory. + + If the inventory for the given control point has already been set for + the turn, it will be overwritten. + """ + inventory = self.inventories[control_point] + for aircraft, count in control_point.base.aircraft.items(): + inventory.add_aircraft(aircraft, count) + + def for_control_point( + self, + control_point: ControlPoint) -> ControlPointAircraftInventory: + """Returns the inventory specific to the given control point.""" + return self.inventories[control_point] + + @property + def available_types_for_player(self) -> Iterator[UnitType]: + """Iterates over all aircraft types available to the player.""" + seen: Set[UnitType] = set() + for control_point, inventory in self.inventories.items(): + if control_point.captured: + for aircraft in inventory.types_available: + if aircraft not in seen: + seen.add(aircraft) + yield aircraft + + def claim_for_flight(self, flight: Flight) -> None: + """Removes aircraft from the inventory for the given flight.""" + inventory = self.for_control_point(flight.from_cp) + inventory.remove_aircraft(flight.unit_type, flight.count) + + def return_from_flight(self, flight: Flight) -> None: + """Returns a flight's aircraft to the inventory.""" + inventory = self.for_control_point(flight.from_cp) + inventory.add_aircraft(flight.unit_type, flight.count) diff --git a/game/models/game_stats.py b/game/models/game_stats.py index 2690d861..e6d628f4 100644 --- a/game/models/game_stats.py +++ b/game/models/game_stats.py @@ -1,3 +1,5 @@ +from typing import List + class FactionTurnMetadata: """ Store metadata about a faction @@ -31,10 +33,8 @@ class GameStats: Store statistics for the current game """ - data_per_turn: [GameTurnMetadata] = [] - def __init__(self): - self.data_per_turn = [] + self.data_per_turn: List[GameTurnMetadata] = [] def update(self, game): """ diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 48c5965c..19255247 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -1,7 +1,8 @@ -from game.db import assigned_units_split - -from .operation import * +from dcs.terrain.terrain import Terrain +from gen.conflictgen import Conflict +from .operation import Operation +from .. import db MAX_DISTANCE_BETWEEN_GROUPS = 12000 diff --git a/game/operation/operation.py b/game/operation/operation.py index c0ce5e67..ecc82e51 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,30 +1,51 @@ -from typing import Set +import logging +import os +from pathlib import Path +from typing import List, Optional, Set -from gen import * -from gen.airfields import AIRFIELD_DATA -from gen.beacons import load_beacons_for_terrain -from gen.radios import RadioRegistry -from gen.tacan import TacanRegistry +from dcs import Mission +from dcs.action import DoScript, DoScriptFile +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 userdata.debriefing import * +from dcs.translation import String +from dcs.triggers import TriggerStart +from dcs.unittype import UnitType + +from gen import Conflict, VisualGenerator +from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData +from gen.airfields import AIRFIELD_DATA +from gen.airsupportgen import AirSupport, AirSupportConflictGenerator +from gen.armor import GroundConflictGenerator, JtacInfo +from gen.beacons import load_beacons_for_terrain +from gen.briefinggen import BriefingGenerator +from gen.environmentgen import EnviromentGenerator +from gen.forcedoptionsgen import ForcedOptionsGenerator +from gen.groundobjectsgen import GroundObjectsGenerator +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 class Operation: attackers_starting_position = None # type: db.StartingPosition defenders_starting_position = None # type: db.StartingPosition - current_mission = None # type: dcs.Mission - regular_mission = None # type: dcs.Mission - quick_mission = None # type: dcs.Mission + current_mission = None # type: Mission + regular_mission = None # type: Mission + quick_mission = None # type: Mission conflict = None # type: Conflict - armorgen = None # type: ArmorConflictGenerator airgen = None # type: AircraftConflictGenerator triggersgen = None # type: TriggersGenerator airsupportgen = None # type: AirSupportConflictGenerator visualgen = None # type: VisualGenerator - envgen = None # type: EnvironmentGenerator + envgen = None # type: EnviromentGenerator groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator @@ -43,7 +64,7 @@ class Operation: defender_name: str, from_cp: ControlPoint, departure_cp: ControlPoint, - to_cp: ControlPoint = None): + to_cp: ControlPoint): self.game = game self.attacker_name = attacker_name self.attacker_country = db.FACTIONS[attacker_name]["country"] @@ -55,7 +76,7 @@ class Operation: self.to_cp = to_cp self.is_quick = False - def units_of(self, country_name: str) -> typing.Collection[UnitType]: + def units_of(self, country_name: str) -> List[UnitType]: return [] def is_successfull(self, debriefing: Debriefing) -> bool: @@ -68,32 +89,14 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.radio_registry = RadioRegistry() - self.tacan_registry = TacanRegistry() - self.airgen = AircraftConflictGenerator( - mission, conflict, self.game.settings, self.game, - self.radio_registry) - self.airsupportgen = AirSupportConflictGenerator( - mission, conflict, self.game, self.radio_registry, - self.tacan_registry) - self.triggersgen = TriggersGenerator(mission, conflict, self.game) - self.visualgen = VisualGenerator(mission, conflict, self.game) - self.envgen = EnviromentGenerator(mission, conflict, self.game) - self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator( - mission, - conflict, - self.game, - self.radio_registry, - self.tacan_registry - ) - self.briefinggen = BriefingGenerator(mission, conflict, self.game) + self.briefinggen = BriefingGenerator(self.current_mission, + self.conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): with open("resources/default_options.lua", "r") as f: options_dict = loads(f.read())["options"] - self.current_mission = dcs.Mission(terrain) + self.current_mission = Mission(terrain) print(self.game.player_country) print(country_dict[db.country_id_from_name(self.game.player_country)]) @@ -124,9 +127,16 @@ class Operation: self.defenders_starting_position = None else: self.attackers_starting_position = self.departure_cp.at - self.defenders_starting_position = self.to_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 generate(self): + radio_registry = RadioRegistry() + tacan_registry = TacanRegistry() + # Dedup beacon/radio frequencies, since some maps have some frequencies # used multiple times. beacons = load_beacons_for_terrain(self.game.theater.terrain.name) @@ -138,7 +148,7 @@ class Operation: logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: - self.tacan_registry.reserve(beacon.tacan_channel) + tacan_registry.reserve(beacon.tacan_channel) for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: @@ -150,16 +160,26 @@ class Operation: # beacon list. for frequency in unique_map_frequencies: - self.radio_registry.reserve(frequency) + radio_registry.reserve(frequency) # Generate meteo + envgen = EnviromentGenerator(self.current_mission, self.conflict, + self.game) if self.environment_settings is None: - self.environment_settings = self.envgen.generate() + self.environment_settings = envgen.generate() else: - self.envgen.load(self.environment_settings) + envgen.load(self.environment_settings) # Generate ground object first - self.groundobjectgen.generate() + + 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(): @@ -180,24 +200,27 @@ class Operation: dead=True, ) - # Air Support (Tanker & Awacs) - self.airsupportgen.generate(self.is_awacs_enabled) + airsupportgen = AirSupportConflictGenerator( + self.current_mission, self.conflict, self.game, radio_registry, + tacan_registry) + airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map - for cp in self.game.theater.controlpoints: - side = cp.captured - if side: - country = self.current_mission.country(self.game.player_country) - else: - country = self.current_mission.country(self.game.enemy_country) - if cp.id in self.game.planners.keys(): - self.airgen.generate_flights( - cp, - country, - self.game.planners[cp.id], - self.groundobjectgen.runways - ) + 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] = [] @@ -221,18 +244,20 @@ class Operation: self.current_mission.groundControl.red_tactical_commander = self.ca_slots # Triggers - if self.game.is_player_attack(self.conflict.attackers_country): - cp = self.conflict.from_cp - else: - cp = self.conflict.to_cp - self.triggersgen.generate() + triggersgen = TriggersGenerator(self.current_mission, self.conflict, + self.game) + triggersgen.generate() # Options - self.forcedoptionsgen.generate() + 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: - self.visualgen.generate() + visualgen.generate() # Inject Plugins Lua Scripts listOfPluginsScripts = [] @@ -327,19 +352,20 @@ class Operation: trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - self.assign_channels_to_flights() + self.assign_channels_to_flights(airgen.flights, + airsupportgen.air_support) kneeboard_generator = KneeboardGenerator(self.current_mission) - for dynamic_runway in self.groundobjectgen.runways.values(): + for dynamic_runway in groundobjectgen.runways.values(): self.briefinggen.add_dynamic_runway(dynamic_runway) - for tanker in self.airsupportgen.air_support.tankers: + for tanker in airsupportgen.air_support.tankers: self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - for awacs in self.airsupportgen.air_support.awacs: + for awacs in airsupportgen.air_support.awacs: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) @@ -347,21 +373,23 @@ class Operation: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) - for flight in self.airgen.flights: + for flight in airgen.flights: self.briefinggen.add_flight(flight) kneeboard_generator.add_flight(flight) self.briefinggen.generate() kneeboard_generator.generate() - def assign_channels_to_flights(self) -> None: + def assign_channels_to_flights(self, flights: List[FlightData], + air_support: AirSupport) -> None: """Assigns preset radio channels for client flights.""" - for flight in self.airgen.flights: + for flight in flights: if not flight.client_units: continue - self.assign_channels_to_flight(flight) + self.assign_channels_to_flight(flight, air_support) - def assign_channels_to_flight(self, flight: FlightData) -> None: + def assign_channels_to_flight(self, flight: FlightData, + air_support: AirSupport) -> None: """Assigns preset radio channels for a client flight.""" airframe = flight.aircraft_type @@ -371,5 +399,7 @@ class Operation: logging.warning(f"No aircraft data for {airframe.id}") return - aircraft_data.channel_allocator.assign_channels_for_flight( - flight, self.airsupportgen.air_support) + if aircraft_data.channel_allocator is not None: + aircraft_data.channel_allocator.assign_channels_for_flight( + flight, air_support + ) diff --git a/userdata/persistency.py b/game/persistency.py similarity index 82% rename from userdata/persistency.py rename to game/persistency.py index 9e55dda0..617274ea 100644 --- a/userdata/persistency.py +++ b/game/persistency.py @@ -2,8 +2,9 @@ import logging import os import pickle import shutil +from typing import Optional -_dcs_saved_game_folder = None # type: str +_dcs_saved_game_folder: Optional[str] = None _file_abs_path = None def setup(user_folder: str): @@ -40,30 +41,33 @@ def restore_game(): try: save = pickle.load(f) return save - except: - logging.error("Invalid Save game") + except Exception: + logging.exception("Invalid Save game") return None + def load_game(path): with open(path, "rb") as f: try: save = pickle.load(f) save.savepath = path return save - except: - logging.error("Invalid Save game") + except Exception: + logging.exception("Invalid Save game") return None + def save_game(game) -> bool: try: with open(_temporary_save_file(), "wb") as f: pickle.dump(game, f) shutil.copy(_temporary_save_file(), game.savepath) return True - except Exception as e: - logging.error(e) + except Exception: + logging.exception("Could not save game") return False + def autosave(game) -> bool: """ Autosave to the autosave location @@ -74,7 +78,7 @@ def autosave(game) -> bool: with open(_autosave_path(), "wb") as f: pickle.dump(game, f) return True - except Exception as e: - logging.error(e) + except Exception: + logging.exception("Could not save game") return False diff --git a/gen/__init__.py b/gen/__init__.py index ad11614f..6fd6547c 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,4 +1,3 @@ -from .aaa import * from .aircraft import * from .armor import * from .airsupportgen import * @@ -12,4 +11,3 @@ from .forcedoptionsgen import * from .kneeboard import * from . import naming - diff --git a/gen/aaa.py b/gen/aaa.py deleted file mode 100644 index d9822202..00000000 --- a/gen/aaa.py +++ /dev/null @@ -1,51 +0,0 @@ -from .conflictgen import * -from .naming import * - -from dcs.mission import * -from dcs.mission import * - -from .conflictgen import * -from .naming import * - -DISTANCE_FACTOR = 0.5, 1 -EXTRA_AA_MIN_DISTANCE = 50000 -EXTRA_AA_MAX_DISTANCE = 150000 -EXTRA_AA_POSITION_FROM_CP = 550 - -class ExtraAAConflictGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game, player_country: Country, enemy_country: Country): - self.mission = mission - self.game = game - self.conflict = conflict - self.player_country = player_country - self.enemy_country = enemy_country - - def generate(self): - - for cp in self.game.theater.controlpoints: - if cp.is_global: - continue - - if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.to_cp.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MAX_DISTANCE: - continue - - country_name = cp.captured and self.player_country or self.enemy_country - position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP) - - self.mission.vehicle_group( - country=self.mission.country(country_name), - name=namegen.next_basedefense_name(), - _type=db.EXTRA_AA[country_name], - position=position, - group_size=1 - ) - diff --git a/gen/aircraft.py b/gen/aircraft.py index ec5dec26..f9cd63f1 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,30 +1,84 @@ +import logging +import random from dataclasses import dataclass -from typing import Type +from typing import Dict, List, Optional, Tuple, Type, Union from dcs import helicopters -from dcs.action import ActivateGroup, AITaskPush, MessageToAll -from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from dcs.action import AITaskPush, ActivateGroup, MessageToAll +from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter +from dcs.country import Country from dcs.flyingunit import FlyingUnit -from dcs.helicopters import helicopter_map, UH_1H +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, + Bf_109K_4, + FW_190A8, + FW_190D9, + F_14B, + I_16, + JF_17, + Ju_88A4, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_33, +) +from dcs.point import PointAction +from dcs.task import ( + AntishipStrike, + AttackGroup, + Bombing, + CAP, + CAS, + ControlledTask, + EPLRS, + EngageTargets, + Escort, + GroundAttack, + MainTask, + NoTask, + OptROE, + OptRTBOnBingoFuel, + OptRTBOnOutOfAmmo, + OptReactOnThreat, + OptRestrictAfterburner, + OptRestrictJettison, + OrbitAction, + PinpointStrike, + SEAD, + StartCommand, + Targets, + Task, +) from dcs.terrain.terrain import Airport, NoParkingSlotError -from dcs.triggers import TriggerOnce, Event +from dcs.translation import String +from dcs.triggers import Event, TriggerOnce +from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup +from dcs.unittype import FlyingType, UnitType +from game import db from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter from gen.airfields import RunwayData from gen.airsupportgen import AirSupport +from gen.ato import AirTaskingOrder from gen.callsigns import create_group_callsign_from_unit -from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, FlightType, FlightWaypoint, FlightWaypointType, ) -from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry -from .conflictgen import * -from .naming import * +from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio +from theater.controlpoint import ControlPoint, ControlPointType +from .naming import namegen +from .conflictgen import Conflict WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_ALT = 500 @@ -264,8 +318,12 @@ class CommonRadioChannelAllocator(RadioChannelAllocator): def assign_channels_for_flight(self, flight: FlightData, air_support: AirSupport) -> None: - flight.assign_channel( - self.intra_flight_radio_index, 1, flight.intra_flight_channel) + if self.intra_flight_radio_index is not None: + flight.assign_channel( + self.intra_flight_radio_index, 1, flight.intra_flight_channel) + + if self.inter_flight_radio_index is None: + return # For cases where the inter-flight and intra-flight radios share presets # (the JF-17 only has one set of channels, even though it can use two @@ -335,8 +393,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator): # the guard channel. radio_id = 1 flight.assign_channel(radio_id, 1, flight.intra_flight_channel) - flight.assign_channel(radio_id, 4, flight.departure.atc) - flight.assign_channel(radio_id, 5, flight.arrival.atc) + if flight.departure.atc is not None: + flight.assign_channel(radio_id, 4, flight.departure.atc) + if flight.arrival.atc is not None: + flight.assign_channel(radio_id, 5, flight.arrival.atc) # TODO: Assign divert to 6 when we support divert airfields. @@ -348,8 +408,10 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator): air_support: AirSupport) -> None: radio_id = 1 flight.assign_channel(radio_id, 1, flight.intra_flight_channel) - flight.assign_channel(radio_id, 2, flight.departure.atc) - flight.assign_channel(radio_id, 3, flight.arrival.atc) + if flight.departure.atc is not None: + flight.assign_channel(radio_id, 2, flight.departure.atc) + if flight.arrival.atc is not None: + flight.assign_channel(radio_id, 3, flight.arrival.atc) # TODO : Some GCI on Channel 4 ? @@ -471,8 +533,6 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] class AircraftConflictGenerator: - escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] - def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game, radio_registry: RadioRegistry): self.m = mission @@ -480,7 +540,7 @@ class AircraftConflictGenerator: self.settings = settings self.conflict = conflict self.radio_registry = radio_registry - self.escort_targets = [] + self.escort_targets: List[Tuple[FlyingGroup, int]] = [] self.flights: List[FlightData] = [] def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: @@ -502,33 +562,23 @@ class AircraftConflictGenerator: def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm - def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], + def _setup_group(self, group: FlyingGroup, for_task: Type[Task], flight: Flight, dynamic_runways: Dict[str, RunwayData]): did_load_loadout = False unit_type = group.units[0].unit_type if unit_type in db.PLANE_PAYLOAD_OVERRIDES: override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type] - if type(override_loadout) == dict: + # Clear pylons + for p in group.units: + p.pylons.clear() - # Clear pylons - for p in group.units: - p.pylons.clear() - - # Now load loadout - if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task] - group.load_loadout(payload_name) - did_load_loadout = True - logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) - elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"] - group.load_loadout(payload_name) - did_load_loadout = True - logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) - elif issubclass(override_loadout, MainTask): - group.load_task_default_loadout(override_loadout) + # Now load loadout + if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: + payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task] + group.load_loadout(payload_name) did_load_loadout = True + logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) if not did_load_loadout: group.load_task_default_loadout(for_task) @@ -590,7 +640,7 @@ class AircraftConflictGenerator: # Special case so Su 33 carrier take off if unit_type is Su_33: - if task is not CAP: + if flight.flight_type is not CAP: for unit in group.units: unit.fuel = Su_33.fuel_max / 2.2 else: @@ -613,9 +663,12 @@ class AircraftConflictGenerator: # so just use the first runway. return runways[0] - def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup: + def _generate_at_airport(self, name: str, side: Country, + unit_type: FlyingType, count: int, + client_count: int, + airport: Optional[Airport] = None, + start_type=None) -> FlyingGroup: assert count > 0 - assert unit is not None if start_type is None: start_type = self._start_type() @@ -633,7 +686,6 @@ class AircraftConflictGenerator: def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup: assert count > 0 - assert unit is not None if unit_type in helicopters.helicopter_map.values(): alt = WARM_START_HELI_ALT @@ -660,9 +712,11 @@ class AircraftConflictGenerator: group.points[0].alt_type = "RADIO" return group - def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup], start_type=None) -> FlyingGroup: + def _generate_at_group(self, name: str, side: Country, + unit_type: FlyingType, count: int, client_count: int, + at: Union[ShipGroup, StaticGroup], + start_type=None) -> FlyingGroup: assert count > 0 - assert unit is not None if start_type is None: start_type = self._start_type() @@ -688,7 +742,7 @@ class AircraftConflictGenerator: return self._generate_at_group(name, side, unit_type, count, client_count, at) else: return self._generate_inflight(name, side, unit_type, count, client_count, at.position) - elif issubclass(at, Airport): + elif isinstance(at, Airport): takeoff_ban = unit_type in db.TAKEOFF_BAN ai_ban = client_count == 0 and self.settings.only_player_takeoff @@ -707,8 +761,9 @@ class AircraftConflictGenerator: point.alt_type = "RADIO" return point - def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None): - if not at: + def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, + at: Optional[db.StartingPosition] = None): + if at is None: at = cp.at position = at if isinstance(at, Point) else at.position @@ -751,31 +806,28 @@ class AircraftConflictGenerator: else: logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) - - def generate_flights(self, cp, country, flight_planner: FlightPlanner, - dynamic_runways: Dict[str, RunwayData]): - # Clear pydcs parking slots - if cp.airport is not None: - logging.info("CLEARING SLOTS @ " + cp.airport.name) - logging.info("===============") + def clear_parking_slots(self) -> None: + for cp in self.game.theater.controlpoints: if cp.airport is not None: - for ps in cp.airport.parking_slots: - logging.info("SLOT : " + str(ps.unit_id)) - ps.unit_id = None - logging.info("----------------") - logging.info("===============") + for parking_slot in cp.airport.parking_slots: + parking_slot.unit_id = None - for flight in flight_planner.flights: - - if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position): - logging.info("Flight not generated : culled") - continue - logging.info("Generating flight : " + str(flight.unit_type)) - group = self.generate_planned_flight(cp, country, flight) - self.setup_flight_group(group, flight, flight.flight_type, - dynamic_runways) - self.setup_group_activation_trigger(flight, group) + def generate_flights(self, country, ato: AirTaskingOrder, + dynamic_runways: Dict[str, RunwayData]) -> None: + self.clear_parking_slots() + for package in ato.packages: + for flight in package.flights: + culled = self.game.position_culled(flight.from_cp.position) + if flight.client_count == 0 and culled: + logging.info("Flight not generated: culled") + continue + logging.info(f"Generating flight: {flight.unit_type}") + group = self.generate_planned_flight(flight.from_cp, country, + flight) + self.setup_flight_group(group, flight, flight.flight_type, + dynamic_runways) + self.setup_group_activation_trigger(flight, group) def setup_group_activation_trigger(self, flight, group): if flight.scheduled_in > 0 and flight.client_count == 0: @@ -932,6 +984,14 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) + elif flight_type == FlightType.ESCORT: + group.task = Escort.name + self._setup_group(group, Escort, flight, dynamic_runways) + # TODO: Cleanup duplication... + group.points[0].tasks.clear() + group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) + group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) + group.points[0].tasks.append(OptRestrictJettison(True)) group.points[0].tasks.append(OptRTBOnBingoFuel(True)) group.points[0].tasks.append(OptRestrictAfterburner(True)) @@ -952,6 +1012,7 @@ class AircraftConflictGenerator: # pt.tasks.append(engagetgt) elif point.waypoint_type == FlightWaypointType.LANDING_POINT: pt.type = "Land" + pt.action = PointAction.Landing elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE: if group.units[0].unit_type == B_17G: diff --git a/gen/airfields.py b/gen/airfields.py index b7e08712..b3185158 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -195,10 +195,12 @@ AIRFIELD_DATA = { runway_length=8623, atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)), outer_ndb={ - "22": ("AP", MHz(443, 0)), "4": "443.00 (AN)" + "22": ("AP", MHz(443, 0)), + "04": ("AN", MHz(443)), }, inner_ndb={ - "22": ("P", MHz(215, 0)), "4": "215.00 (N)" + "22": ("P", MHz(215, 0)), + "04": ("N", MHz(215)), }, ), diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 791c80b6..8a98dba7 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,8 +1,21 @@ from dataclasses import dataclass, field +from typing import List, Type +from dcs.mission import Mission, StartType +from dcs.planes import IL_78M +from dcs.task import ( + AWACS, + ActivateBeaconCommand, + MainTask, + Refueling, + SetImmortalCommand, + SetInvisibleCommand, +) + +from game import db +from .naming import namegen from .callsigns import callsign_for_support_unit -from .conflictgen import * -from .naming import * +from .conflictgen import Conflict from .radios import RadioFrequency, RadioRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry @@ -49,7 +62,7 @@ class AirSupportConflictGenerator: self.tacan_registry = tacan_registry @classmethod - def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]: + def support_tasks(cls) -> List[Type[MainTask]]: return [Refueling, AWACS] def generate(self, is_awacs_enabled): @@ -76,6 +89,7 @@ class AirSupportConflictGenerator: speed=574, tacanchannel=str(tacan), ) + tanker_group.set_frequency(freq.mhz) callsign = callsign_for_support_unit(tanker_group) tacan_callsign = { @@ -118,6 +132,8 @@ class AirSupportConflictGenerator: frequency=freq.mhz, start_type=StartType.Warm, ) + awacs_flight.set_frequency(freq.mhz) + awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) diff --git a/gen/armor.py b/gen/armor.py index 5042149f..426dc05a 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,13 +1,39 @@ +import logging +import random from dataclasses import dataclass +from typing import List +from dcs import Mission from dcs.action import AITaskPush -from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess -from dcs.triggers import TriggerOnce, Event +from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged +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.triggers import Event, TriggerOnce +from dcs.unit import Vehicle +from dcs.unittype import VehicleType -from gen import namegen -from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE +from game import db +from .naming import namegen +from gen.ground_forces.ai_ground_planner import ( + CombatGroupRole, + DISTANCE_FROM_FRONTLINE, +) from .callsigns import callsign_for_support_unit -from .conflictgen import * +from .conflictgen import Conflict +from .ground_forces.combat_stance import CombatStance SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -48,7 +74,7 @@ class GroundConflictGenerator: self.jtacs: List[JtacInfo] = [] def _group_point(self, point) -> Point: - distance = randint( + distance = random.randint( int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]), ) @@ -165,7 +191,7 @@ class GroundConflictGenerator: heading=forward_heading, move_formation=PointAction.OffRoad) - for i in range(randint(3, 10)): + for i in range(random.randint(3, 10)): u = random.choice(possible_infantry_units) position = infantry_position.random_point_within(55, 5) self.mission.vehicle_group( @@ -183,6 +209,11 @@ class GroundConflictGenerator: 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 group.role == CombatGroupRole.ARTILLERY: # Fire on any ennemy in range if self.game.settings.perf_artillery: diff --git a/gen/ato.py b/gen/ato.py new file mode 100644 index 00000000..ee3a3c55 --- /dev/null +++ b/gen/ato.py @@ -0,0 +1,136 @@ +"""Air Tasking Orders. + +The classes of the Air Tasking Order (ATO) define all of the missions that have +been planned, and which aircraft have been assigned to them. Each planned +mission, or "package" is composed of individual flights. The package may contain +dissimilar aircraft performing different roles, but all for the same goal. For +example, the package to strike an enemy airfield may contain an escort flight, +a SEAD flight, and the strike aircraft themselves. CAP packages may contain only +the single CAP flight. +""" +from collections import defaultdict +from dataclasses import dataclass, field +import logging +from typing import Dict, Iterator, List, Optional + +from dcs.mapping import Point +from .flights.flight import Flight, FlightType +from theater.missiontarget import MissionTarget + + +@dataclass(frozen=True) +class Task: + """The main task of a flight or package.""" + + #: The type of task. + task_type: FlightType + + #: The location of the objective. + location: str + + +@dataclass +class Package: + """A mission package.""" + + #: The mission target. Currently can be either a ControlPoint or a + #: TheaterGroundObject (non-ControlPoint map objectives). + target: MissionTarget + + #: The set of flights in the package. + flights: List[Flight] = field(default_factory=list) + + delay: int = field(default=0) + + join_point: Optional[Point] = field(default=None, init=False, hash=False) + split_point: Optional[Point] = field(default=None, init=False, hash=False) + ingress_point: Optional[Point] = field(default=None, init=False, hash=False) + egress_point: Optional[Point] = field(default=None, init=False, hash=False) + + def add_flight(self, flight: Flight) -> None: + """Adds a flight to the package.""" + self.flights.append(flight) + + def remove_flight(self, flight: Flight) -> None: + """Removes a flight from the package.""" + self.flights.remove(flight) + if not self.flights: + self.ingress_point = None + self.egress_point = None + + @property + def primary_task(self) -> Optional[FlightType]: + if not self.flights: + return None + + flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0) + for flight in self.flights: + flight_counts[flight.flight_type] += 1 + + # The package will contain a mix of mission types, but in general we can + # determine the goal of the mission because some mission types are more + # likely to be the main task than others. For example, a package with + # only CAP flights is a CAP package, a flight with CAP and strike is a + # strike package, a flight with CAP and DEAD is a DEAD package, and a + # flight with strike and SEAD is an OCA/Strike package. The type of + # package is determined by the highest priority flight in the package. + task_priorities = [ + FlightType.CAS, + FlightType.STRIKE, + FlightType.ANTISHIP, + 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.ESCORT, + ] + for task in task_priorities: + if flight_counts[task]: + return task + + # If we get here, our task_priorities list above is incomplete. Log the + # issue and return the type of *any* flight in the package. + some_mission = next(iter(self.flights)).flight_type + logging.warning(f"Unhandled mission type: {some_mission}") + return some_mission + + @property + def package_description(self) -> str: + """Generates a package description based on flight composition.""" + task = self.primary_task + if task is None: + return "No mission" + return task.name + + def __hash__(self) -> int: + # TODO: Far from perfect. Number packages? + return hash(self.target.name) + + +@dataclass +class AirTaskingOrder: + """The entire ATO for one coalition.""" + + #: The set of all planned packages in the ATO. + packages: List[Package] = field(default_factory=list) + + def add_package(self, package: Package) -> None: + """Adds a package to the ATO.""" + self.packages.append(package) + + def remove_package(self, package: Package) -> None: + """Removes a package from the ATO.""" + self.packages.remove(package) + + def clear(self) -> None: + """Removes all packages from the ATO.""" + self.packages.clear() diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 10e07001..82744a8a 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -106,7 +106,7 @@ class BriefingGenerator(MissionInfoGenerator): aircraft = flight.aircraft_type flight_unit_name = db.unit_type_name(aircraft) self.description += "-" * 50 + "\n" - self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" + self.description += f"{flight_unit_name} x {flight.size}\n\n" for i, wpt in enumerate(flight.waypoints): self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 9b83b51e..3c9eecfe 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -1,21 +1,11 @@ import logging -import typing -import pdb -import dcs +import random +from typing import Tuple -from random import randint -from dcs import Mission +from dcs.country import Country +from dcs.mapping import Point -from dcs.mission import * -from dcs.vehicles import * -from dcs.unitgroup import * -from dcs.unittype import * -from dcs.mapping import * -from dcs.point import * -from dcs.task import * -from dcs.country import * - -from theater import * +from theater import ConflictTheater, ControlPoint AIR_DISTANCE = 40000 @@ -65,24 +55,6 @@ def _heading_sum(h, a) -> int: class Conflict: - attackers_side = None # type: str - defenders_side = None # type: str - attackers_country = None # type: Country - defenders_country = None # type: Country - from_cp = None # type: ControlPoint - to_cp = None # type: ControlPoint - position = None # type: Point - size = None # type: int - radials = None # type: typing.List[int] - - heading = None # type: int - distance = None # type: int - - ground_attackers_location = None # type: Point - ground_defenders_location = None # type: Point - air_attackers_location = None # type: Point - air_defenders_location = None # type: Point - def __init__(self, theater: ConflictTheater, from_cp: ControlPoint, @@ -155,7 +127,7 @@ class Conflict: else: return self.position - def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> typing.Optional[Point]: + def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point: return Conflict._find_ground_position(at, max_distance, heading, self.theater) @classmethod @@ -163,7 +135,7 @@ class Conflict: return from_cp.has_frontline and to_cp.has_frontline @classmethod - def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[typing.Tuple[Point, int]]: + 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) @@ -174,9 +146,7 @@ class Conflict: @classmethod - def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]: - initial, heading = cls.frontline_position(theater, from_cp, to_cp) - + 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) ]) @@ -193,9 +163,6 @@ class Conflict: return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length """ frontline = cls.frontline_position(theater, from_cp, to_cp) - if not frontline: - return None - center_position, heading = frontline left_position, right_position = None, None @@ -243,7 +210,7 @@ class Conflict: """ @classmethod - def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]: + 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): @@ -302,10 +269,14 @@ class Conflict: distance = to_cp.size * GROUND_DISTANCE_FACTOR attackers_location = position.point_from_heading(attack_heading, distance) - attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater) + 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, distance * 2, _heading_sum(defense_heading, 180), theater) + defenders_location = Conflict._find_ground_position( + defenders_location, int(distance * 2), + _heading_sum(defense_heading, 180), theater) return cls( position=position, @@ -429,7 +400,7 @@ class Conflict: 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, randint(0, int(distance))) + 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)) @@ -456,7 +427,9 @@ class Conflict: distance = to_cp.size * GROUND_DISTANCE_FACTOR defenders_location = position.point_from_heading(defense_heading, distance) - defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater) + defenders_location = Conflict._find_ground_position( + defenders_location, int(distance * 2), + _heading_sum(defense_heading, 180), theater) return cls( position=position, diff --git a/gen/environmentgen.py b/gen/environmentgen.py index 1d861842..57d70452 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -1,20 +1,11 @@ import logging -import typing import random -from datetime import datetime, timedelta, time +from datetime import timedelta from dcs.mission import Mission -from dcs.triggers import * -from dcs.condition import * -from dcs.action import * -from dcs.unit import Skill -from dcs.point import MovingPoint, PointProperties -from dcs.action import * -from dcs.weather import * +from dcs.weather import Weather, Wind -from game import db -from theater import * -from gen import * +from .conflictgen import Conflict WEATHER_CLOUD_BASE = 2000, 3000 WEATHER_CLOUD_DENSITY = 1, 8 diff --git a/gen/fleet/ship_group_generator.py b/gen/fleet/ship_group_generator.py index 455d9f27..db974d6c 100644 --- a/gen/fleet/ship_group_generator.py +++ b/gen/fleet/ship_group_generator.py @@ -28,13 +28,13 @@ SHIP_MAP = { } -def generate_ship_group(game, ground_object, faction:str): +def generate_ship_group(game, ground_object, faction_name: str): """ This generate a ship group :return: Nothing, but put the group reference inside the ground object """ - faction = db.FACTIONS[faction] - if "boat" in faction.keys(): + faction = db.FACTIONS[faction_name] + if "boat" in faction: generators = faction["boat"] if len(generators) > 0: gen = random.choice(generators) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 99cf8427..7bc3fd26 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -1,789 +1,515 @@ -import math -import operator +from __future__ import annotations + +import logging import random +import operator +from dataclasses import dataclass +from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type + +from dcs.unittype import FlyingType, UnitType from game import db -from game.data.doctrine import MODERN_DOCTRINE from game.data.radar_db import UNITS_WITH_RADAR -from game.utils import meter_to_feet, nm_to_meter +from game.infos.information import Information +from game.utils import nm_to_meter from gen import Conflict -from gen.flights.ai_flight_planner_db import INTERCEPT_CAPABLE, CAP_CAPABLE, CAS_CAPABLE, SEAD_CAPABLE, STRIKE_CAPABLE, \ - DRONES -from gen.flights.flight import Flight, FlightType, FlightWaypoint, FlightWaypointType +from gen.ato import Package +from gen.flights.ai_flight_planner_db import ( + CAP_CAPABLE, + CAP_PREFERRED, + CAS_CAPABLE, + CAS_PREFERRED, + SEAD_CAPABLE, + SEAD_PREFERRED, + STRIKE_CAPABLE, + STRIKE_PREFERRED, +) +from gen.flights.closestairfields import ( + ClosestAirfields, + ObjectiveDistanceCache, +) +from gen.flights.flight import ( + Flight, + FlightType, +) +from gen.flights.flightplan import FlightPlanBuilder +from theater import ( + ControlPoint, + FrontLine, + MissionTarget, + TheaterGroundObject, +) + +# Avoid importing some types that cause circular imports unless type checking. +if TYPE_CHECKING: + from game import Game + from game.inventory import GlobalAircraftInventory -MISSION_DURATION = 80 +@dataclass(frozen=True) +class ProposedFlight: + """A flight outline proposed by the mission planner. + + Proposed flights haven't been assigned specific aircraft yet. They have only + a task, a required number of aircraft, and a maximum distance allowed + between the objective and the departure airfield. + """ + + #: The flight's role. + task: FlightType + + #: The number of aircraft required. + num_aircraft: int + + #: The maximum distance between the objective and the departure airfield. + max_distance: int + + def __str__(self) -> str: + return f"{self.task.name} {self.num_aircraft} ship" -class FlightPlanner: +@dataclass(frozen=True) +class ProposedMission: + """A mission outline proposed by the mission planner. - def __init__(self, from_cp, game): - # TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine - # TODO : the flight planner should plan package and operations - self.from_cp = from_cp + Proposed missions haven't been assigned aircraft yet. They have only an + objective location and a list of proposed flights that are required for the + mission. + """ + + #: The mission objective. + location: MissionTarget + + #: The proposed flights that are required for the mission. + flights: List[ProposedFlight] + + def __str__(self) -> str: + flights = ', '.join([str(f) for f in self.flights]) + return f"{self.location.name}: {flights}" + + +class AircraftAllocator: + """Finds suitable aircraft for proposed missions.""" + + def __init__(self, closest_airfields: ClosestAirfields, + global_inventory: GlobalAircraftInventory, + is_player: bool) -> None: + self.closest_airfields = closest_airfields + self.global_inventory = global_inventory + self.is_player = is_player + + def find_aircraft_for_flight( + self, flight: ProposedFlight + ) -> Optional[Tuple[ControlPoint, UnitType]]: + """Finds aircraft suitable for the given mission. + + Searches for aircraft capable of performing the given mission within the + maximum allowed range. If insufficient aircraft are available for the + mission, None is returned. + + Airfields are searched ordered nearest to farthest from the target and + searched twice. The first search looks for aircraft which prefer the + mission type, and the second search looks for any aircraft which are + capable of the mission type. For example, an F-14 from a nearby carrier + will be preferred for the CAP of an airfield that has only F-16s, but if + the carrier has only F/A-18s the F-16s will be used for CAP instead. + + Note that aircraft *will* be removed from the global inventory on + success. This is to ensure that the same aircraft are not matched twice + on subsequent calls. If the found aircraft are not used, the caller is + responsible for returning them to the inventory. + """ + result = self.find_aircraft_of_type( + flight, self.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) + ) + + @staticmethod + def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: + cap_missions = (FlightType.BARCAP, FlightType.CAP, 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.CAP, 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]]: + airfields_in_range = self.closest_airfields.airfields_within( + flight.max_distance + ) + for airfield in airfields_in_range: + if not airfield.is_friendly(self.is_player): + continue + inventory = self.global_inventory.for_control_point(airfield) + for aircraft, available in inventory.all_aircraft: + if aircraft in types and available >= flight.num_aircraft: + inventory.remove_aircraft(aircraft, flight.num_aircraft) + return airfield, aircraft + + return None + + +class PackageBuilder: + """Builds a Package for the flights it receives.""" + + def __init__(self, location: MissionTarget, + closest_airfields: ClosestAirfields, + global_inventory: GlobalAircraftInventory, + is_player: bool) -> None: + self.package = Package(location) + self.allocator = AircraftAllocator(closest_airfields, global_inventory, + is_player) + self.global_inventory = global_inventory + + def plan_flight(self, plan: ProposedFlight) -> bool: + """Allocates aircraft for the given flight and adds them to the package. + + If no suitable aircraft are available, False is returned. If the failed + flight was critical and the rest of the mission will be scrubbed, the + caller should return any previously planned flights to the inventory + using release_planned_aircraft. + """ + assignment = self.allocator.find_aircraft_for_flight(plan) + if assignment is None: + return False + airfield, aircraft = assignment + flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) + self.package.add_flight(flight) + return True + + def build(self) -> Package: + """Returns the built package.""" + return self.package + + def release_planned_aircraft(self) -> None: + """Returns any planned flights to the inventory.""" + flights = list(self.package.flights) + for flight in flights: + self.global_inventory.return_from_flight(flight) + self.package.remove_flight(flight) + + +class ObjectiveFinder: + """Identifies potential objectives for the mission planner.""" + + # TODO: Merge into doctrine. + AIRFIELD_THREAT_RANGE = nm_to_meter(150) + SAM_THREAT_RANGE = nm_to_meter(100) + + def __init__(self, game: Game, is_player: bool) -> None: self.game = game - self.aircraft_inventory = {} # local copy of the airbase inventory + self.is_player = is_player - if from_cp.captured: - self.faction = self.game.player_faction - else: - self.faction = self.game.enemy_faction + def enemy_sams(self) -> Iterator[TheaterGroundObject]: + """Iterates over all enemy SAM sites.""" + # Control points might have the same ground object several times, for + # some reason. + found_targets: Set[str] = set() + for cp in self.enemy_control_points(): + for ground_object in cp.ground_objects: + if ground_object.name in found_targets: + continue - if "doctrine" in self.faction.keys(): - self.doctrine = self.faction["doctrine"] - else: - self.doctrine = MODERN_DOCTRINE + if ground_object.dcs_identifier != "AA": + continue + if not self.object_has_radar(ground_object): + continue - def reset(self): + # TODO: Yield in order of most threatening. + # Need to sort in order of how close their defensive range comes + # to friendly assets. To do that we need to add effective range + # information to the database. + yield ground_object + found_targets.add(ground_object.name) + + def threatening_sams(self) -> Iterator[TheaterGroundObject]: + """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). """ - Reset the planned flights and available units + sams: List[Tuple[TheaterGroundObject, int]] = [] + for sam in self.enemy_sams(): + ranges: List[int] = [] + for cp in self.friendly_control_points(): + ranges.append(sam.distance_to(cp)) + sams.append((sam, min(ranges))) + + sams = sorted(sams, key=operator.itemgetter(1)) + for sam, _range in sams: + yield sam + + def strike_targets(self) -> Iterator[TheaterGroundObject]: + """Iterates over enemy strike targets. + + Targets are sorted by their closest proximity to any friendly control + point (airfield or fleet). """ - self.aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()}) - self.interceptor_flights = [] - self.cap_flights = [] - self.cas_flights = [] - self.strike_flights = [] - self.sead_flights = [] - self.custom_flights = [] - self.flights = [] - self.potential_sead_targets = [] - self.potential_strike_targets = [] + targets: List[Tuple[TheaterGroundObject, int]] = [] + # Control points might have the same ground object several times, for + # some reason. + found_targets: Set[str] = set() + for enemy_cp in self.enemy_control_points(): + for ground_object in enemy_cp.ground_objects: + if ground_object.name in found_targets: + continue + ranges: List[int] = [] + for friendly_cp in self.friendly_control_points(): + ranges.append(ground_object.distance_to(friendly_cp)) + targets.append((ground_object, min(ranges))) + found_targets.add(ground_object.name) + targets = sorted(targets, key=operator.itemgetter(1)) + for target, _range in targets: + yield target - def plan_flights(self): + @staticmethod + def object_has_radar(ground_object: TheaterGroundObject) -> bool: + """Returns True if the ground object contains a unit with radar.""" + for group in ground_object.groups: + for unit in group.units: + if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR: + return True + return False - self.reset() - self.compute_sead_targets() - self.compute_strike_targets() + def front_lines(self) -> Iterator[FrontLine]: + """Iterates over all active front lines in the theater.""" + for cp in self.friendly_control_points(): + for connected in cp.connected_points: + if connected.is_friendly(self.is_player): + continue - # The priority is to assign air-superiority fighter or interceptor to interception roles, so they can scramble if there is an attacker - # self.commision_interceptors() + if Conflict.has_frontline_between(cp, connected): + yield FrontLine(cp, connected) - # Then some CAP patrol for the next 2 hours - self.commision_cap() + def vulnerable_control_points(self) -> Iterator[ControlPoint]: + """Iterates over friendly CPs that are vulnerable to enemy CPs. - # Then setup cas - self.commision_cas() + Vulnerability is defined as any enemy CP within threat range of of the + CP. + """ + for cp in self.friendly_control_points(): + airfields_in_proximity = self.closest_airfields_to(cp) + airfields_in_threat_range = airfields_in_proximity.airfields_within( + self.AIRFIELD_THREAT_RANGE + ) + for airfield in airfields_in_threat_range: + if not airfield.is_friendly(self.is_player): + yield cp + break - # Then prepare some sead flights if required - self.commision_sead() + def friendly_control_points(self) -> Iterator[ControlPoint]: + """Iterates over all friendly control points.""" + return (c for c in self.game.theater.controlpoints if + c.is_friendly(self.is_player)) - self.commision_strike() + def enemy_control_points(self) -> Iterator[ControlPoint]: + """Iterates over all enemy control points.""" + return (c for c in self.game.theater.controlpoints if + not c.is_friendly(self.is_player)) - # TODO : commision ANTISHIP + def all_possible_targets(self) -> Iterator[MissionTarget]: + """Iterates over all possible mission targets in the theater. - def remove_flight(self, index): - try: - flight = self.flights[index] - if flight in self.interceptor_flights: self.interceptor_flights.remove(flight) - if flight in self.cap_flights: self.cap_flights.remove(flight) - if flight in self.cas_flights: self.cas_flights.remove(flight) - if flight in self.strike_flights: self.strike_flights.remove(flight) - if flight in self.sead_flights: self.sead_flights.remove(flight) - if flight in self.custom_flights: self.custom_flights.remove(flight) - self.flights.remove(flight) - except IndexError: + Valid mission targets are control points (airfields and carriers), front + lines, and ground objects (SAM sites, factories, resource extraction + sites, etc). + """ + for cp in self.game.theater.controlpoints: + yield cp + yield from cp.ground_objects + yield from self.front_lines() + + @staticmethod + def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: + """Returns the closest airfields to the given location.""" + return ObjectiveDistanceCache.get_closest_airfields(location) + + +class CoalitionMissionPlanner: + """Coalition flight planning AI. + + This class is responsible for automatically planning missions for the + coalition at the start of the turn. + + The primary goal of the mission planner is to protect existing friendly + assets. Missions will be planned with the following priorities: + + 1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy + losses of friendly aircraft. + 2. CAP for front line areas to protect ground and CAS units. + 3. DEAD to reduce necessity of SEAD for future missions. + 4. CAS to protect friendly ground units. + 5. Strike missions to reduce the enemy's resources. + + TODO: Anti-ship and airfield strikes to reduce enemy sortie rates. + TODO: BAI to prevent enemy forces from reaching the front line. + TODO: Should fleets always have a CAP? + + TODO: Stance and doctrine-specific planning behavior. + """ + + # TODO: Merge into doctrine, also limit by aircraft. + MAX_CAP_RANGE = nm_to_meter(100) + MAX_CAS_RANGE = nm_to_meter(50) + MAX_SEAD_RANGE = nm_to_meter(150) + MAX_STRIKE_RANGE = nm_to_meter(150) + + NON_CAP_MIN_DELAY = 1 + NON_CAP_MAX_DELAY = 5 + + def __init__(self, game: Game, is_player: bool) -> None: + self.game = game + 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 + + def propose_missions(self) -> Iterator[ProposedMission]: + """Identifies and iterates over potential mission in priority order.""" + # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. + for cp in self.objective_finder.vulnerable_control_points(): + yield ProposedMission(cp, [ + ProposedFlight(FlightType.CAP, 2, self.MAX_CAP_RANGE), + ]) + + # Find front lines, plan CAP. + for front_line in self.objective_finder.front_lines(): + yield ProposedMission(front_line, [ + ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), + ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), + ]) + + # Find enemy SAM sites with ranges that cover friendly CPs, front lines, + # or objects, plan DEAD. + # Find enemy SAM sites with ranges that extend to within 50 nmi of + # friendly CPs, front, lines, or objects, plan DEAD. + for sam in self.objective_finder.threatening_sams(): + yield ProposedMission(sam, [ + ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), + # TODO: Max escort range. + ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE), + ]) + + # Plan strike missions. + for target in self.objective_finder.strike_targets(): + yield ProposedMission(target, [ + ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), + # TODO: Max escort range. + ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE), + ]) + + def plan_missions(self) -> None: + """Identifies and plans mission for the turn.""" + for proposed_mission in self.propose_missions(): + self.plan_mission(proposed_mission) + + self.stagger_missions() + + for cp in self.objective_finder.friendly_control_points(): + inventory = self.game.aircraft_inventory.for_control_point(cp) + for aircraft, available in inventory.all_aircraft: + self.message("Unused aircraft", + f"{available} {aircraft.id} from {cp}") + + def plan_mission(self, mission: ProposedMission) -> None: + """Allocates aircraft for a proposed mission and adds it to the ATO.""" + builder = PackageBuilder( + mission.location, + self.objective_finder.closest_airfields_to(mission.location), + self.game.aircraft_inventory, + self.is_player + ) + + missing_types: Set[FlightType] = set() + for proposed_flight in mission.flights: + if not builder.plan_flight(proposed_flight): + missing_types.add(proposed_flight.task) + + if missing_types: + missing_types_str = ", ".join( + sorted([t.name for t in missing_types])) + builder.release_planned_aircraft() + self.message( + "Insufficient aircraft", + f"Not enough aircraft in range for {mission.location.name} " + f"capable of: {missing_types_str}") return - - def commision_interceptors(self): - """ - Pick some aircraft to assign them to interception roles - """ - - # At least try to generate one interceptor group - number_of_interceptor_groups = min(max(sum([v for k, v in self.aircraft_inventory.items()]) / 4, self.doctrine["MAX_NUMBER_OF_INTERCEPTION_GROUP"]), 1) - possible_interceptors = [k for k in self.aircraft_inventory.keys() if k in INTERCEPT_CAPABLE] - - if len(possible_interceptors) <= 0: - possible_interceptors = [k for k,v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] - - if number_of_interceptor_groups > 0: - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_interceptors}) - for i in range(number_of_interceptor_groups): - try: - unit = random.choice([k for k,v in inventory.items() if v >= 2]) - except IndexError: - break - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.INTERCEPTION) - flight.scheduled_in = 1 - flight.points = [] - - self.interceptor_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - def commision_cap(self): - """ - Pick some aircraft to assign them to defensive CAP roles (BARCAP) - """ - - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["CAP_EVERY_X_MINUTES"])): - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.CAP) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["CAP_EVERY_X_MINUTES"] - 5, self.doctrine["CAP_EVERY_X_MINUTES"] + 5) - - if len(self._get_cas_locations()) > 0: - enemy_cp = random.choice(self._get_cas_locations()) - self.generate_frontline_cap(flight, flight.from_cp, enemy_cp) - else: - self.generate_barcap(flight, flight.from_cp) - - self.cap_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - def commision_cas(self): - """ - Pick some aircraft to assign them to CAS - """ - - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAS_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - cas_location = self._get_cas_locations() - - if len(cas_location) > 0: - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["CAS_EVERY_X_MINUTES"])): - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.CAS) - flight.points = [] - flight.scheduled_in = offset + i * random.randint(self.doctrine["CAS_EVERY_X_MINUTES"] - 5, self.doctrine["CAS_EVERY_X_MINUTES"] + 5) - location = random.choice(cas_location) - - self.generate_cas(flight, flight.from_cp, location) - - self.cas_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - def commision_sead(self): - """ - Pick some aircraft to assign them to SEAD tasks - """ - - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in SEAD_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - - if len(self.potential_sead_targets) > 0: - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["SEAD_EVERY_X_MINUTES"])): - - if len(self.potential_sead_targets) <= 0: - break - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, random.choice([FlightType.SEAD, FlightType.DEAD])) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, self.doctrine["SEAD_EVERY_X_MINUTES"] + 5) - - location = self.potential_sead_targets[0][0] - self.potential_sead_targets.pop(0) - - self.generate_sead(flight, location, []) - - self.sead_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - - def commision_strike(self): - """ - Pick some aircraft to assign them to STRIKE tasks - """ - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in STRIKE_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - - if len(self.potential_strike_targets) > 0: - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["STRIKE_EVERY_X_MINUTES"])): - - if len(self.potential_strike_targets) <= 0: - break - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - if unit in DRONES: - count = 1 - else: - count = 2 - - inventory[unit] = inventory[unit] - count - flight = Flight(unit, count, self.from_cp, FlightType.STRIKE) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5) - - location = self.potential_strike_targets[0][0] - self.potential_strike_targets.pop(0) - - self.generate_strike(flight, location) - - self.strike_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - def _get_cas_locations(self): - return self._get_cas_locations_for_cp(self.from_cp) - - def _get_cas_locations_for_cp(self, for_cp): - cas_locations = [] - for cp in for_cp.connected_points: - if cp.captured != for_cp.captured: - cas_locations.append(cp) - return cas_locations - - def compute_strike_targets(self): - """ - @return a list of potential strike targets in range - """ - - # target, distance - self.potential_strike_targets = [] - - for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance > 2*self.doctrine["STRIKE_MAX_RANGE"]: - # Then it's unlikely any child ground object is in range - return - - added_group = [] - for g in cp.ground_objects: - if g.group_id in added_group or g.is_dead: continue - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance < self.doctrine["SEAD_MAX_RANGE"]: - self.potential_strike_targets.append((g, distance)) - added_group.append(g) - - self.potential_strike_targets.sort(key=operator.itemgetter(1)) - - def compute_sead_targets(self): - """ - @return a list of potential sead targets in range - """ - - # target, distance - self.potential_sead_targets = [] - - for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - # Then it's unlikely any ground object is range - if distance > 2*self.doctrine["SEAD_MAX_RANGE"]: - return - - for g in cp.ground_objects: - - if g.dcs_identifier == "AA": - - # Check that there is at least one unit with a radar in the ground objects unit groups - number_of_units = sum([len([r for r in group.units if db.unit_type_from_name(r.type) in UNITS_WITH_RADAR]) for group in g.groups]) - if number_of_units <= 0: - continue - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance < self.doctrine["SEAD_MAX_RANGE"]: - self.potential_sead_targets.append((g, distance)) - - self.potential_sead_targets.sort(key=operator.itemgetter(1)) - - def __repr__(self): - return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\ - + "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40 - - def get_available_aircraft(self): - base_aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()}) - for f in self.flights: - if f.unit_type in base_aircraft_inventory.keys(): - base_aircraft_inventory[f.unit_type] = base_aircraft_inventory[f.unit_type] - f.count - if base_aircraft_inventory[f.unit_type] <= 0: - del base_aircraft_inventory[f.unit_type] - return base_aircraft_inventory - - - def generate_strike(self, flight, location): - - flight.flight_type = FlightType.STRIKE - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - heading = flight.from_cp.position.heading_between_point(location.position) - ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 - - ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_STRIKE, - ingress_pos.x, - ingress_pos.y, - self.doctrine["INGRESS_ALT"] + package = builder.build() + flight_plan_builder = FlightPlanBuilder(self.game, package, + self.is_player) + for flight in package.flights: + flight_plan_builder.populate_flight_plan(flight) + self.ato.add_package(package) + + def stagger_missions(self) -> None: + def start_time_generator(count: int, earliest: int, latest: int, + margin: int) -> Iterator[int]: + interval = latest // count + for time in range(earliest, latest, interval): + error = random.randint(-margin, margin) + yield max(0, time + error) + + dca_types = ( + FlightType.BARCAP, + FlightType.CAP, + FlightType.INTERCEPTION, ) - ingress_point.pretty_name = "INGRESS on " + location.obj_name - ingress_point.description = "INGRESS on " + location.obj_name - ingress_point.name = "INGRESS" - flight.points.append(ingress_point) - if len(location.groups) > 0 and location.dcs_identifier == "AA": - for g in location.groups: - for j, u in enumerate(g.units): - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - u.position.x, - u.position.y, - 0 - ) - point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) - point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) - point.name = location.obj_name + "#" + str(j) - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) - else: - if hasattr(location, "obj_name"): - buildings = self.game.theater.find_ground_objects_by_obj_name(location.obj_name) - print(buildings) - for building in buildings: - print("BUILDING " + str(building.is_dead) + " " + str(building.dcs_identifier)) - if building.is_dead: - continue + non_dca_packages = [p for p in self.ato.packages if + p.primary_task not in dca_types] - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - building.position.x, - building.position.y, - 0 - ) - point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" - point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" - point.name = building.obj_name - point.only_for_player = True - ingress_point.targets.append(building) - flight.points.append(point) - else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 - ) - point.description = "STRIKE on " + location.obj_name - point.pretty_name = "STRIKE on " + location.obj_name - point.name = location.obj_name - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) - - egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine["EGRESS_ALT"] + start_time = start_time_generator( + count=len(non_dca_packages), + earliest=5, + latest=90, + margin=5 ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.obj_name - egress_point.description = "EGRESS from " + location.obj_name - flight.points.append(egress_point) + for package in non_dca_packages: + package.delay = next(start_time) + for flight in package.flights: + flight.scheduled_in = package.delay - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) + def message(self, title, text) -> None: + """Emits a planning message to the player. - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - def generate_barcap(self, flight, for_cp): + If the mission planner belongs to the players coalition, this emits a + message to the info panel. """ - Generate a barcap flight at a given location - :param flight: Flight to setup - :param for_cp: CP to protect - """ - flight.flight_type = FlightType.BARCAP if for_cp.is_carrier else FlightType.CAP - patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1]) - - if len(for_cp.ground_objects) > 0: - loc = random.choice(for_cp.ground_objects) - hdg = for_cp.position.heading_between_point(loc.position) - radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1]) - orbit0p = loc.position.point_from_heading(hdg - 90, radius) - orbit1p = loc.position.point_from_heading(hdg + 90, radius) - else: - loc = for_cp.position.point_from_heading(random.randint(0, 360), random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0], self.doctrine["CAP_DISTANCE_FROM_CP"][1])) - hdg = for_cp.position.heading_between_point(loc) - radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1]) - orbit0p = loc.point_from_heading(hdg - 90, radius) - orbit1p = loc.point_from_heading(hdg + 90, radius) - - # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - orbit0.targets.append(for_cp) - obj_added = [] - for ground_object in for_cp.ground_objects: - if ground_object.obj_name not in obj_added and not ground_object.airbase_group: - orbit0.targets.append(ground_object) - obj_added.append(ground_object.obj_name) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - - def generate_frontline_cap(self, flight, ally_cp, enemy_cp): - """ - Generate a cap flight for the frontline between ally_cp and enemy cp in order to ensure air superiority and - protect friendly CAP airbase - :param flight: Flight to setup - :param ally_cp: CP to protect - :param enemy_cp: Enemy connected cp - """ - flight.flight_type = FlightType.CAP - patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1]) - - # Find targets waypoints - ingress, heading, distance = Conflict.frontline_vector(ally_cp, enemy_cp, self.game.theater) - center = ingress.point_from_heading(heading, distance / 2) - orbit_center = center.point_from_heading(heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))) - - combat_width = distance / 2 - if combat_width > 500000: - combat_width = 500000 - if combat_width < 35000: - combat_width = 35000 - - radius = combat_width*1.25 - orbit0p = orbit_center.point_from_heading(heading, radius) - orbit1p = orbit_center.point_from_heading(heading + 180, radius) - - # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - # Note : Targets of a PATROL TRACK waypoints are the points to be defended - orbit0.targets.append(flight.from_cp) - orbit0.targets.append(center) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - - def generate_sead(self, flight, location, custom_targets = []): - """ - Generate a sead flight at a given location - :param flight: Flight to setup - :param location: Location of the SEAD target - :param custom_targets: Custom targets if any - """ - flight.points = [] - flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) - - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - heading = flight.from_cp.position.heading_between_point(location.position) - ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 - - ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_SEAD, - ingress_pos.x, - ingress_pos.y, - self.doctrine["INGRESS_ALT"] - ) - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS on " + location.obj_name - ingress_point.description = "INGRESS on " + location.obj_name - flight.points.append(ingress_point) - - if len(custom_targets) > 0: - for target in custom_targets: - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - target.position.x, - target.position.y, - 0 - ) - point.alt_type = "RADIO" - if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + target.type - point.pretty_name = "DEAD on " + location.obj_name - point.only_for_player = True - else: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - flight.points.append(point) - ingress_point.targets.append(location) - ingress_point.targetGroup = location - else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 + if self.is_player: + self.game.informations.append( + Information(title, text, self.game.turn) ) - point.alt_type = "RADIO" - if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + location.obj_name - point.pretty_name = "DEAD on " + location.obj_name - point.only_for_player = True - else: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - ingress_point.targets.append(location) - ingress_point.targetGroup = location - flight.points.append(point) - - egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine["EGRESS_ALT"] - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.obj_name - egress_point.description = "EGRESS from " + location.obj_name - flight.points.append(egress_point) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - - def generate_cas(self, flight, from_cp, location): - """ - Generate a CAS flight at a given location - :param flight: Flight to setup - :param location: Location of the CAS targets - """ - is_helo = hasattr(flight.unit_type, "helicopter") and flight.unit_type.helicopter - cap_alt = 1000 - flight.points = [] - flight.flight_type = FlightType.CAS - - ingress, heading, distance = Conflict.frontline_vector(from_cp, location, self.game.theater) - center = ingress.point_from_heading(heading, distance / 2) - egress = ingress.point_from_heading(heading, distance) - - ascend = self.generate_ascend_point(flight.from_cp) - if is_helo: - cap_alt = 500 - ascend.alt = 500 - flight.points.append(ascend) - - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_CAS, - ingress.x, - ingress.y, - cap_alt - ) - ingress_point.alt_type = "RADIO" - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS" - ingress_point.description = "Ingress into CAS area" - flight.points.append(ingress_point) - - center_point = FlightWaypoint( - FlightWaypointType.CAS, - center.x, - center.y, - cap_alt - ) - center_point.alt_type = "RADIO" - center_point.description = "Provide CAS" - center_point.name = "CAS" - center_point.pretty_name = "CAS" - flight.points.append(center_point) - - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress.x, - egress.y, - cap_alt - ) - egress_point.alt_type = "RADIO" - egress_point.description = "Egress from CAS area" - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS" - flight.points.append(egress_point) - - descend = self.generate_descend_point(flight.from_cp) - if is_helo: - descend.alt = 300 - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - def generate_ascend_point(self, from_cp): - """ - Generate ascend point - :param from_cp: Airport you're taking off from - :return: - """ - ascend_heading = from_cp.heading - pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000) - ascend = FlightWaypoint( - FlightWaypointType.ASCEND_POINT, - pos_ascend.x, - pos_ascend.y, - self.doctrine["PATTERN_ALTITUDE"] - ) - ascend.name = "ASCEND" - ascend.alt_type = "RADIO" - ascend.description = "Ascend" - ascend.pretty_name = "Ascend" - return ascend - - def generate_descend_point(self, from_cp): - """ - Generate approach/descend point - :param from_cp: Airport you're landing at - :return: - """ - ascend_heading = from_cp.heading - descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000) - descend = FlightWaypoint( - FlightWaypointType.DESCENT_POINT, - descend.x, - descend.y, - self.doctrine["PATTERN_ALTITUDE"] - ) - descend.name = "DESCEND" - descend.alt_type = "RADIO" - descend.description = "Descend to pattern alt" - descend.pretty_name = "Descend to pattern alt" - return descend - - def generate_rtb_waypoint(self, from_cp): - """ - Generate RTB landing point - :param from_cp: Airport you're landing at - :return: - """ - rtb = from_cp.position - rtb = FlightWaypoint( - FlightWaypointType.LANDING_POINT, - rtb.x, - rtb.y, - 0 - ) - rtb.name = "LANDING" - rtb.alt_type = "RADIO" - rtb.description = "RTB" - rtb.pretty_name = "RTB" - return rtb + else: + logging.info(f"{title}: {text}") diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index e1393a1a..715a7f66 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -1,5 +1,78 @@ -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + AH_1W, + AH_64A, + AH_64D, + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, + OH_58D, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + A_20G, + B_17G, + Bf_109K_4, + C_101CC, + FA_18C_hornet, + FW_190A8, + FW_190D9, + F_14B, + F_15C, + F_15E, + F_16A, + F_16C_50, + F_4E, + F_5E_3, + F_86F_Sabre, + F_A_18C, + JF_17, + J_11A, + Ju_88A4, + L_39ZA, + MQ_9_Reaper, + M_2000C, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_27K, + MiG_29A, + MiG_29G, + MiG_29K, + MiG_29S, + MiG_31, + Mirage_2000_5, + P_47D_30, + P_47D_30bl1, + P_47D_40, + P_51D, + P_51D_30_NA, + RQ_1A_Predator, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_27, + Su_30, + Su_33, + Su_34, + Tornado_GR4, + Tornado_IDS, + WingLoong_I, +) # Interceptor are the aircraft prioritized for interception tasks # If none is available, the AI will use regular CAP-capable aircraft instead @@ -7,6 +80,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M +# 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. + INTERCEPT_CAPABLE = [ MiG_21Bis, MiG_25PD, @@ -77,6 +154,42 @@ CAP_CAPABLE = [ Rafale_M, ] +CAP_PREFERRED = [ + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29A, + MiG_29G, + MiG_29S, + MiG_31, + + Su_27, + J_11A, + Su_30, + Su_33, + + M_2000C, + Mirage_2000_5, + + F_86F_Sabre, + F_14B, + F_15C, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, + + Rafale_M, +] + # Used for CAS (Close air support) and BAI (Battlefield Interdiction) CAS_CAPABLE = [ @@ -155,6 +268,59 @@ CAS_CAPABLE = [ RQ_1A_Predator ] +CAS_PREFERRED = [ + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_34, + + JF_17, + + A_10A, + A_10C, + A_10C_2, + AV8BNA, + + F_15E, + + Tornado_GR4, + + C_101CC, + MB_339PAN, + L_39ZA, + AJS37, + + SA342M, + SA342L, + OH_58D, + + AH_64A, + AH_64D, + AH_1W, + + UH_1H, + + Mi_8MT, + Mi_28N, + Mi_24V, + Ka_50, + + P_47D_30, + P_47D_30bl1, + P_47D_40, + A_20G, + + A_4E_C, + Rafale_A_S, + + WingLoong_I, + MQ_9_Reaper, + RQ_1A_Predator +] + # Aircraft used for SEAD / DEAD tasks SEAD_CAPABLE = [ F_4E, @@ -179,6 +345,12 @@ SEAD_CAPABLE = [ Rafale_A_S ] +SEAD_PREFERRED = [ + F_4E, + Su_25T, + Tornado_IDS, +] + # Aircraft used for Strike mission STRIKE_CAPABLE = [ MiG_15bis, @@ -236,6 +408,15 @@ STRIKE_CAPABLE = [ ] +STRIKE_PREFERRED = [ + AJS37, + F_15E, + Tornado_GR4, + + A_20G, + B_17G, +] + ANTISHIP_CAPABLE = [ Su_24M, Su_17M4, diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py new file mode 100644 index 00000000..a6045dde --- /dev/null +++ b/gen/flights/closestairfields.py @@ -0,0 +1,51 @@ +"""Objective adjacency lists.""" +from typing import Dict, Iterator, List, Optional + +from theater import ConflictTheater, ControlPoint, MissionTarget + + +class ClosestAirfields: + """Precalculates which control points are closes to the given target.""" + + def __init__(self, target: MissionTarget, + all_control_points: List[ControlPoint]) -> None: + self.target = target + self.closest_airfields: List[ControlPoint] = sorted( + all_control_points, key=lambda c: self.target.distance_to(c) + ) + + def airfields_within(self, meters: int) -> Iterator[ControlPoint]: + """Iterates over all airfields within the given range of the target. + + Note that this iterates over *all* airfields, not just friendly + airfields. + """ + for cp in self.closest_airfields: + if cp.distance_to(self.target) < meters: + yield cp + else: + break + + +class ObjectiveDistanceCache: + theater: Optional[ConflictTheater] = None + closest_airfields: Dict[str, ClosestAirfields] = {} + + @classmethod + def set_theater(cls, theater: ConflictTheater) -> None: + if cls.theater is not None: + cls.closest_airfields = {} + cls.theater = theater + + @classmethod + def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields: + if cls.theater is None: + raise RuntimeError( + "Call ObjectiveDistanceCache.set_theater before using" + ) + + if location.name not in cls.closest_airfields: + cls.closest_airfields[location.name] = ClosestAirfields( + location, cls.theater.controlpoints + ) + return cls.closest_airfields[location.name] diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 0c5c7956..90b27ccf 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,10 @@ from enum import Enum -from typing import List +from typing import Dict, Optional from game import db from dcs.unittype import UnitType from dcs.point import MovingPoint, PointAction -from theater.controlpoint import ControlPoint +from theater.controlpoint import ControlPoint, MissionTarget class FlightType(Enum): @@ -47,6 +47,8 @@ class FlightWaypointType(Enum): TARGET_GROUP_LOC = 13 # A target group approximate location TARGET_SHIP = 14 # A target ship known location CUSTOM = 15 # User waypoint (no specific behaviour) + JOIN = 16 + SPLIT = 17 class PredefinedWaypointCategory(Enum): @@ -71,8 +73,8 @@ class FlightWaypoint: self.alt_type = "BARO" self.name = "" self.description = "" - self.targets = [] - self.targetGroup = None + self.targets: List[MissionTarget] = [] + self.targetGroup: Optional[MissionTarget] = None self.obj_name = "" self.pretty_name = "" self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED @@ -108,15 +110,9 @@ class FlightWaypoint: class Flight: - unit_type: UnitType = None - from_cp = None - points: List[FlightWaypoint] = [] - flight_type: FlightType = None count: int = 0 client_count: int = 0 - targets = [] use_custom_loadout = False - loadout = {} preset_loadout_name = "" start_type = "Runway" group = False # Contains DCS Mission group data after mission has been generated @@ -124,14 +120,14 @@ class Flight: # How long before this flight should take off scheduled_in = 0 - def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): + def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType): self.unit_type = unit_type self.count = count self.from_cp = from_cp self.flight_type = flight_type - self.points = [] - self.targets = [] - self.loadout = {} + self.points: List[FlightWaypoint] = [] + self.targets: List[MissionTarget] = [] + self.loadout: Dict[str, str] = {} self.start_type = "Runway" def __repr__(self): @@ -141,10 +137,10 @@ class Flight: # Test if __name__ == '__main__': - from pydcs.dcs.planes import A_10C + from dcs.planes import A_10C from theater import ControlPoint, Point, List - from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0) + from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0) f = Flight(A_10C(), 4, from_cp, FlightType.CAS) f.scheduled_in = 50 print(f) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py new file mode 100644 index 00000000..5e5fefe2 --- /dev/null +++ b/gen/flights/flightplan.py @@ -0,0 +1,442 @@ +"""Flight plan generation. + +Flights are first planned generically by either the player or by the +MissionPlanner. Those only plan basic information like the objective, aircraft +type, and the size of the flight. The FlightPlanBuilder is responsible for +generating the waypoints for the mission. +""" +from __future__ import annotations + +import logging +import random +from typing import List, Optional, TYPE_CHECKING + +from dcs.mapping import Point +from dcs.unit import Unit + +from game.data.doctrine import Doctrine, MODERN_DOCTRINE +from game.utils import nm_to_meter +from gen.ato import Package +from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from .closestairfields import ObjectiveDistanceCache +from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType +from .waypointbuilder import WaypointBuilder +from ..conflictgen import Conflict + +if TYPE_CHECKING: + from game import Game + + +class InvalidObjectiveLocation(RuntimeError): + """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." + ) + + +class FlightPlanBuilder: + """Generates flight plans for flights.""" + + def __init__(self, game: Game, package: Package, is_player: bool) -> None: + self.game = game + self.package = package + self.is_player = is_player + if is_player: + faction = self.game.player_faction + else: + faction = self.game.enemy_faction + self.doctrine: Doctrine = faction.get("doctrine", MODERN_DOCTRINE) + + def populate_flight_plan( + self, flight: Flight, + # TODO: Custom targets should be an attribute of the flight. + custom_targets: Optional[List[Unit]] = None) -> None: + """Creates a default flight plan for the given mission.""" + if flight not in self.package.flights: + raise RuntimeError("Flight must be a part of the package") + self.generate_missing_package_waypoints() + + # TODO: Flesh out mission types. + try: + task = flight.flight_type + if task == FlightType.ANTISHIP: + logging.error( + "Anti-ship flight plan generation not implemented" + ) + elif task == FlightType.BAI: + logging.error("BAI flight plan generation not implemented") + elif task == FlightType.BARCAP: + self.generate_barcap(flight) + elif task == FlightType.CAP: + self.generate_barcap(flight) + elif task == FlightType.CAS: + self.generate_cas(flight) + elif task == FlightType.DEAD: + self.generate_sead(flight, custom_targets) + elif task == FlightType.ELINT: + logging.error("ELINT flight plan generation not implemented") + elif task == FlightType.ESCORT: + self.generate_escort(flight) + elif task == FlightType.EVAC: + logging.error("Evac flight plan generation not implemented") + elif task == FlightType.EWAR: + logging.error("EWar flight plan generation not implemented") + elif task == FlightType.INTERCEPTION: + logging.error( + "Intercept flight plan generation not implemented" + ) + elif task == FlightType.LOGISTICS: + logging.error( + "Logistics flight plan generation not implemented" + ) + elif task == FlightType.RECON: + logging.error("Recon flight plan generation not implemented") + elif task == FlightType.SEAD: + self.generate_sead(flight, custom_targets) + elif task == FlightType.STRIKE: + self.generate_strike(flight) + elif task == FlightType.TARCAP: + self.generate_frontline_cap(flight) + elif task == FlightType.TROOP_TRANSPORT: + logging.error( + "Troop transport flight plan generation not implemented" + ) + except InvalidObjectiveLocation as ex: + logging.error(f"Could not create flight plan: {ex}") + + def generate_missing_package_waypoints(self) -> None: + if self.package.ingress_point is None: + self.package.ingress_point = self._ingress_point() + if self.package.egress_point is None: + self.package.egress_point = self._egress_point() + if self.package.join_point is None: + self.package.join_point = self._join_point() + if self.package.split_point is None: + self.package.split_point = self._split_point() + + def generate_strike(self, flight: Flight) -> None: + """Generates a strike flight plan. + + Args: + flight: The flight to generate the flight plan for. + """ + location = self.package.target + + # TODO: Support airfield strikes. + if not isinstance(location, TheaterGroundObject): + raise InvalidObjectiveLocation(flight.flight_type, location) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.join(self.package.join_point) + builder.ingress_strike(self.package.ingress_point, location) + + if len(location.groups) > 0 and location.dcs_identifier == "AA": + # TODO: Replace with DEAD? + # Strike missions on SEAD targets target units. + for g in location.groups: + for j, u in enumerate(g.units): + builder.strike_point(u, f"{u.type} #{j}", location) + else: + # TODO: Does this actually happen? + # ConflictTheater is built with the belief that multiple ground + # objects have the same name. If that's the case, + # TheaterGroundObject needs some refactoring because it behaves very + # differently for SAM sites than it does for strike targets. + buildings = self.game.theater.find_ground_objects_by_obj_name( + location.obj_name + ) + for building in buildings: + if building.is_dead: + continue + + builder.strike_point( + building, + f"{building.obj_name} {building.category}", + location + ) + + builder.egress(self.package.egress_point, location) + builder.split(self.package.split_point) + builder.rtb(flight.from_cp) + + flight.points = builder.build() + + def generate_barcap(self, flight: Flight) -> None: + """Generate a BARCAP flight at a given location. + + Args: + flight: The flight to generate the flight plan for. + """ + location = self.package.target + + if isinstance(location, FrontLine): + raise InvalidObjectiveLocation(flight.flight_type, location) + + patrol_alt = random.randint( + self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude + ) + + closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) + for airfield in closest_cache.closest_airfields: + if airfield.captured != self.is_player: + closest_airfield = airfield + break + else: + logging.error("Could not find any enemy airfields") + return + + heading = location.position.heading_between_point( + closest_airfield.position + ) + + end = location.position.point_from_heading( + heading, + random.randint(self.doctrine.cap_min_distance_from_cp, + self.doctrine.cap_max_distance_from_cp) + ) + diameter = random.randint( + self.doctrine.cap_min_track_length, + self.doctrine.cap_max_track_length + ) + start = end.point_from_heading(heading - 180, diameter) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.race_track(start, end, patrol_alt) + builder.rtb(flight.from_cp) + flight.points = builder.build() + + def generate_frontline_cap(self, flight: Flight) -> None: + """Generate a CAP flight plan for the given front line. + + Args: + flight: The flight to generate the flight plan for. + """ + location = self.package.target + + if not isinstance(location, FrontLine): + raise InvalidObjectiveLocation(flight.flight_type, location) + + ally_cp, enemy_cp = location.control_points + patrol_alt = random.randint(self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude) + + # Find targets waypoints + ingress, heading, distance = Conflict.frontline_vector( + ally_cp, enemy_cp, self.game.theater + ) + center = ingress.point_from_heading(heading, distance / 2) + orbit_center = center.point_from_heading( + heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15)) + ) + + combat_width = distance / 2 + if combat_width > 500000: + combat_width = 500000 + if combat_width < 35000: + combat_width = 35000 + + radius = combat_width*1.25 + orbit0p = orbit_center.point_from_heading(heading, radius) + orbit1p = orbit_center.point_from_heading(heading + 180, radius) + + # Create points + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.race_track(orbit0p, orbit1p, patrol_alt) + builder.rtb(flight.from_cp) + flight.points = builder.build() + + def generate_sead(self, flight: Flight, + custom_targets: Optional[List[Unit]]) -> None: + """Generate a SEAD/DEAD flight at a given location. + + Args: + flight: The flight to generate the flight plan for. + custom_targets: Specific radar equipped units selected by the user. + """ + location = self.package.target + + if not isinstance(location, TheaterGroundObject): + raise InvalidObjectiveLocation(flight.flight_type, location) + + if custom_targets is None: + custom_targets = [] + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.join(self.package.join_point) + builder.ingress_sead(self.package.ingress_point, location) + + # TODO: Unify these. + # There doesn't seem to be any reason to treat the UI fragged missions + # different from the automatic missions. + if custom_targets: + for target in custom_targets: + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) + point.alt_type = "RADIO" + if flight.flight_type == FlightType.DEAD: + builder.dead_point(target, location.name, location) + else: + builder.sead_point(target, location.name, location) + else: + if flight.flight_type == FlightType.DEAD: + builder.dead_area(location) + else: + builder.sead_area(location) + + builder.egress(self.package.egress_point, location) + builder.split(self.package.split_point) + builder.rtb(flight.from_cp) + + flight.points = builder.build() + + def generate_escort(self, flight: Flight) -> None: + # TODO: Decide common waypoints for the package ahead of time. + # Packages should determine some common points like push, ingress, + # egress, and split points ahead of time so they can be shared by all + # flights. + + patrol_alt = random.randint( + self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude + ) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.join(self.package.join_point) + builder.race_track( + self.package.ingress_point, + self.package.egress_point, + patrol_alt + ) + builder.split(self.package.split_point) + builder.rtb(flight.from_cp) + + flight.points = builder.build() + + def generate_cas(self, flight: Flight) -> None: + """Generate a CAS flight plan for the given target. + + 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) + + is_helo = getattr(flight.unit_type, "helicopter", False) + cap_alt = 500 if is_helo else 1000 + + ingress, heading, distance = Conflict.frontline_vector( + location.control_points[0], location.control_points[1], + self.game.theater + ) + center = ingress.point_from_heading(heading, distance / 2) + egress = ingress.point_from_heading(heading, distance) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp, is_helo) + builder.join(self.package.join_point) + builder.ingress_cas(ingress, location) + builder.cas(center, cap_alt) + builder.egress(egress, location) + builder.split(self.package.split_point) + builder.rtb(flight.from_cp, is_helo) + + flight.points = builder.build() + + # TODO: Make a model for the waypoint builder and use that in the UI. + def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint: + """Generate ascend point. + + Args: + departure: Departure airfield or carrier. + """ + builder = WaypointBuilder(self.doctrine) + builder.ascent(departure) + return builder.build()[0] + + def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: + """Generate approach/descend point. + + Args: + arrival: Arrival airfield or carrier. + """ + builder = WaypointBuilder(self.doctrine) + builder.descent(arrival) + return builder.build()[0] + + def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint: + """Generate RTB landing point. + + Args: + arrival: Arrival airfield or carrier. + """ + builder = WaypointBuilder(self.doctrine) + builder.land(arrival) + return builder.build()[0] + + def _join_point(self) -> Point: + ingress_point = self.package.ingress_point + assert ingress_point is not None + heading = self._heading_to_package_airfield(ingress_point) + return ingress_point.point_from_heading(heading, + -self.doctrine.join_distance) + + def _split_point(self) -> Point: + egress_point = self.package.egress_point + assert egress_point is not None + heading = self._heading_to_package_airfield(egress_point) + return egress_point.point_from_heading(heading, + -self.doctrine.split_distance) + + def _ingress_point(self) -> Point: + heading = self._target_heading_to_package_airfield() + return self.package.target.position.point_from_heading( + heading - 180 + 25, self.doctrine.ingress_egress_distance + ) + + def _egress_point(self) -> Point: + heading = self._target_heading_to_package_airfield() + return self.package.target.position.point_from_heading( + heading - 180 - 25, self.doctrine.ingress_egress_distance + ) + + def _target_heading_to_package_airfield(self) -> int: + return self._heading_to_package_airfield(self.package.target.position) + + def _heading_to_package_airfield(self, point: Point) -> int: + return self.package_airfield().position.heading_between_point(point) + + # TODO: Set ingress/egress/join/split points in the Package. + def package_airfield(self) -> ControlPoint: + # We'll always have a package, but if this is being planned via the UI + # it could be the first flight in the package. + if not self.package.flights: + raise RuntimeError( + "Cannot determine source airfield for package with no flights" + ) + + # The package airfield is either the flight's airfield (when there is no + # package) or the closest airfield to the objective that is the + # departure airfield for some flight in the package. + cache = ObjectiveDistanceCache.get_closest_airfields( + self.package.target + ) + for airfield in cache.closest_airfields: + for flight in self.package.flights: + if flight.from_cp == airfield: + return airfield + raise RuntimeError( + "Could not find any airfield assigned to this package" + ) diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py new file mode 100644 index 00000000..6fc3fd85 --- /dev/null +++ b/gen/flights/waypointbuilder.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +from typing import List, Optional, Union + +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, MissionTarget, TheaterGroundObject +from .flight import FlightWaypoint, FlightWaypointType + + +class WaypointBuilder: + def __init__(self, doctrine: Doctrine) -> None: + self.doctrine = doctrine + self.waypoints: List[FlightWaypoint] = [] + self.ingress_point: Optional[FlightWaypoint] = None + + def build(self) -> List[FlightWaypoint]: + return self.waypoints + + def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None: + """Create ascent waypoint for the given departure airfield or carrier. + + Args: + departure: Departure airfield or carrier. + """ + # TODO: Pick runway based on wind direction. + heading = departure.heading + position = departure.position.point_from_heading( + heading, nm_to_meter(5) + ) + waypoint = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + position.x, + position.y, + 500 if is_helo else self.doctrine.pattern_altitude + ) + waypoint.name = "ASCEND" + waypoint.alt_type = "RADIO" + waypoint.description = "Ascend" + waypoint.pretty_name = "Ascend" + self.waypoints.append(waypoint) + + def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None: + """Create descent waypoint for the given arrival airfield or carrier. + + Args: + arrival: Arrival airfield or carrier. + """ + # TODO: Pick runway based on wind direction. + # ControlPoint.heading is the departure heading. + heading = (arrival.heading + 180) % 360 + position = arrival.position.point_from_heading( + heading, nm_to_meter(5) + ) + waypoint = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + position.x, + position.y, + 300 if is_helo else self.doctrine.pattern_altitude + ) + waypoint.name = "DESCEND" + waypoint.alt_type = "RADIO" + waypoint.description = "Descend to pattern altitude" + waypoint.pretty_name = "Ascend" + self.waypoints.append(waypoint) + + def land(self, arrival: ControlPoint) -> None: + """Create descent waypoint for the given arrival airfield or carrier. + + Args: + arrival: Arrival airfield or carrier. + """ + position = arrival.position + waypoint = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + position.x, + position.y, + 0 + ) + waypoint.name = "LANDING" + waypoint.alt_type = "RADIO" + waypoint.description = "Land" + waypoint.pretty_name = "Land" + self.waypoints.append(waypoint) + + def join(self, position: Point) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.JOIN, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "Join" + waypoint.description = "Rendezvous with package" + waypoint.name = "JOIN" + self.waypoints.append(waypoint) + + def split(self, position: Point) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.SPLIT, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "Split" + waypoint.description = "Depart from package" + waypoint.name = "SPLIT" + self.waypoints.append(waypoint) + + def ingress_cas(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_CAS, position, objective) + + def ingress_sead(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective) + + def ingress_strike(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective) + + def _ingress(self, ingress_type: FlightWaypointType, position: Point, + objective: MissionTarget) -> None: + if self.ingress_point is not None: + raise RuntimeError("A flight plan can have only one ingress point.") + + waypoint = FlightWaypoint( + ingress_type, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "INGRESS on " + objective.name + waypoint.description = "INGRESS on " + objective.name + waypoint.name = "INGRESS" + self.waypoints.append(waypoint) + self.ingress_point = waypoint + + def egress(self, position: Point, target: MissionTarget) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.EGRESS, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "EGRESS from " + target.name + waypoint.description = "EGRESS from " + target.name + waypoint.name = "EGRESS" + self.waypoints.append(waypoint) + + def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + # TODO: Seems fishy. + if self.ingress_point is not None: + self.ingress_point.targetGroup = location + + def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + # TODO: Seems fishy. + if self.ingress_point is not None: + self.ingress_point.targetGroup = location + + def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + + def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str, + description: str, location: MissionTarget) -> None: + if self.ingress_point is None: + raise RuntimeError( + "An ingress point must be added before target points." + ) + + waypoint = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) + waypoint.description = description + waypoint.pretty_name = description + waypoint.name = name + waypoint.only_for_player = True + self.waypoints.append(waypoint) + # TODO: This seems wrong, but it's what was there before. + self.ingress_point.targets.append(location) + + def sead_area(self, target: MissionTarget) -> None: + self._target_area(f"SEAD on {target.name}", target) + # TODO: Seems fishy. + if self.ingress_point is not None: + self.ingress_point.targetGroup = target + + def dead_area(self, target: MissionTarget) -> None: + self._target_area(f"DEAD on {target.name}", target) + # TODO: Seems fishy. + if self.ingress_point is not None: + self.ingress_point.targetGroup = target + + def _target_area(self, name: str, location: MissionTarget) -> None: + if self.ingress_point is None: + raise RuntimeError( + "An ingress point must be added before target points." + ) + + waypoint = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) + waypoint.description = name + waypoint.pretty_name = name + waypoint.name = name + waypoint.only_for_player = True + self.waypoints.append(waypoint) + # TODO: This seems wrong, but it's what was there before. + self.ingress_point.targets.append(location) + + def cas(self, position: Point, altitude: int) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.CAS, + position.x, + position.y, + altitude + ) + waypoint.alt_type = "RADIO" + waypoint.description = "Provide CAS" + waypoint.name = "CAS" + waypoint.pretty_name = "CAS" + self.waypoints.append(waypoint) + + def race_track_start(self, position: Point, altitude: int) -> None: + """Creates a racetrack start waypoint. + + Args: + position: Position of the waypoint. + altitude: Altitude of the racetrack in meters. + """ + waypoint = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + position.x, + position.y, + altitude + ) + waypoint.name = "RACETRACK START" + waypoint.description = "Orbit between this point and the next point" + waypoint.pretty_name = "Race-track start" + self.waypoints.append(waypoint) + + # TODO: Does this actually do anything? + # orbit0.targets.append(location) + # Note: Targets of PATROL TRACK waypoints are the points to be defended. + # orbit0.targets.append(flight.from_cp) + # orbit0.targets.append(center) + + def race_track_end(self, position: Point, altitude: int) -> None: + """Creates a racetrack end waypoint. + + Args: + position: Position of the waypoint. + altitude: Altitude of the racetrack in meters. + """ + waypoint = FlightWaypoint( + FlightWaypointType.PATROL, + position.x, + position.y, + altitude + ) + waypoint.name = "RACETRACK END" + waypoint.description = "Orbit between this point and the previous point" + waypoint.pretty_name = "Race-track end" + self.waypoints.append(waypoint) + + def race_track(self, start: Point, end: Point, altitude: int) -> None: + """Creates two waypoint for a racetrack orbit. + + Args: + start: The beginning racetrack waypoint. + end: The ending racetrack waypoint. + altitude: The racetrack altitude. + """ + self.race_track_start(start, altitude) + self.race_track_end(end, altitude) + + def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None: + """Creates descent ant landing waypoints for the given control point. + + Args: + arrival: Arrival airfield or carrier. + """ + self.descent(arrival, is_helo) + self.land(arrival) diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index 877c9831..bda87407 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -1,13 +1,13 @@ import random from enum import Enum +from typing import Dict, List -from dcs.vehicles import * - -from gen import Conflict -from gen.ground_forces.combat_stance import CombatStance -from theater import ControlPoint +from dcs.vehicles import Armor, Artillery, Infantry, Unarmed +from dcs.unittype import VehicleType import pydcs_extensions.frenchpack.frenchpack as frenchpack +from gen.ground_forces.combat_stance import CombatStance +from theater import ControlPoint TYPE_TANKS = [ Armor.MBT_T_55, @@ -207,8 +207,8 @@ GROUP_SIZES_BY_COMBAT_STANCE = { class CombatGroup: - def __init__(self, role:CombatGroupRole): - self.units = [] + def __init__(self, role: CombatGroupRole): + self.units: List[VehicleType] = [] self.role = role self.assigned_enemy_cp = None self.start_position = None @@ -222,33 +222,22 @@ class CombatGroup: class GroundPlanner: - cp = None - combat_groups_dict = {} - connected_enemy_cp = [] - - tank_groups = [] - apc_group = [] - ifv_group = [] - art_group = [] - shorad_groups = [] - logi_groups = [] - def __init__(self, cp:ControlPoint, game): self.cp = cp self.game = game self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured] - self.tank_groups = [] - self.apc_group = [] - self.ifv_group = [] - self.art_group = [] - self.atgm_group = [] - self.logi_groups = [] - self.shorad_groups = [] + self.tank_groups: List[CombatGroup] = [] + self.apc_group: List[CombatGroup] = [] + self.ifv_group: List[CombatGroup] = [] + self.art_group: List[CombatGroup] = [] + self.atgm_group: List[CombatGroup] = [] + self.logi_groups: List[CombatGroup] = [] + self.shorad_groups: List[CombatGroup] = [] - self.units_per_cp = {} + self.units_per_cp: Dict[int, List[CombatGroup]] = {} for cp in self.connected_enemy_cp: self.units_per_cp[cp.id] = [] - self.reserve = [] + self.reserve: List[CombatGroup] = [] def plan_groundwar(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index d1cd5378..2d1894e8 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,11 +1,23 @@ -from dcs.statics import * -from dcs.unit import Ship, Vehicle +import logging +import random +from typing import Dict, Iterator -from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS +from dcs import Mission +from dcs.statics import fortification_map, warehouse_map +from dcs.task import ( + ActivateBeaconCommand, + ActivateICLSCommand, + EPLRS, + OptAlarmState, +) +from dcs.unit import Ship, Vehicle +from dcs.unitgroup import StaticGroup + +from game import db +from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name from .airfields import RunwayData -from .conflictgen import * -from .naming import * +from .conflictgen import Conflict from .radios import RadioRegistry from .tacan import TacanBand, TacanRegistry @@ -26,7 +38,7 @@ class GroundObjectsGenerator: self.icls_alloc = iter(range(1, 21)) self.runways: Dict[str, RunwayData] = {} - def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]: + def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]: if self.conflict.is_vector: center = self.conflict.center heading = self.conflict.heading - 90 @@ -80,6 +92,10 @@ class GroundObjectsGenerator: vehicle.heading = u.heading vehicle.player_can_drive = True vg.add_unit(vehicle) + + if hasattr(utype, 'eplrs'): + if utype.eplrs: + vg.points[0].tasks.append(EPLRS(vg.id)) else: vg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 6843e395..e7a86bc5 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -82,6 +82,8 @@ class KneeboardPageWriter: def table(self, cells: List[List[str]], headers: Optional[List[str]] = None) -> None: + if headers is None: + headers = [] table = tabulate(cells, headers=headers, numalign="right") self.text(table, font=self.table_font) @@ -136,7 +138,7 @@ class FlightPlanBuilder: def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: self.rows.append([ - waypoint.number, + str(waypoint.number), waypoint.waypoint.pretty_name, str(int(units.meters_to_feet(waypoint.waypoint.alt))) ]) @@ -194,7 +196,7 @@ class BriefingPage(KneeboardPage): tankers.append([ tanker.callsign, tanker.variant, - tanker.tacan, + str(tanker.tacan), self.format_frequency(tanker.freq), ]) writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) @@ -225,12 +227,22 @@ class BriefingPage(KneeboardPage): atc = "" if runway.atc is not None: atc = self.format_frequency(runway.atc) + if runway.tacan is None: + tacan = "" + else: + tacan = str(runway.tacan) + if runway.ils is not None: + ils = str(runway.ils) + elif runway.icls is not None: + ils = str(runway.icls) + else: + ils = "" return [ row_title, runway.airfield_name, atc, - runway.tacan or "", - runway.ils or runway.icls or "", + tacan, + ils, runway.runway_name, ] diff --git a/gen/missiles/missiles_group_generator.py b/gen/missiles/missiles_group_generator.py index c63fcca9..4e2e9d73 100644 --- a/gen/missiles/missiles_group_generator.py +++ b/gen/missiles/missiles_group_generator.py @@ -8,13 +8,13 @@ MISSILES_MAP = { } -def generate_missile_group(game, ground_object, faction:str): +def generate_missile_group(game, ground_object, faction_name: str): """ This generate a ship group :return: Nothing, but put the group reference inside the ground object """ - faction = db.FACTIONS[faction] - if "missiles" in faction.keys(): + faction = db.FACTIONS[faction_name] + if "missiles" in faction: generators = faction["missiles"] if len(generators) > 0: gen = random.choice(generators) diff --git a/gen/triggergen.py b/gen/triggergen.py index fbd8062e..ba87bb3e 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -1,19 +1,12 @@ -import typing -import random -from datetime import datetime, timedelta, time - +from dcs.action import MarkToAll +from dcs.condition import TimeAfter from dcs.mission import Mission -from dcs.triggers import * -from dcs.condition import * -from dcs.action import * +from dcs.task import Option +from dcs.translation import String +from dcs.triggers import Event, TriggerOnce from dcs.unit import Skill -from dcs.point import MovingPoint, PointProperties -from dcs.action import * -from game import db -from theater import * -from gen.airsupportgen import AirSupportConflictGenerator -from gen import * +from .conflictgen import Conflict PUSH_TRIGGER_SIZE = 3000 PUSH_TRIGGER_ACTIVATION_AGL = 25 diff --git a/gen/visualgen.py b/gen/visualgen.py index c3be7cad..5bc315e5 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -1,18 +1,20 @@ -import typing +from __future__ import annotations + import random -from datetime import datetime, timedelta +from typing import TYPE_CHECKING +from dcs.mapping import Point from dcs.mission import Mission -from dcs.statics import * from dcs.unit import Static +from dcs.unittype import StaticType -from theater import * -from .conflictgen import * -#from game.game import Game -from game import db +if TYPE_CHECKING: + from game import Game + +from .conflictgen import Conflict, FRONTLINE_LENGTH -class MarkerSmoke(unittype.StaticType): +class MarkerSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -20,7 +22,7 @@ class MarkerSmoke(unittype.StaticType): rate = 0.1 -class Smoke(unittype.StaticType): +class Smoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -28,7 +30,7 @@ class Smoke(unittype.StaticType): rate = 1 -class BigSmoke(unittype.StaticType): +class BigSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -36,7 +38,7 @@ class BigSmoke(unittype.StaticType): rate = 1 -class MassiveSmoke(unittype.StaticType): +class MassiveSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -44,7 +46,7 @@ class MassiveSmoke(unittype.StaticType): rate = 1 -class Outpost(unittype.StaticType): +class Outpost(StaticType): id = "outpost" name = "outpost" category = "Fortifications" @@ -90,9 +92,7 @@ def turn_heading(heading, fac): class VisualGenerator: - game = None # type: Game - - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game: Game): self.mission = mission self.conflict = conflict self.game = game diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..045a50e6 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,12 @@ +[mypy] +namespace_packages = True + +[mypy-dcs.*] +follow_imports=silent +ignore_missing_imports = True + +[mypy-PIL.*] +ignore_missing_imports = True + +[mypy-winreg.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/pydcs b/pydcs index ceea62a8..c203e5a1 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit ceea62a8e0731c21b3e1a3e90682aa0affc168f1 +Subproject commit c203e5a1b8d5eb42d559dab074e668bf37fa5158 diff --git a/pydcs_extensions/frenchpack/frenchpack.py b/pydcs_extensions/frenchpack/frenchpack.py index 12d51827..2d5f5433 100644 --- a/pydcs_extensions/frenchpack/frenchpack.py +++ b/pydcs_extensions/frenchpack/frenchpack.py @@ -25,7 +25,7 @@ class ERC_90(unittype.VehicleType): detection_range = 0 threat_range = 4000 air_weapon_dist = 4000 - eprls = True + eplrs = True class VAB__50(unittype.VehicleType): @@ -34,7 +34,7 @@ class VAB__50(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class VAB_T20_13(unittype.VehicleType): @@ -43,7 +43,7 @@ class VAB_T20_13(unittype.VehicleType): detection_range = 0 threat_range = 2000 air_weapon_dist = 2000 - eprls = True + eplrs = True class VAB_MEPHISTO(unittype.VehicleType): @@ -52,7 +52,7 @@ class VAB_MEPHISTO(unittype.VehicleType): detection_range = 0 threat_range = 4000 air_weapon_dist = 4000 - eprls = True + eplrs = True class VBL__50(unittype.VehicleType): @@ -61,7 +61,7 @@ class VBL__50(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class VBL_AANF1(unittype.VehicleType): @@ -70,7 +70,7 @@ class VBL_AANF1(unittype.VehicleType): detection_range = 0 threat_range = 1000 air_weapon_dist = 1000 - eprls = True + eplrs = True class VBAE_CRAB(unittype.VehicleType): @@ -79,7 +79,7 @@ class VBAE_CRAB(unittype.VehicleType): detection_range = 0 threat_range = 3500 air_weapon_dist = 3500 - eprls = True + eplrs = True class VBAE_CRAB_MMP(unittype.VehicleType): @@ -88,7 +88,7 @@ class VBAE_CRAB_MMP(unittype.VehicleType): detection_range = 0 threat_range = 3500 air_weapon_dist = 3500 - eprls = True + eplrs = True class AMX_30B2(unittype.VehicleType): @@ -121,7 +121,7 @@ class DIM__TOYOTA_BLUE(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__TOYOTA_GREEN(unittype.VehicleType): @@ -130,7 +130,7 @@ class DIM__TOYOTA_GREEN(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__TOYOTA_DESERT(unittype.VehicleType): @@ -139,7 +139,7 @@ class DIM__TOYOTA_DESERT(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__KAMIKAZE(unittype.VehicleType): @@ -148,7 +148,7 @@ class DIM__KAMIKAZE(unittype.VehicleType): detection_range = 0 threat_range = 50 air_weapon_dist = 50 - eprls = True + eplrs = True ## FORTIFICATION @@ -187,7 +187,7 @@ class TRM_2000(unittype.VehicleType): detection_range = 3500 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class TRM_2000_Fuel(unittype.VehicleType): id = "TRM2000_Citerne" @@ -195,7 +195,7 @@ class TRM_2000_Fuel(unittype.VehicleType): detection_range = 3500 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VAB_MEDICAL(unittype.VehicleType): id = "VABH" @@ -203,7 +203,7 @@ class VAB_MEDICAL(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VAB(unittype.VehicleType): id = "VAB_RADIO" @@ -211,7 +211,7 @@ class VAB(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VBL(unittype.VehicleType): id = "VBL-Radio" @@ -219,7 +219,7 @@ class VBL(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class Tracma_TD_1500(unittype.VehicleType): id = "Tracma" @@ -236,7 +236,7 @@ class SMOKE_SAM_IR(unittype.VehicleType): detection_range = 20000 threat_range = 20000 air_weapon_dist = 20000 - eprls = True + eplrs = True class _53T2(unittype.VehicleType): id = "AA20" @@ -251,7 +251,7 @@ class TRM_2000_53T2(unittype.VehicleType): detection_range = 6000 threat_range = 2000 air_weapon_dist = 2000 - eprls = True + eplrs = True class TRM_2000_PAMELA(unittype.VehicleType): id = "TRMMISTRAL" @@ -259,7 +259,7 @@ class TRM_2000_PAMELA(unittype.VehicleType): detection_range = 8000 threat_range = 10000 air_weapon_dist = 10000 - eprls = True + eplrs = True ## INFANTRY @@ -285,4 +285,4 @@ class VAB_MORTIER(unittype.VehicleType): detection_range = 0 threat_range = 15000 air_weapon_dist = 15000 - eprls = True \ No newline at end of file + eplrs = True \ No newline at end of file diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py new file mode 100644 index 00000000..e09dd92a --- /dev/null +++ b/qt_ui/dialogs.py @@ -0,0 +1,65 @@ +"""Application-wide dialog management.""" +from typing import Optional + +from gen.flights.flight import Flight +from theater.missiontarget import MissionTarget +from .models import GameModel, PackageModel +from .windows.mission.QEditFlightDialog import QEditFlightDialog +from .windows.mission.QPackageDialog import ( + QEditPackageDialog, + QNewPackageDialog, +) + + +class Dialog: + """Dialog management singleton. + + Opens dialogs and keeps references to dialog windows so that their creators + do not need to worry about the lifetime of the dialog object, and can open + dialogs without needing to have their own reference to common data like the + game model. + """ + + #: The game model. Is only None before initialization, as the game model + #: itself is responsible for handling the case where no game is loaded. + game_model: Optional[GameModel] = None + + new_package_dialog: Optional[QNewPackageDialog] = None + edit_package_dialog: Optional[QEditPackageDialog] = None + edit_flight_dialog: Optional[QEditFlightDialog] = None + + @classmethod + def set_game(cls, game_model: GameModel) -> None: + """Sets the game model.""" + cls.game_model = game_model + + @classmethod + def open_new_package_dialog(cls, mission_target: MissionTarget): + """Opens the dialog to create a new package with the given target.""" + cls.new_package_dialog = QNewPackageDialog( + cls.game_model.game, + cls.game_model.ato_model, + mission_target + ) + cls.new_package_dialog.show() + + @classmethod + def open_edit_package_dialog(cls, package_model: PackageModel): + """Opens the dialog to edit the given package.""" + cls.edit_package_dialog = QEditPackageDialog( + cls.game_model.game, + cls.game_model.ato_model, + package_model + ) + cls.edit_package_dialog.show() + + @classmethod + def open_edit_flight_dialog(cls, package_model: PackageModel, + flight: Flight) -> None: + """Opens the dialog to edit the given flight.""" + cls.edit_flight_dialog = QEditFlightDialog( + cls.game_model.game, + package_model.package, + flight + ) + cls.edit_flight_dialog.show() diff --git a/userdata/liberation_install.py b/qt_ui/liberation_install.py similarity index 99% rename from userdata/liberation_install.py rename to qt_ui/liberation_install.py index 5f19ec0a..0440043d 100644 --- a/userdata/liberation_install.py +++ b/qt_ui/liberation_install.py @@ -4,13 +4,14 @@ from shutil import copyfile import dcs -from userdata import persistency +from game import persistency global __dcs_saved_game_directory global __dcs_installation_directory PREFERENCES_FILE_PATH = "liberation_preferences.json" + def init(): global __dcs_saved_game_directory global __dcs_installation_directory diff --git a/userdata/liberation_theme.py b/qt_ui/liberation_theme.py similarity index 68% rename from userdata/liberation_theme.py rename to qt_ui/liberation_theme.py index 703d15ee..79714209 100644 --- a/userdata/liberation_theme.py +++ b/qt_ui/liberation_theme.py @@ -1,7 +1,7 @@ import json +import logging import os - -import qt_ui.uiconstants as CONST +from typing import Dict global __theme_index @@ -10,29 +10,44 @@ THEME_PREFERENCES_FILE_PATH = "liberation_theme.json" DEFAULT_THEME_INDEX = 1 +# new themes can be added here +THEMES: Dict[int, Dict[str, str]] = { + 0: {'themeName': 'Vanilla', + 'themeFile': 'windows-style.css', + 'themeIcons': 'medium', + }, + + 1: {'themeName': 'DCS World', + 'themeFile': 'style-dcs.css', + 'themeIcons': 'light', + }, + +} + + def init(): global __theme_index __theme_index = DEFAULT_THEME_INDEX - print("init setting theme index to " + str(__theme_index)) if os.path.isfile(THEME_PREFERENCES_FILE_PATH): try: with(open(THEME_PREFERENCES_FILE_PATH)) as prefs: pref_data = json.loads(prefs.read()) __theme_index = pref_data["theme_index"] - print(__theme_index) set_theme_index(__theme_index) save_theme_config() - print("file setting theme index to " + str(__theme_index)) except: # is this necessary? set_theme_index(DEFAULT_THEME_INDEX) - print("except setting theme index to " + str(__theme_index)) + logging.exception("Unable to change theme") else: # is this necessary? set_theme_index(DEFAULT_THEME_INDEX) - print("else setting theme index to " + str(__theme_index)) + logging.error( + f"Using default theme because {THEME_PREFERENCES_FILE_PATH} " + "does not exist" + ) # set theme index then use save_theme_config to save to file @@ -49,19 +64,19 @@ def get_theme_index(): # get theme name based on current index def get_theme_name(): - theme_name = CONST.THEMES[get_theme_index()]['themeName'] + theme_name = THEMES[get_theme_index()]['themeName'] return theme_name # get theme icon sub-folder name based on current index def get_theme_icons(): - theme_icons = CONST.THEMES[get_theme_index()]['themeIcons'] + theme_icons = THEMES[get_theme_index()]['themeIcons'] return str(theme_icons) # get theme stylesheet css based on current index def get_theme_css_file(): - theme_file = CONST.THEMES[get_theme_index()]['themeFile'] + theme_file = THEMES[get_theme_index()]['themeFile'] return str(theme_file) diff --git a/qt_ui/logging_config.py b/qt_ui/logging_config.py new file mode 100644 index 00000000..739e8ac0 --- /dev/null +++ b/qt_ui/logging_config.py @@ -0,0 +1,22 @@ +"""Logging APIs.""" +import logging +import os +from logging.handlers import RotatingFileHandler + + +def init_logging(version: str) -> None: + """Initializes the logging configuration.""" + if not os.path.isdir("./logs"): + os.mkdir("logs") + + fmt = "%(asctime)s :: %(levelname)s :: %(message)s" + logging.basicConfig(level=logging.DEBUG, format=fmt) + logger = logging.getLogger() + + handler = RotatingFileHandler('./logs/liberation.log', 'a', 5000000, 1) + handler.setLevel(logging.INFO) + handler.setFormatter(logging.Formatter(fmt)) + + logger.addHandler(handler) + + logger.info(f"DCS Liberation {version}") diff --git a/qt_ui/main.py b/qt_ui/main.py index e019d32c..9503fb58 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -1,5 +1,3 @@ -from userdata import logging_config - import logging import os import sys @@ -9,11 +7,17 @@ from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen -from qt_ui import uiconstants +from game import persistency +from qt_ui import ( + liberation_install, + liberation_theme, + logging_config, + uiconstants, +) from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.QLiberationWindow import QLiberationWindow -from qt_ui.windows.preferences.QLiberationFirstStartWindow import QLiberationFirstStartWindow -from userdata import liberation_install, persistency, liberation_theme +from qt_ui.windows.preferences.QLiberationFirstStartWindow import \ + QLiberationFirstStartWindow # Logging setup logging_config.init_logging(uiconstants.VERSION_STRING) diff --git a/qt_ui/models.py b/qt_ui/models.py new file mode 100644 index 00000000..87d52538 --- /dev/null +++ b/qt_ui/models.py @@ -0,0 +1,268 @@ +"""Qt data models for game objects.""" +from typing import Any, Callable, Dict, Iterator, TypeVar, Optional + +from PySide2.QtCore import ( + QAbstractListModel, + QModelIndex, + Qt, + Signal, +) +from PySide2.QtGui import QIcon + +from game import db +from game.game import Game +from gen.ato import AirTaskingOrder, Package +from gen.flights.flight import Flight +from qt_ui.uiconstants import AIRCRAFT_ICONS +from theater.missiontarget import MissionTarget + + +class DeletableChildModelManager: + """Manages lifetimes for child models. + + Qt's data models don't have a good way of modeling related data aside from + lists, tables, or trees of similar objects. We could build one monolithic + GameModel that tracks all of the data in the game and use the parent/child + relationships of that model to index down into the ATO, packages, flights, + etc, but doing so is error prone because it requires us to manually manage + that relationship tree and keep our own mappings from row/column into + specific members. + + However, creating child models outside of the tree means that removing an + item from the parent will not signal the child's deletion to any views, so + we must track this explicitly. + + Any model which has child data types should use this class to track the + deletion of child models. All child model types must define a signal named + `deleted`. This signal will be emitted when the child model is being + deleted. Any views displaying such data should subscribe to those events and + update their display accordingly. + """ + + #: The type of data owned by models created by this class. + DataType = TypeVar("DataType") + + #: The type of model managed by this class. + ModelType = TypeVar("ModelType") + + ModelDict = Dict[DataType, ModelType] + + def __init__(self, create_model: Callable[[DataType], ModelType]) -> None: + self.create_model = create_model + self.models: DeletableChildModelManager.ModelDict = {} + + def acquire(self, data: DataType) -> ModelType: + """Returns a model for the given child data. + + If a model has already been created for the given data, it will be + returned. The data type must be hashable. + """ + if data in self.models: + return self.models[data] + model = self.create_model(data) + self.models[data] = model + return model + + def release(self, data: DataType) -> None: + """Releases the model matching the given data, if one exists. + + If the given data has had a model created for it, that model will be + deleted and its `deleted` signal will be emitted. + """ + if data in self.models: + model = self.models[data] + del self.models[data] + model.deleted.emit() + + def clear(self) -> None: + """Deletes all managed models.""" + for data in list(self.models.keys()): + self.release(data) + + +class NullListModel(QAbstractListModel): + """Generic empty list model.""" + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return 0 + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + return None + + +class PackageModel(QAbstractListModel): + """The model for an ATO package.""" + + #: Emitted when this package is being deleted from the ATO. + deleted = Signal() + + def __init__(self, package: Package) -> None: + super().__init__() + self.package = package + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.package.flights) + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + if not index.isValid(): + return None + flight = self.flight_at_index(index) + if role == Qt.DisplayRole: + return self.text_for_flight(flight) + if role == Qt.DecorationRole: + return self.icon_for_flight(flight) + return None + + @staticmethod + def text_for_flight(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) + delay = flight.scheduled_in + origin = flight.from_cp.name + return f"[{task}] {count} x {name} from {origin} in {delay} minutes" + + @staticmethod + def icon_for_flight(flight: Flight) -> Optional[QIcon]: + """Returns the icon that should be displayed for the flight.""" + name = db.unit_type_name(flight.unit_type) + if name in AIRCRAFT_ICONS: + return QIcon(AIRCRAFT_ICONS[name]) + return None + + def add_flight(self, flight: Flight) -> None: + """Adds the given flight to the package.""" + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.package.add_flight(flight) + self.endInsertRows() + + def delete_flight_at_index(self, index: QModelIndex) -> None: + """Removes the flight at the given index from the package.""" + self.delete_flight(self.flight_at_index(index)) + + def delete_flight(self, flight: Flight) -> None: + """Removes the given flight from the package. + + If the flight is using claimed inventory, the caller is responsible for + returning that inventory. + """ + index = self.package.flights.index(flight) + self.beginRemoveRows(QModelIndex(), index, index) + self.package.remove_flight(flight) + self.endRemoveRows() + + def flight_at_index(self, index: QModelIndex) -> Flight: + """Returns the flight located at the given index.""" + return self.package.flights[index.row()] + + @property + def mission_target(self) -> MissionTarget: + """Returns the mission target of the package.""" + package = self.package + target = package.target + return target + + @property + def description(self) -> str: + """Returns the description of the package.""" + return self.package.package_description + + @property + def flights(self) -> Iterator[Flight]: + """Iterates over the flights in the package.""" + for flight in self.package.flights: + yield flight + + +class AtoModel(QAbstractListModel): + """The model for an AirTaskingOrder.""" + + def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None: + super().__init__() + self.game = game + self.ato = ato + self.package_models = DeletableChildModelManager(PackageModel) + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.ato.packages) + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + if not index.isValid(): + return None + if role == Qt.DisplayRole: + package = self.ato.packages[index.row()] + return f"{package.package_description} {package.target.name}" + return None + + def add_package(self, package: Package) -> None: + """Adds a package to the ATO.""" + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.ato.add_package(package) + self.endInsertRows() + + def delete_package_at_index(self, index: QModelIndex) -> None: + """Removes the package at the given index from the ATO.""" + self.delete_package(self.package_at_index(index)) + + def delete_package(self, package: Package) -> None: + """Removes the given package from the ATO.""" + self.package_models.release(package) + index = self.ato.packages.index(package) + self.beginRemoveRows(QModelIndex(), index, index) + self.ato.remove_package(package) + for flight in package.flights: + self.game.aircraft_inventory.return_from_flight(flight) + self.endRemoveRows() + + def package_at_index(self, index: QModelIndex) -> Package: + """Returns the package at the given index.""" + return self.ato.packages[index.row()] + + def replace_from_game(self, game: Optional[Game]) -> None: + """Updates the ATO object to match the updated game object. + + If the game is None (as is the case when no game has been loaded), an + empty ATO will be used. + """ + self.beginResetModel() + self.game = game + self.package_models.clear() + if self.game is not None: + self.ato = game.blue_ato + else: + self.ato = AirTaskingOrder() + self.endResetModel() + + def get_package_model(self, index: QModelIndex) -> PackageModel: + """Returns a model for the package at the given index.""" + return self.package_models.acquire(self.package_at_index(index)) + + @property + def packages(self) -> Iterator[PackageModel]: + """Iterates over all the packages in the ATO.""" + for package in self.ato.packages: + yield self.package_models.acquire(package) + + +class GameModel: + """A model for the Game object. + + 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 + # TODO: Add red ATO model, add cheat option to show red flight plan. + self.ato_model = AtoModel(self.game, AirTaskingOrder()) + + def set(self, game: Optional[Game]) -> None: + """Updates the managed Game object. + + The argument will be None when no game has been loaded. In this state, + much of the UI is still visible and needs to handle that behavior. To + simplify that case, the AtoModel will model an empty ATO when no game is + loaded. + """ + self.game = game + self.ato_model.replace_from_game(self.game) diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index ada6c94c..5c97dc72 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -1,14 +1,12 @@ -# URL for UI links import os from typing import Dict from PySide2.QtGui import QColor, QFont, QPixmap -from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP -from userdata.liberation_theme import get_theme_icons +from .liberation_theme import get_theme_icons -VERSION_STRING = "2.1.3" +VERSION_STRING = "2.1.4" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki", @@ -28,20 +26,6 @@ FONT_PRIMARY_I = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=True) FONT_PRIMARY_B = QFont(FONT_NAME, FONT_SIZE, weight=75, italic=False) FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False) -# new themes can be added here -THEMES: Dict[int, Dict[str, str]] = { - 0: {'themeName': 'Vanilla', - 'themeFile': 'windows-style.css', - 'themeIcons': 'medium', - }, - - 1: {'themeName': 'DCS World', - 'themeFile': 'style-dcs.css', - 'themeIcons': 'light', - }, - -} - COLORS: Dict[str, QColor] = { "white": QColor(255, 255, 255), "white_transparent": QColor(255, 255, 255, 35), @@ -85,10 +69,10 @@ def load_icons(): ICONS["Hangar"] = QPixmap("./resources/ui/misc/hangar.png") ICONS["Terrain_Caucasus"] = QPixmap("./resources/ui/terrain_caucasus.gif") - ICONS["Terrain_Persian_Gulf"] = QPixmap("./resources/ui/terrain_pg.gif") + ICONS["Terrain_PersianGulf"] = QPixmap("./resources/ui/terrain_pg.gif") ICONS["Terrain_Nevada"] = QPixmap("./resources/ui/terrain_nevada.gif") ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif") - ICONS["Terrain_Channel"] = QPixmap("./resources/ui/terrain_channel.gif") + 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") @@ -127,15 +111,12 @@ EVENT_ICONS: Dict[str, QPixmap] = {} def load_event_icons(): for image in os.listdir("./resources/ui/events/"): - print(image) if image.endswith(".PNG"): EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image)) def load_aircraft_icons(): for aircraft in os.listdir("./resources/ui/units/aircrafts/"): - print(aircraft) if aircraft.endswith(".jpg"): - print(aircraft[:-7] + " : " + os.path.join("./resources/ui/units/aircrafts/", aircraft) + " ") AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft)) AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"] AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"] @@ -144,7 +125,5 @@ def load_aircraft_icons(): def load_vehicle_icons(): for vehicle in os.listdir("./resources/ui/units/vehicles/"): - print(vehicle) if vehicle.endswith(".jpg"): - print(vehicle[:-7] + " : " + os.path.join("./resources/ui/units/vehicles/", vehicle) + " ") VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle)) diff --git a/qt_ui/widgets/QFlightSizeSpinner.py b/qt_ui/widgets/QFlightSizeSpinner.py new file mode 100644 index 00000000..a2619507 --- /dev/null +++ b/qt_ui/widgets/QFlightSizeSpinner.py @@ -0,0 +1,13 @@ +"""Spin box for selecting the number of aircraft in a flight.""" +from PySide2.QtWidgets import QSpinBox + + +class QFlightSizeSpinner(QSpinBox): + """Spin box for selecting the number of aircraft in a flight.""" + + def __init__(self, min_size: int = 1, max_size: int = 4, + default_size: int = 2) -> None: + super().__init__() + self.setMinimum(min_size) + self.setMaximum(max_size) + self.setValue(default_size) diff --git a/qt_ui/widgets/QLabeledWidget.py b/qt_ui/widgets/QLabeledWidget.py new file mode 100644 index 00000000..88459896 --- /dev/null +++ b/qt_ui/widgets/QLabeledWidget.py @@ -0,0 +1,17 @@ +"""A layout containing a widget with an associated label.""" +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget + + +class QLabeledWidget(QHBoxLayout): + """A layout containing a widget with an associated label. + + Best used for vertical forms, where the given widget is the input and the + label is used to name the input. + """ + + def __init__(self, text: str, widget: QWidget) -> None: + super().__init__() + self.addWidget(QLabel(text)) + self.addStretch() + self.addWidget(widget, alignment=Qt.AlignRight) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 30725095..fae3a11d 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -1,16 +1,17 @@ -from PySide2.QtWidgets import QFrame, QHBoxLayout, QPushButton, QVBoxLayout, QGroupBox +from typing import Optional -from game import Game -from qt_ui.widgets.QBudgetBox import QBudgetBox -from qt_ui.widgets.QFactionsInfos import QFactionsInfos -from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu -from qt_ui.windows.stats.QStatsWindow import QStatsWindow -from qt_ui.widgets.QTurnCounter import QTurnCounter +from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton import qt_ui.uiconstants as CONST +from game import Game +from game.event import CAP, CAS, FrontlineAttackEvent +from qt_ui.widgets.QBudgetBox import QBudgetBox +from qt_ui.widgets.QFactionsInfos import QFactionsInfos +from qt_ui.widgets.QTurnCounter import QTurnCounter from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from qt_ui.windows.mission.QMissionPlanning import QMissionPlanning from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow +from qt_ui.windows.stats.QStatsWindow import QStatsWindow +from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow class QTopPanel(QFrame): @@ -33,10 +34,10 @@ class QTopPanel(QFrame): self.passTurnButton.setProperty("style", "btn-primary") self.passTurnButton.clicked.connect(self.passTurn) - self.proceedButton = QPushButton("Mission Planning") + self.proceedButton = QPushButton("Take off") self.proceedButton.setIcon(CONST.ICONS["Proceed"]) - self.proceedButton.setProperty("style", "btn-success") - self.proceedButton.clicked.connect(self.proceed) + self.proceedButton.setProperty("style", "start-button") + self.proceedButton.clicked.connect(self.launch_mission) if self.game and self.game.turn == 0: self.proceedButton.setEnabled(False) @@ -75,7 +76,7 @@ class QTopPanel(QFrame): self.layout.setContentsMargins(0,0,0,0) self.setLayout(self.layout) - def setGame(self, game:Game): + def setGame(self, game: Optional[Game]): self.game = game if game is not None: self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day) @@ -100,9 +101,31 @@ class QTopPanel(QFrame): GameUpdateSignal.get_instance().updateGame(self.game) self.proceedButton.setEnabled(True) - def proceed(self): - self.subwindow = QMissionPlanning(self.game) - self.subwindow.show() + def launch_mission(self): + """Finishes planning and waits for mission completion.""" + # 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) + waiting.show() def budget_update(self, game:Game): self.budgetBox.setGame(game) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py new file mode 100644 index 00000000..e4c178c4 --- /dev/null +++ b/qt_ui/widgets/ato.py @@ -0,0 +1,252 @@ +"""Widgets for displaying air tasking orders.""" +import logging +from typing import Optional + +from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt +from PySide2.QtWidgets import ( + QAbstractItemView, + QGroupBox, + QHBoxLayout, + QListView, + QPushButton, + QSplitter, + QVBoxLayout, +) + +from gen.ato import Package +from gen.flights.flight import Flight +from ..models import AtoModel, GameModel, NullListModel, PackageModel +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal + + +class QFlightList(QListView): + """List view for displaying the flights of a package.""" + + def __init__(self, model: Optional[PackageModel]) -> None: + super().__init__() + self.package_model = model + self.set_package(model) + self.setIconSize(QSize(91, 24)) + self.setSelectionBehavior(QAbstractItemView.SelectItems) + + def set_package(self, model: Optional[PackageModel]) -> None: + """Sets the package model to display.""" + if model is None: + self.disconnect_model() + else: + self.package_model = model + self.setModel(model) + # noinspection PyUnresolvedReferences + model.deleted.connect(self.disconnect_model) + self.selectionModel().setCurrentIndex( + model.index(0, 0, QModelIndex()), + QItemSelectionModel.Select + ) + + def disconnect_model(self) -> None: + """Clears the listview of any model attachments. + + Displays an empty list until set_package is called with a valid model. + """ + model = self.model() + if model is not None and isinstance(model, PackageModel): + model.deleted.disconnect(self.disconnect_model) + self.setModel(NullListModel()) + + @property + def selected_item(self) -> Optional[Flight]: + """Returns the selected flight, if any.""" + index = self.currentIndex() + if not index.isValid(): + return None + return self.package_model.flight_at_index(index) + + +class QFlightPanel(QGroupBox): + """The flight display portion of the ATO panel. + + Displays the flights assigned to the selected package, and includes edit and + delete buttons for flight management. + """ + + def __init__(self, game_model: GameModel, + package_model: Optional[PackageModel] = None) -> None: + super().__init__("Flights") + self.game_model = game_model + self.package_model = package_model + + self.vbox = QVBoxLayout() + self.setLayout(self.vbox) + + self.flight_list = QFlightList(package_model) + self.vbox.addWidget(self.flight_list) + + self.button_row = QHBoxLayout() + self.vbox.addLayout(self.button_row) + + self.edit_button = QPushButton("Edit") + self.edit_button.clicked.connect(self.on_edit) + self.button_row.addWidget(self.edit_button) + + self.delete_button = QPushButton("Delete") + # noinspection PyTypeChecker + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_row.addWidget(self.delete_button) + + self.selection_changed.connect(self.on_selection_changed) + self.on_selection_changed() + + def set_package(self, model: Optional[PackageModel]) -> None: + """Sets the package model to display.""" + self.package_model = model + self.flight_list.set_package(model) + self.on_selection_changed() + + @property + def selection_changed(self): + """Returns the signal emitted when the flight selection changes.""" + return self.flight_list.selectionModel().selectionChanged + + def on_selection_changed(self) -> None: + """Updates the status of the edit and delete buttons.""" + index = self.flight_list.currentIndex() + enabled = index.isValid() + self.edit_button.setEnabled(enabled) + self.delete_button.setEnabled(enabled) + + def on_edit(self) -> None: + """Opens the flight edit dialog.""" + index = self.flight_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot edit flight when no flight is selected.") + return + from qt_ui.dialogs import Dialog + Dialog.open_edit_flight_dialog( + self.package_model, self.package_model.flight_at_index(index) + ) + + def on_delete(self) -> None: + """Removes the selected flight from the package.""" + index = self.flight_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot delete flight when no flight is selected.") + return + self.game_model.game.aircraft_inventory.return_from_flight( + self.flight_list.selected_item) + self.package_model.delete_flight_at_index(index) + GameUpdateSignal.get_instance().redraw_flight_paths() + + +class QPackageList(QListView): + """List view for displaying the packages of an ATO.""" + + def __init__(self, model: AtoModel) -> None: + super().__init__() + self.ato_model = model + self.setModel(model) + self.setIconSize(QSize(91, 24)) + self.setSelectionBehavior(QAbstractItemView.SelectItems) + + @property + def selected_item(self) -> Optional[Package]: + """Returns the selected package, if any.""" + index = self.currentIndex() + if not index.isValid(): + return None + return self.ato_model.package_at_index(index) + + +class QPackagePanel(QGroupBox): + """The package display portion of the ATO panel. + + Displays the package assigned to the player's ATO, and includes edit and + delete buttons for package management. + """ + + def __init__(self, model: AtoModel) -> None: + super().__init__("Packages") + self.ato_model = model + self.ato_model.layoutChanged.connect(self.on_selection_changed) + + self.vbox = QVBoxLayout() + self.setLayout(self.vbox) + + self.package_list = QPackageList(self.ato_model) + self.vbox.addWidget(self.package_list) + + self.button_row = QHBoxLayout() + self.vbox.addLayout(self.button_row) + + self.edit_button = QPushButton("Edit") + self.edit_button.clicked.connect(self.on_edit) + self.button_row.addWidget(self.edit_button) + + self.delete_button = QPushButton("Delete") + # noinspection PyTypeChecker + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_row.addWidget(self.delete_button) + + self.selection_changed.connect(self.on_selection_changed) + self.on_selection_changed() + + @property + def selection_changed(self): + """Returns the signal emitted when the flight selection changes.""" + return self.package_list.selectionModel().selectionChanged + + def on_selection_changed(self) -> None: + """Updates the status of the edit and delete buttons.""" + index = self.package_list.currentIndex() + enabled = index.isValid() + self.edit_button.setEnabled(enabled) + self.delete_button.setEnabled(enabled) + + def on_edit(self) -> None: + """Opens the package edit dialog.""" + index = self.package_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot edit package when no package is selected.") + return + from qt_ui.dialogs import Dialog + Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index)) + + def on_delete(self) -> None: + """Removes the package from the ATO.""" + index = self.package_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot delete package when no package is selected.") + return + self.ato_model.delete_package_at_index(index) + GameUpdateSignal.get_instance().redraw_flight_paths() + + +class QAirTaskingOrderPanel(QSplitter): + """A split panel for displaying the packages and flights of an ATO. + + Used as the left-bar of the main UI. The top half of the panel displays the + packages of the player's ATO, and the bottom half displays the flights of + the selected package. + """ + def __init__(self, game_model: GameModel) -> None: + super().__init__(Qt.Vertical) + self.ato_model = game_model.ato_model + + self.package_panel = QPackagePanel(self.ato_model) + self.package_panel.selection_changed.connect(self.on_package_change) + self.ato_model.rowsInserted.connect(self.on_package_change) + self.addWidget(self.package_panel) + + self.flight_panel = QFlightPanel(game_model) + self.addWidget(self.flight_panel) + + def on_package_change(self) -> None: + """Sets the newly selected flight for display in the bottom panel.""" + index = self.package_panel.package_list.currentIndex() + if index.isValid(): + self.flight_panel.set_package( + self.ato_model.get_package_model(index) + ) + else: + self.flight_panel.set_package(None) diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py new file mode 100644 index 00000000..1f490e4d --- /dev/null +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -0,0 +1,16 @@ +"""Combo box for selecting aircraft types.""" +from typing import Iterable + +from PySide2.QtWidgets import QComboBox + +from dcs.planes import PlaneType + + +class QAircraftTypeSelector(QComboBox): + """Combo box for selecting among the given aircraft types.""" + + def __init__(self, aircraft_types: Iterable[PlaneType]) -> None: + super().__init__() + for aircraft in aircraft_types: + self.addItem(f"{aircraft.id}", userData=aircraft) + self.model().sort(0) diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py new file mode 100644 index 00000000..9577b26c --- /dev/null +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -0,0 +1,105 @@ +"""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, +) + + +class QFlightTypeComboBox(QComboBox): + """Combo box for selecting a flight task type.""" + + COMMON_ENEMY_MISSIONS = [ + FlightType.ESCORT, + FlightType.TARCAP, + FlightType.SEAD, + FlightType.DEAD, + # TODO: FlightType.ELINT, + # TODO: FlightType.EWAR, + # TODO: FlightType.RECON, + ] + + FRIENDLY_AIRBASE_MISSIONS = [ + FlightType.CAP, + # TODO: FlightType.INTERCEPTION + # TODO: FlightType.LOGISTICS + ] + + FRIENDLY_CARRIER_MISSIONS = [ + FlightType.BARCAP, + # TODO: FlightType.INTERCEPTION + # TODO: Buddy tanking for the A-4? + # TODO: Rescue chopper? + # TODO: Inter-ship logistics? + ] + + ENEMY_CARRIER_MISSIONS = [ + FlightType.ESCORT, + FlightType.TARCAP, + # TODO: FlightType.ANTISHIP + ] + + ENEMY_AIRBASE_MISSIONS = [ + # TODO: FlightType.STRIKE + ] + COMMON_ENEMY_MISSIONS + + FRIENDLY_GROUND_OBJECT_MISSIONS = [ + FlightType.CAP, + # TODO: FlightType.LOGISTICS + # TODO: FlightType.TROOP_TRANSPORT + ] + + ENEMY_GROUND_OBJECT_MISSIONS = [ + FlightType.STRIKE, + ] + COMMON_ENEMY_MISSIONS + + FRONT_LINE_MISSIONS = [ + FlightType.CAS, + # 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.parent_control_point(self.theater).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__}" + ) diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py new file mode 100644 index 00000000..b0995a6b --- /dev/null +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -0,0 +1,41 @@ +"""Combo box for selecting a departure airfield.""" +from typing import Iterable + +from PySide2.QtWidgets import QComboBox + +from dcs.planes import PlaneType +from game.inventory import GlobalAircraftInventory +from theater.controlpoint import ControlPoint + + +class QOriginAirfieldSelector(QComboBox): + """A combo box for selecting a flight's departure airfield. + + The combo box will automatically be populated with all departure airfields + that have unassigned inventory of the given aircraft type. + """ + + def __init__(self, global_inventory: GlobalAircraftInventory, + origins: Iterable[ControlPoint], + aircraft: PlaneType) -> None: + super().__init__() + self.global_inventory = global_inventory + self.origins = list(origins) + self.aircraft = aircraft + self.rebuild_selector() + + def change_aircraft(self, aircraft: PlaneType) -> None: + if self.aircraft == aircraft: + return + self.aircraft = aircraft + self.rebuild_selector() + + def rebuild_selector(self) -> None: + self.clear() + for origin in self.origins: + 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() diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py new file mode 100644 index 00000000..f1425893 --- /dev/null +++ b/qt_ui/widgets/map/QFrontLine.py @@ -0,0 +1,82 @@ +"""Common base for objects drawn on the game map.""" +from typing import Optional + +from PySide2.QtCore import Qt +from PySide2.QtGui import QPen +from PySide2.QtWidgets import ( + QAction, + QGraphicsLineItem, + QGraphicsSceneContextMenuEvent, + QGraphicsSceneHoverEvent, + QGraphicsSceneMouseEvent, + QMenu, +) + +import qt_ui.uiconstants as const +from qt_ui.dialogs import Dialog +from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog +from theater.missiontarget import MissionTarget + + +class QFrontLine(QGraphicsLineItem): + """Base class for objects drawn on the game map. + + Game map objects have an on_click behavior that triggers on left click, and + change the mouse cursor on hover. + """ + + def __init__(self, x1: float, y1: float, x2: float, y2: float, + mission_target: MissionTarget) -> None: + super().__init__(x1, y1, x2, y2) + self.mission_target = mission_target + self.new_package_dialog: Optional[QNewPackageDialog] = None + self.setAcceptHoverEvents(True) + + pen = QPen(brush=const.COLORS["bright_red"]) + pen.setColor(const.COLORS["orange"]) + pen.setWidth(8) + self.setPen(pen) + + def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): + self.setCursor(Qt.PointingHandCursor) + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + if event.button() == Qt.LeftButton: + self.on_click() + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: + menu = QMenu("Menu") + + object_details_action = QAction(self.object_dialog_text) + 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) + + menu.exec_(event.screenPos()) + + @property + def object_dialog_text(self) -> str: + """Text to for the object's dialog in the context menu. + + Right clicking a map object will open a context menu and the first item + will open the details dialog for this object. This menu action has the + same behavior as the on_click event. + + Return: + The text that should be displayed for the menu item. + """ + return "Details" + + def on_click(self) -> None: + """The action to take when this map object is left-clicked. + + Typically this should open a details view of the object. + """ + raise NotImplementedError + + def open_new_package_dialog(self) -> None: + """Opens the dialog for planning a new mission package.""" + Dialog.open_new_package_dialog(self.mission_target) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 41194ca0..3d775367 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,26 +1,34 @@ -import typing -from typing import Dict +import logging +from typing import Dict, List, Optional, Tuple -from PySide2 import QtCore -from PySide2.QtCore import Qt, QRect, QPointF -from PySide2.QtGui import QPixmap, QBrush, QColor, QWheelEvent, QPen, QFont -from PySide2.QtWidgets import QGraphicsView, QFrame, QGraphicsOpacityEffect +from PySide2.QtCore import Qt +from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent +from PySide2.QtWidgets import ( + QFrame, + QGraphicsItem, + QGraphicsOpacityEffect, + QGraphicsScene, + QGraphicsView, +) from dcs import Point from dcs.mapping import point_from_heading import qt_ui.uiconstants as CONST from game import Game, db from game.data.radar_db import UNITS_WITH_RADAR -from game.event import UnitsDeliveryEvent, Event, ControlPointType from gen import Conflict +from gen.flights.flight import Flight +from qt_ui.models import GameModel 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.widgets.map.QFrontLine import QFrontLine from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from theater import ControlPoint +from theater import ControlPoint, FrontLine class QLiberationMap(QGraphicsView): + WAYPOINT_SIZE = 4 instance = None display_rules: Dict[str, bool] = { @@ -32,11 +40,13 @@ class QLiberationMap(QGraphicsView): "flight_paths": False } - def __init__(self, game: Game): + def __init__(self, game_model: GameModel): super(QLiberationMap, self).__init__() QLiberationMap.instance = self + self.game_model = game_model + self.game: Optional[Game] = game_model.game - self.frontline_vector_cache = {} + self.flight_path_items: List[QGraphicsItem] = [] self.setMinimumSize(800,600) self.setMaximumHeight(2160) @@ -45,7 +55,11 @@ class QLiberationMap(QGraphicsView): self.factorized = 1 self.init_scene() self.connectSignals() - self.setGame(game) + self.setGame(game_model.game) + + GameUpdateSignal.get_instance().flight_paths_changed.connect( + lambda: self.draw_flight_plans(self.scene()) + ) def init_scene(self): scene = QLiberationScene(self) @@ -59,9 +73,9 @@ class QLiberationMap(QGraphicsView): def connectSignals(self): GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) - def setGame(self, game: Game): + def setGame(self, game: Optional[Game]): self.game = game - print("Reloading Map Canvas") + logging.debug("Reloading Map Canvas") if self.game is not None: self.reload_scene() @@ -124,8 +138,10 @@ class QLiberationMap(QGraphicsView): pos = self._transform_point(cp.position) - scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE, - CONST.CP_SIZE, cp, self.game)) + scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, + pos[1] - CONST.CP_SIZE / 2, + CONST.CP_SIZE, + CONST.CP_SIZE, cp, self.game_model)) if cp.captured: pen = QPen(brush=CONST.COLORS[playerColor]) @@ -168,38 +184,7 @@ class QLiberationMap(QGraphicsView): if self.get_display_rule("lines"): self.scene_create_lines_for_cp(cp, playerColor, enemyColor) - for cp in self.game.theater.controlpoints: - - if cp.captured: - pen = QPen(brush=CONST.COLORS[playerColor]) - brush = CONST.COLORS[playerColor+"_transparent"] - - flight_path_pen = QPen(brush=CONST.COLORS[playerColor]) - flight_path_pen.setColor(CONST.COLORS[playerColor]) - - else: - pen = QPen(brush=CONST.COLORS[enemyColor]) - brush = CONST.COLORS[enemyColor+"_transparent"] - - flight_path_pen = QPen(brush=CONST.COLORS[enemyColor]) - flight_path_pen.setColor(CONST.COLORS[enemyColor]) - - flight_path_pen.setWidth(1) - flight_path_pen.setStyle(Qt.DashDotLine) - - pos = self._transform_point(cp.position) - if self.get_display_rule("flight_paths"): - if cp.id in self.game.planners.keys(): - planner = self.game.planners[cp.id] - for flight in planner.flights: - scene.addEllipse(pos[0], pos[1], 4, 4) - prev_pos = list(pos) - for point in flight.points: - new_pos = self._transform_point(Point(point.x, point.y)) - scene.addLine(prev_pos[0]+2, prev_pos[1]+2, new_pos[0]+2, new_pos[1]+2, flight_path_pen) - scene.addEllipse(new_pos[0], new_pos[1], 4, 4, pen, brush) - prev_pos = list(new_pos) - scene.addLine(prev_pos[0] + 2, prev_pos[1] + 2, pos[0] + 2, pos[1] + 2, flight_path_pen) + self.draw_flight_plans(scene) for cp in self.game.theater.controlpoints: pos = self._transform_point(cp.position) @@ -209,6 +194,53 @@ class QLiberationMap(QGraphicsView): text.setDefaultTextColor(Qt.white) text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1) + def draw_flight_plans(self, scene) -> None: + for item in self.flight_path_items: + try: + scene.removeItem(item) + except RuntimeError: + # Something may have caused those items to already be removed. + pass + self.flight_path_items.clear() + if not self.get_display_rule("flight_paths"): + return + for package in self.game_model.ato_model.packages: + for flight in package.flights: + self.draw_flight_plan(scene, flight) + + def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None: + is_player = flight.from_cp.captured + pos = self._transform_point(flight.from_cp.position) + + self.draw_waypoint(scene, pos, is_player) + prev_pos = tuple(pos) + for point in flight.points: + new_pos = self._transform_point(Point(point.x, point.y)) + self.draw_flight_path(scene, prev_pos, new_pos, is_player) + self.draw_waypoint(scene, new_pos, is_player) + prev_pos = tuple(new_pos) + self.draw_flight_path(scene, prev_pos, pos, is_player) + + def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], + player: bool) -> None: + waypoint_pen = self.waypoint_pen(player) + waypoint_brush = self.waypoint_brush(player) + self.flight_path_items.append(scene.addEllipse( + position[0], position[1], self.WAYPOINT_SIZE, + self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush + )) + + def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], + pos1: Tuple[int, int], player: bool): + flight_path_pen = self.flight_path_pen(player) + # Draw the line to the *middle* of the waypoint. + offset = self.WAYPOINT_SIZE // 2 + self.flight_path_items.append(scene.addLine( + pos0[0] + offset, pos0[1] + offset, + pos1[0] + offset, pos1[1] + offset, + flight_path_pen + )) + def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor): scene = self.scene() pos = self._transform_point(cp.position) @@ -234,31 +266,12 @@ class QLiberationMap(QGraphicsView): p1 = point_from_heading(pos2[0], pos2[1], h+180, 25) p2 = point_from_heading(pos2[0], pos2[1], h, 25) - frontline_pen = QPen(brush=CONST.COLORS["bright_red"]) - frontline_pen.setColor(CONST.COLORS["orange"]) - frontline_pen.setWidth(8) - scene.addLine(p1[0], p1[1], p2[0], p2[1], pen=frontline_pen) + scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1], + FrontLine(cp, connected_cp))) else: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) - def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint): - # Cache mechanism to avoid performing frontline vector computation on every frame - key = str(from_cp.id) + "_" + str(to_cp.id) - if key in self.frontline_vector_cache: - return self.frontline_vector_cache[key] - else: - frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater) - self.frontline_vector_cache[key] = frontline - return frontline - - def _frontline_center(self, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[Point]: - frontline_vector = self._frontline_vector(from_cp, to_cp) - if frontline_vector: - return frontline_vector[0].point_from_heading(frontline_vector[1], frontline_vector[2]/2) - else: - return None - def wheelEvent(self, event: QWheelEvent): if event.angleDelta().y() > 0: @@ -308,6 +321,29 @@ class QLiberationMap(QGraphicsView): return X > treshold and X or treshold, Y > treshold and Y or treshold + def base_faction_color_name(self, player: bool) -> str: + if player: + return self.game.get_player_color() + else: + return self.game.get_enemy_color() + + def waypoint_pen(self, player: bool) -> QPen: + name = self.base_faction_color_name(player) + return QPen(brush=CONST.COLORS[name]) + + def waypoint_brush(self, player: bool) -> QColor: + name = self.base_faction_color_name(player) + return CONST.COLORS[f"{name}_transparent"] + + def flight_path_pen(self, player: bool) -> QPen: + name = self.base_faction_color_name(player) + color = CONST.COLORS[name] + pen = QPen(brush=color) + pen.setColor(color) + pen.setWidth(1) + pen.setStyle(Qt.DashDotLine) + return pen + def addBackground(self): scene = self.scene() diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index 09061e16..f5b2e1c4 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -1,100 +1,65 @@ -from PySide2.QtCore import QRect, Qt +from typing import Optional + from PySide2.QtGui import QColor, QPainter -from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsSceneHoverEvent, QGraphicsSceneContextMenuEvent, QMenu, \ - QAction, QGraphicsSceneMouseEvent -import qt_ui.uiconstants as CONST -from game import Game +import qt_ui.uiconstants as const +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 -from theater import ControlPoint, db +from theater import ControlPoint +from .QMapObject import QMapObject -class QMapControlPoint(QGraphicsRectItem): - - def __init__(self, parent, x: float, y: float, w: float, h: float, model: ControlPoint, game: Game): - super(QMapControlPoint, self).__init__(x, y, w, h) - self.model = model - self.game = game +class QMapControlPoint(QMapObject): + def __init__(self, parent, x: float, y: float, w: float, h: float, + control_point: ControlPoint, game_model: GameModel) -> None: + super().__init__(x, y, w, h, mission_target=control_point) + self.game_model = game_model + self.control_point = control_point self.parent = parent - self.setAcceptHoverEvents(True) self.setZValue(1) - self.setToolTip(self.model.name) - - - def paint(self, painter, option, widget=None): - #super(QMapControlPoint, self).paint(painter, option, widget) + self.setToolTip(self.control_point.name) + self.base_details_dialog: Optional[QBaseMenu2] = None + def paint(self, painter, option, widget=None) -> None: if self.parent.get_display_rule("cp"): painter.save() painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(self.brush_color) painter.setPen(self.pen_color) - if self.model.has_runway(): + if self.control_point.has_runway(): if self.isUnderMouse(): - painter.setBrush(CONST.COLORS["white"]) + painter.setBrush(const.COLORS["white"]) painter.setPen(self.pen_color) r = option.rect painter.drawEllipse(r.x(), r.y(), r.width(), r.height()) - - #gauge = QRect(r.x(), - # r.y()+CONST.CP_SIZE/2 + 2, - # r.width(), - # CONST.CP_SIZE / 4) - - #painter.setBrush(CONST.COLORS["bright_red"]) - #painter.setPen(CONST.COLORS["black"]) - #painter.drawRect(gauge) - - #gauge2 = QRect(r.x(), - # r.y() + CONST.CP_SIZE / 2 + 2, - # r.width()*self.model.base.strength, - # CONST.CP_SIZE / 4) - - #painter.setBrush(CONST.COLORS["green"]) - #painter.drawRect(gauge2) - else: - # TODO : not drawing sunk carriers. Can be improved to display sunk carrier. - pass + # TODO: Draw sunk carriers differently. + # Either don't draw them at all, or perhaps use a sunk ship icon. painter.restore() - def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent): - self.update() - - def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - - def mousePressEvent(self, event:QGraphicsSceneMouseEvent): - self.openBaseMenu() - #self.contextMenuEvent(event) - - def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): - - if self.model.captured: - openBaseMenu = QAction("Open base menu") - else: - openBaseMenu = QAction("Open intel menu") - - openBaseMenu.triggered.connect(self.openBaseMenu) - - menu = QMenu("Menu", self.parent) - menu.addAction(openBaseMenu) - menu.exec_(event.screenPos()) - - @property - def brush_color(self)->QColor: - return self.model.captured and CONST.COLORS["blue"] or CONST.COLORS["super_red"] + def brush_color(self) -> QColor: + if self.control_point.captured: + return const.COLORS["blue"] + else: + return const.COLORS["super_red"] @property def pen_color(self) -> QColor: - return self.model.captured and CONST.COLORS["white"] or CONST.COLORS["white"] + return const.COLORS["white"] - def openBaseMenu(self): - self.baseMenu = QBaseMenu2(self.window(), self.model, self.game) - self.baseMenu.show() \ No newline at end of file + @property + def object_dialog_text(self) -> str: + if self.control_point.captured: + return "Open base menu" + else: + return "Open intel menu" + + def on_click(self) -> None: + self.base_details_dialog = QBaseMenu2( + self.window(), + self.control_point, + self.game_model + ) + self.base_details_dialog.show() diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index a79ce1ab..1ed9f3d2 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -1,86 +1,86 @@ -from PySide2.QtCore import QPoint, QRect, QPointF, Qt -from PySide2.QtGui import QPainter, QBrush -from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsItem, QGraphicsSceneHoverEvent, QGraphicsSceneMouseEvent +from typing import List, Optional -import qt_ui.uiconstants as CONST -from game import db, Game +from PySide2.QtCore import QRect +from PySide2.QtGui import QBrush +from PySide2.QtWidgets import QGraphicsItem + +import qt_ui.uiconstants as const +from game import Game from game.data.building_data import FORTIFICATION_BUILDINGS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from theater import TheaterGroundObject, ControlPoint +from .QMapObject import QMapObject -class QMapGroundObject(QGraphicsRectItem): - - def __init__(self, parent, x: float, y: float, w: float, h: float, cp: ControlPoint, model: TheaterGroundObject, game:Game, buildings=[]): - super(QMapGroundObject, self).__init__(x, y, w, h) - self.model = model - self.cp = cp +class QMapGroundObject(QMapObject): + def __init__(self, parent, x: float, y: float, w: float, h: float, + control_point: ControlPoint, + ground_object: TheaterGroundObject, game: Game, + buildings: Optional[List[TheaterGroundObject]] = None) -> None: + super().__init__(x, y, w, h, mission_target=ground_object) + self.ground_object = ground_object + self.control_point = control_point self.parent = parent self.game = game - self.setAcceptHoverEvents(True) self.setZValue(2) - self.buildings = buildings + self.buildings = buildings if buildings is not None else [] self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) + self.ground_object_dialog: Optional[QGroundObjectMenu] = None - if len(self.model.groups) > 0: + if self.ground_object.groups: units = {} - for g in self.model.groups: - print(g) + for g in self.ground_object.groups: for u in g.units: - if u.type in units.keys(): + if u.type in units: units[u.type] = units[u.type]+1 else: units[u.type] = 1 - tooltip = "[" + self.model.obj_name + "]" + "\n" + tooltip = "[" + self.ground_object.obj_name + "]" + "\n" for unit in units.keys(): tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n" self.setToolTip(tooltip[:-1]) else: - tooltip = "[" + self.model.obj_name + "]" + "\n" + tooltip = "[" + self.ground_object.obj_name + "]" + "\n" for building in buildings: if not building.is_dead: tooltip = tooltip + str(building.dcs_identifier) + "\n" self.setToolTip(tooltip[:-1]) - def mousePressEvent(self, event:QGraphicsSceneMouseEvent): - self.openEditionMenu() - - def paint(self, painter, option, widget=None): - #super(QMapControlPoint, self).paint(painter, option, widget) - - playerIcons = "_blue" - enemyIcons = "" + def paint(self, painter, option, widget=None) -> None: + player_icons = "_blue" + enemy_icons = "" if self.parent.get_display_rule("go"): painter.save() - cat = self.model.category - if cat == "aa" and self.model.sea_object: + cat = self.ground_object.category + if cat == "aa" and self.ground_object.sea_object: cat = "ship" - rect = QRect(option.rect.x()+2,option.rect.y(),option.rect.width()-2,option.rect.height()) + rect = QRect(option.rect.x() + 2, option.rect.y(), + option.rect.width() - 2, option.rect.height()) - is_dead = self.model.is_dead + is_dead = self.ground_object.is_dead for building in self.buildings: if not building.is_dead: is_dead = False break - if not is_dead and not self.cp.captured: - painter.drawPixmap(rect, CONST.ICONS[cat + enemyIcons]) + if not is_dead and not self.control_point.captured: + painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) elif not is_dead: - painter.drawPixmap(rect, CONST.ICONS[cat + playerIcons]) + painter.drawPixmap(rect, const.ICONS[cat + player_icons]) else: - painter.drawPixmap(rect, CONST.ICONS["destroyed"]) + painter.drawPixmap(rect, const.ICONS["destroyed"]) - self.drawHealthGauge(painter, option) + self.draw_health_gauge(painter, option) painter.restore() - def drawHealthGauge(self, painter, option): + def draw_health_gauge(self, painter, option) -> None: units_alive = 0 units_dead = 0 - if len(self.model.groups) == 0: + if len(self.ground_object.groups) == 0: for building in self.buildings: if building.dcs_identifier in FORTIFICATION_BUILDINGS: continue @@ -89,7 +89,7 @@ class QMapGroundObject(QGraphicsRectItem): else: units_alive += 1 - for g in self.model.groups: + for g in self.ground_object.groups: units_alive += len(g.units) if hasattr(g, "units_losts"): units_dead += len(g.units_losts) @@ -97,22 +97,18 @@ class QMapGroundObject(QGraphicsRectItem): if units_dead + units_alive > 0: ratio = float(units_alive)/(float(units_dead) + float(units_alive)) bar_height = ratio * option.rect.height() - painter.fillRect(option.rect.x(), option.rect.y(), 2, option.rect.height(), QBrush(CONST.COLORS["dark_red"])) - painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, QBrush(CONST.COLORS["green"])) - - - def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - - def openEditionMenu(self): - self.editionMenu = QGroundObjectMenu(self.window(), self.model, self.buildings, self.cp, self.game) - self.editionMenu.show() + painter.fillRect(option.rect.x(), option.rect.y(), 2, + option.rect.height(), + QBrush(const.COLORS["dark_red"])) + painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, + QBrush(const.COLORS["green"])) + def on_click(self) -> None: + self.ground_object_dialog = QGroundObjectMenu( + self.window(), + self.ground_object, + self.buildings, + self.control_point, + self.game + ) + self.ground_object_dialog.show() diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py new file mode 100644 index 00000000..c98cce5e --- /dev/null +++ b/qt_ui/widgets/map/QMapObject.py @@ -0,0 +1,75 @@ +"""Common base for objects drawn on the game map.""" +from typing import Optional + +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QAction, + QGraphicsRectItem, + QGraphicsSceneContextMenuEvent, + QGraphicsSceneHoverEvent, + QGraphicsSceneMouseEvent, + QMenu, +) + +from qt_ui.dialogs import Dialog +from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog +from theater.missiontarget import MissionTarget + + +class QMapObject(QGraphicsRectItem): + """Base class for objects drawn on the game map. + + Game map objects have an on_click behavior that triggers on left click, and + change the mouse cursor on hover. + """ + + def __init__(self, x: float, y: float, w: float, h: float, + mission_target: MissionTarget) -> None: + super().__init__(x, y, w, h) + self.mission_target = mission_target + self.new_package_dialog: Optional[QNewPackageDialog] = None + self.setAcceptHoverEvents(True) + + def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): + self.setCursor(Qt.PointingHandCursor) + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + if event.button() == Qt.LeftButton: + self.on_click() + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: + menu = QMenu("Menu", self.parent) + + object_details_action = QAction(self.object_dialog_text) + 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) + + menu.exec_(event.screenPos()) + + @property + def object_dialog_text(self) -> str: + """Text to for the object's dialog in the context menu. + + Right clicking a map object will open a context menu and the first item + will open the details dialog for this object. This menu action has the + same behavior as the on_click event. + + Return: + The text that should be displayed for the menu item. + """ + return "Details" + + def on_click(self) -> None: + """The action to take when this map object is left-clicked. + + Typically this should open a details view of the object. + """ + raise NotImplementedError + + def open_new_package_dialog(self) -> None: + """Opens the dialog for planning a new mission package.""" + Dialog.open_new_package_dialog(self.mission_target) diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index dd32dd58..8a52d555 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import Optional + from PySide2.QtCore import QObject, Signal from game import Game @@ -19,21 +23,31 @@ class GameUpdateSignal(QObject): budgetupdated = Signal(Game) debriefingReceived = Signal(DebriefingSignal) + flight_paths_changed = Signal() + def __init__(self): super(GameUpdateSignal, self).__init__() GameUpdateSignal.instance = self - def updateGame(self, game: Game): + def redraw_flight_paths(self) -> None: + # noinspection PyUnresolvedReferences + self.flight_paths_changed.emit() + + def updateGame(self, game: Optional[Game]): + # noinspection PyUnresolvedReferences self.gameupdated.emit(game) def updateBudget(self, game: Game): + # noinspection PyUnresolvedReferences self.budgetupdated.emit(game) def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing): sig = DebriefingSignal(game, gameEvent, debriefing) + # noinspection PyUnresolvedReferences self.gameupdated.emit(game) + # noinspection PyUnresolvedReferences self.debriefingReceived.emit(sig) @staticmethod - def get_instance(): + def get_instance() -> GameUpdateSignal: return GameUpdateSignal.instance diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 752d19d5..e0ecce57 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -1,8 +1,15 @@ from PySide2.QtGui import QIcon, QPixmap -from PySide2.QtWidgets import QLabel, QDialog, QVBoxLayout, QGroupBox, QGridLayout, QPushButton +from PySide2.QtWidgets import ( + QDialog, + QGridLayout, + QGroupBox, + QLabel, + QPushButton, + QVBoxLayout, +) -from game.game import Event, db, Game -from userdata.debriefing import Debriefing +from game.debriefing import Debriefing +from game.game import Event, Game, db class QDebriefingWindow(QDialog): diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index bf086c5d..3933083c 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,23 +1,35 @@ import logging import sys import webbrowser +from typing import Optional from PySide2.QtCore import Qt from PySide2.QtGui import QIcon -from PySide2.QtWidgets import QWidget, QVBoxLayout, QMainWindow, QAction, QMessageBox, QDesktopWidget, \ - QSplitter, QFileDialog +from PySide2.QtWidgets import ( + QAction, + QDesktopWidget, + QFileDialog, + QMainWindow, + QMessageBox, + QSplitter, + QVBoxLayout, + QWidget, +) import qt_ui.uiconstants as CONST -from game import Game +from game import Game, persistency +from qt_ui.dialogs import Dialog +from qt_ui.models import GameModel 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 GameUpdateSignal, DebriefingSignal +from qt_ui.windows.GameUpdateSignal import DebriefingSignal, GameUpdateSignal from qt_ui.windows.QDebriefingWindow import QDebriefingWindow -from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard from qt_ui.windows.infos.QInfoPanel import QInfoPanel -from qt_ui.windows.preferences.QLiberationPreferencesWindow import QLiberationPreferencesWindow -from userdata import persistency +from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard +from qt_ui.windows.preferences.QLiberationPreferencesWindow import \ + QLiberationPreferencesWindow class QLiberationWindow(QMainWindow): @@ -25,6 +37,10 @@ class QLiberationWindow(QMainWindow): def __init__(self): super(QLiberationWindow, self).__init__() + self.game: Optional[Game] = None + self.game_model = GameModel() + Dialog.set_game(self.game_model) + self.ato_panel = None self.info_panel = None self.setGame(persistency.restore_game()) @@ -44,16 +60,19 @@ class QLiberationWindow(QMainWindow): self.setGeometry(0, 0, screen.width(), screen.height()) self.setWindowState(Qt.WindowMaximized) - def initUi(self): - - self.liberation_map = QLiberationMap(self.game) + self.ato_panel = QAirTaskingOrderPanel(self.game_model) + self.liberation_map = QLiberationMap(self.game_model) self.info_panel = QInfoPanel(self.game) hbox = QSplitter(Qt.Horizontal) - hbox.addWidget(self.info_panel) - hbox.addWidget(self.liberation_map) - hbox.setSizes([2, 8]) + vbox = QSplitter(Qt.Vertical) + hbox.addWidget(self.ato_panel) + hbox.addWidget(vbox) + vbox.addWidget(self.liberation_map) + vbox.addWidget(self.info_panel) + hbox.setSizes([100, 600]) + vbox.setSizes([600, 100]) vbox = QVBoxLayout() vbox.setMargin(0) @@ -210,10 +229,13 @@ class QLiberationWindow(QMainWindow): def exit(self): sys.exit(0) - def setGame(self, game: Game): + def setGame(self, game: Optional[Game]): + if game is not None: + game.on_load() self.game = game if self.info_panel: self.info_panel.setGame(game) + self.game_model.set(self.game) def showAboutDialog(self): text = "

DCS Liberation " + CONST.VERSION_STRING + "

" + \ diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index be56c99c..c35a482e 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -2,15 +2,24 @@ import json import os from PySide2 import QtCore -from PySide2.QtCore import QObject, Signal, Qt -from PySide2.QtGui import QMovie, QIcon, QPixmap -from PySide2.QtWidgets import QLabel, QDialog, QGroupBox, QGridLayout, QPushButton, QFileDialog, QMessageBox, QTextEdit, \ - QHBoxLayout +from PySide2.QtCore import QObject, Qt, Signal +from PySide2.QtGui import QIcon, QMovie, QPixmap +from PySide2.QtWidgets import ( + QDialog, + QFileDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QTextEdit, +) +from game.debriefing import Debriefing, wait_for_debriefing from game.game import Event, Game, logging +from game.persistency import base_path from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from userdata.debriefing import wait_for_debriefing, Debriefing -from userdata.persistency import base_path class DebriefingFileWrittenSignal(QObject): @@ -163,7 +172,7 @@ class QWaitingForMissionResultWindow(QDialog): def process_debriefing(self): self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing) - self.game.pass_turn(ignored_cps=[self.gameEvent.to_cp, ]) + self.game.pass_turn() GameUpdateSignal.get_instance().sendDebriefing(self.game, self.gameEvent, self.debriefing) self.close() diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 678f6098..cf5e1a34 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -1,31 +1,31 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QPixmap -from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget, QDialog, QGridLayout +from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget -from game import Game -from game.event import ControlPointType +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 +from theater import ControlPoint, ControlPointType class QBaseMenu2(QDialog): - def __init__(self, parent, cp: ControlPoint, game: Game): + def __init__(self, parent, cp: ControlPoint, game_model: GameModel): super(QBaseMenu2, self).__init__(parent) # Attrs self.cp = cp - self.game = game + 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, game) + 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) except: self.airport = None @@ -70,7 +70,9 @@ class QBaseMenu2(QDialog): self.mainLayout.addWidget(header, 0, 0) self.mainLayout.addWidget(self.topLayoutWidget, 1, 0) self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0) - totalBudget = QLabel(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + totalBudget = QLabel( + QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget) + ) totalBudget.setObjectName("budgetField") totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom) totalBudget.setProperty("style", "budget-label") @@ -78,7 +80,7 @@ class QBaseMenu2(QDialog): self.setLayout(self.mainLayout) def closeEvent(self, closeEvent:QCloseEvent): - GameUpdateSignal.get_instance().updateGame(self.game) + GameUpdateSignal.get_instance().updateGame(self.game_model.game) def get_base_image(self): if self.cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index dcb05ee6..11846bda 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -1,6 +1,6 @@ -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget -from game import Game +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 @@ -10,29 +10,29 @@ from theater import ControlPoint class QBaseMenuTabs(QTabWidget): - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): super(QBaseMenuTabs, self).__init__() self.cp = cp if cp: if not cp.captured: - self.intel = QIntelInfo(cp, game) + self.intel = QIntelInfo(cp, game_model.game) self.addTab(self.intel, "Intel") if not cp.is_carrier: - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Base Defenses") else: if cp.has_runway(): - self.airfield_command = QAirfieldCommand(cp, game) + 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) + self.ground_forces_hq = QGroundForcesHQ(cp, game_model) self.addTab(self.ground_forces_hq, "Ground Forces HQ") - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + 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) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Fleet") else: diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index f180dd58..b41ac68a 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,13 +1,19 @@ +from PySide2.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QSpacerItem, +) import logging - -from PySide2.QtWidgets import QLabel, QPushButton, \ - QSizePolicy, QSpacerItem, QGroupBox, QHBoxLayout from dcs.unittype import UnitType from theater import db -class QRecruitBehaviour: + +class QRecruitBehaviour: game = None cp = None deliveryEvent = None @@ -17,14 +23,22 @@ class QRecruitBehaviour: recruitable_types = [] BUDGET_FORMAT = "Available Budget: ${}M" - def __init__(self): + def __init__(self) -> None: + self.deliveryEvent = None self.bought_amount_labels = {} self.existing_units_labels = {} self.recruitable_types = [] self.update_available_budget() - def add_purchase_row(self, unit_type, layout, row): + @property + def budget(self) -> int: + return self.game_model.game.budget + @budget.setter + def budget(self, value: int) -> None: + self.game_model.game.budget = value + + def add_purchase_row(self, unit_type, layout, row): exist = QGroupBox() exist.setProperty("style", "buy-box") exist.setMaximumHeight(36) @@ -102,7 +116,8 @@ class QRecruitBehaviour: parent = parent.parent() for child in parent.children(): if child.objectName() == "budgetField": - child.setText(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + child.setText( + QRecruitBehaviour.BUDGET_FORMAT.format(self.budget)) def buy(self, unit_type): @@ -113,9 +128,9 @@ class QRecruitBehaviour: return price = db.PRICES[unit_type] - if self.game.budget >= price: + if self.budget >= price: self.deliveryEvent.deliver({unit_type: 1}) - self.game.budget -= price + self.budget -= price else: # TODO : display modal warning logging.info("Not enough money !") @@ -125,13 +140,13 @@ class QRecruitBehaviour: def sell(self, unit_type): if self.deliveryEvent.units.get(unit_type, 0) > 0: price = db.PRICES[unit_type] - self.game.budget += price + 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] elif self.cp.base.total_units_of_type(unit_type) > 0: price = db.PRICES[unit_type] - self.game.budget += price + self.budget += price self.cp.base.commit_losses({unit_type: 1}) self._update_count_label(unit_type) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index a4abf404..b679bf7b 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -1,25 +1,38 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QScrollArea, QFrame, QWidget, QHBoxLayout, QLabel +from typing import Optional -from game.event import UnitsDeliveryEvent +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QHBoxLayout, + QLabel, + QWidget, +) + +from game.event.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.uiconstants import ICONS from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint, CAP, CAS, db, ControlPointType -from game import Game +from theater import CAP, CAS, ControlPoint, db class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): - - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model + self.deliveryEvent: Optional[UnitsDeliveryEvent] = None - for event in self.game.events: + 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.units_delivery_event(self.cp) + 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) @@ -36,8 +49,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): main_layout = QVBoxLayout() units = { - CAP: db.find_unittype(CAP, self.game.player_name), - CAS: db.find_unittype(CAS, self.game.player_name), + CAP: db.find_unittype(CAP, self.game_model.game.player_name), + CAS: db.find_unittype(CAS, self.game_model.game.player_name), } scroll_content = QWidget() @@ -46,7 +59,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): for task_type in units.keys(): units_column = list(set(units[task_type])) - if len(units_column) == 0: continue + if len(units_column) == 0: + continue units_column.sort(key=lambda x: db.PRICES[x]) for unit_type in units_column: if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE: diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 74b3c973..5274640d 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -1,27 +1,30 @@ -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QHBoxLayout, QGroupBox, QVBoxLayout -from game import Game -from qt_ui.widgets.base.QAirportInformation import QAirportInformation -from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import QAircraftRecruitmentMenu +from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout + +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): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp:ControlPoint, game_model: GameModel): super(QAirfieldCommand, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game), 0, 0) + layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) try: planned = QGroupBox("Planned Flights") planned_layout = QVBoxLayout() - planned_layout.addWidget(QPlannedFlightsView(self.game.planners[self.cp.id])) + planned_layout.addWidget( + QPlannedFlightsView(self.game_model, self.cp) + ) planned.setLayout(planned_layout) layout.addWidget(planned, 0, 1) except: diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index e260d64c..ec1cabf6 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -1,27 +1,33 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QFrame, QWidget, QScrollArea +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) -from game import Game from game.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from theater import ControlPoint, PinpointStrike, db class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game.events: + 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.units_delivery_event(self.cp) + self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) self.init_ui() @@ -29,7 +35,8 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): main_layout = QVBoxLayout() units = { - PinpointStrike: db.find_unittype(PinpointStrike, self.game.player_name), + PinpointStrike: db.find_unittype(PinpointStrike, + self.game_model.game.player_name), } scroll_content = QWidget() diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py index 1ea116e3..bb18594f 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py @@ -1,21 +1,24 @@ from PySide2.QtWidgets import QFrame, QGridLayout -from game import Game -from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import QArmorRecruitmentMenu -from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import QGroundForcesStrategy +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): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: super(QGroundForcesHQ, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game), 0, 0) - layout.addWidget(QGroundForcesStrategy(self.cp, self.game), 0, 1) + layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game_model), 0, 0) + layout.addWidget(QGroundForcesStrategy(self.cp, self.game_model.game), + 0, 1) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QChooseAirbase.py b/qt_ui/windows/mission/QChooseAirbase.py deleted file mode 100644 index 50a86538..00000000 --- a/qt_ui/windows/mission/QChooseAirbase.py +++ /dev/null @@ -1,32 +0,0 @@ -from PySide2.QtCore import Signal -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel - -from game import Game - - -class QChooseAirbase(QGroupBox): - - selected_airbase_changed = Signal(str) - - def __init__(self, game:Game, title=""): - super(QChooseAirbase, self).__init__(title) - self.game = game - - self.layout = QHBoxLayout() - self.depart_from_label = QLabel("Airbase : ") - self.depart_from = QComboBox() - - for i, cp in enumerate([b for b in self.game.theater.controlpoints if b.captured and b.id in self.game.planners]): - self.depart_from.addItem(str(cp.name), cp) - self.depart_from.setCurrentIndex(0) - self.depart_from.currentTextChanged.connect(self._on_airbase_selected) - self.layout.addWidget(self.depart_from_label) - self.layout.addWidget(self.depart_from) - self.setLayout(self.layout) - - def _on_airbase_selected(self): - selected = self.depart_from.currentText() - self.selected_airbase_changed.emit(selected) - - - diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py new file mode 100644 index 00000000..9f795b79 --- /dev/null +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -0,0 +1,36 @@ +"""Dialog window for editing flights.""" +from PySide2.QtWidgets import ( + QDialog, + QVBoxLayout, +) + +from game import Game +from gen.ato import Package +from gen.flights.flight import Flight +from qt_ui.uiconstants import EVENT_ICONS +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner + + +class QEditFlightDialog(QDialog): + """Dialog window for editing flight plans and loadouts.""" + + def __init__(self, game: Game, package: Package, flight: Flight) -> None: + super().__init__() + + self.game = game + + self.setWindowTitle("Create flight") + self.setWindowIcon(EVENT_ICONS["strike"]) + + layout = QVBoxLayout() + + self.flight_planner = QFlightPlanner(package, flight, game) + layout.addWidget(self.flight_planner) + + self.setLayout(layout) + self.finished.connect(self.on_close) + + @staticmethod + def on_close(_result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() diff --git a/qt_ui/windows/mission/QMissionPlanning.py b/qt_ui/windows/mission/QMissionPlanning.py deleted file mode 100644 index 04ffdeb3..00000000 --- a/qt_ui/windows/mission/QMissionPlanning.py +++ /dev/null @@ -1,159 +0,0 @@ -from PySide2.QtCore import Qt, Slot, QItemSelectionModel, QPoint -from PySide2.QtWidgets import QDialog, QGridLayout, QScrollArea, QVBoxLayout, QPushButton, QHBoxLayout, QMessageBox -from game import Game -from game.event import CAP, CAS, FrontlineAttackEvent -from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow -from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView -from qt_ui.windows.mission.QChooseAirbase import QChooseAirbase -from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner - - -class QMissionPlanning(QDialog): - - def __init__(self, game: Game): - super(QMissionPlanning, self).__init__() - self.game = game - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setMinimumSize(1000, 440) - self.setWindowTitle("Mission Preparation") - self.setWindowIcon(EVENT_ICONS["strike"]) - self.init_ui() - print("DONE") - - def init_ui(self): - - self.captured_cp = [cp for cp in self.game.theater.controlpoints if cp.captured] - - self.layout = QGridLayout() - self.left_bar_layout = QVBoxLayout() - - self.select_airbase = QChooseAirbase(self.game) - self.select_airbase.selected_airbase_changed.connect(self.on_departure_cp_changed) - self.planned_flight_view = QPlannedFlightsView(None) - self.available_aircraft_at_selected_location = {} - if self.captured_cp[0].id in self.game.planners.keys(): - self.planner = self.game.planners[self.captured_cp[0].id] - self.planned_flight_view.set_flight_planner(self.planner) - self.selected_cp = self.captured_cp[0] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - - self.planned_flight_view.selectionModel().setCurrentIndex(self.planned_flight_view.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows) - self.planned_flight_view.selectionModel().selectionChanged.connect(self.on_flight_selection_change) - - if len(self.planned_flight_view.flight_planner.flights) > 0: - self.flight_planner = QFlightPlanner(self.planned_flight_view.flight_planner.flights[0], self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - else: - self.flight_planner = QFlightPlanner(None, self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - - self.add_flight_button = QPushButton("Add Flight") - self.add_flight_button.clicked.connect(self.on_add_flight) - self.delete_flight_button = QPushButton("Delete Selected") - self.delete_flight_button.setProperty("style", "btn-danger") - self.delete_flight_button.clicked.connect(self.on_delete_flight) - - self.button_layout = QHBoxLayout() - self.button_layout.addStretch() - self.button_layout.addWidget(self.delete_flight_button) - self.button_layout.addWidget(self.add_flight_button) - - self.mission_start_button = QPushButton("Take Off") - self.mission_start_button.setProperty("style", "start-button") - self.mission_start_button.clicked.connect(self.on_start) - - self.left_bar_layout.addWidget(self.select_airbase) - self.left_bar_layout.addWidget(self.planned_flight_view) - self.left_bar_layout.addLayout(self.button_layout) - - self.layout.addLayout(self.left_bar_layout, 0, 0) - self.layout.addWidget(self.flight_planner, 0, 1) - self.layout.addWidget(self.mission_start_button, 1, 1, alignment=Qt.AlignRight) - - self.setLayout(self.layout) - - @Slot(str) - def on_departure_cp_changed(self, cp_name): - cps = [cp for cp in self.game.theater.controlpoints if cp.name == cp_name] - - print(cps) - - if len(cps) == 1: - self.selected_cp = cps[0] - self.planner = self.game.planners[cps[0].id] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - self.planned_flight_view.set_flight_planner(self.planner) - else: - self.available_aircraft_at_selected_location = {} - self.planned_flight_view.set_flight_planner(None) - - def on_flight_selection_change(self): - - print("On flight selection change") - - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planned_flight_view.repaint() - - if self.flight_planner is not None: - self.flight_planner.on_planned_flight_changed.disconnect() - self.flight_planner.clearTabs() - - try: - flight = self.planner.flights[index] - except IndexError: - flight = None - self.flight_planner = QFlightPlanner(flight, self.game, self.planner, self.flight_planner.currentIndex()) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - self.layout.addWidget(self.flight_planner, 0, 1) - - def update_planned_flight_view(self): - self.planned_flight_view.update_content() - - def on_add_flight(self): - possible_aircraft_type = list(self.selected_cp.base.aircraft.keys()) - - if len(possible_aircraft_type) == 0: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("No more aircraft are available on " + self.selected_cp.name + " airbase.") - msg.setWindowTitle("No more aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() - else: - self.subwindow = QFlightCreator(self.game, self.selected_cp, possible_aircraft_type, self.planned_flight_view) - self.subwindow.show() - - def on_delete_flight(self): - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planner.remove_flight(index) - self.planned_flight_view.set_flight_planner(self.planner, index) - - - def on_start(self): - - # TODO : refactor this nonsense - self.gameEvent = None - for event in self.game.events: - if isinstance(event, FrontlineAttackEvent) and event.is_player_attacking: - self.gameEvent = event - if self.gameEvent is None: - self.gameEvent = 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) - #if self.awacs_checkbox.isChecked() == 1: - # self.gameEvent.is_awacs_enabled = True - # self.game.awacs_expense_commit() - #else: - # self.gameEvent.is_awacs_enabled = False - self.gameEvent.is_awacs_enabled = True - self.gameEvent.ca_slots = 1 - self.gameEvent.departure_cp = self.game.theater.controlpoints[0] - self.gameEvent.player_attacking({CAS:{}, CAP:{}}) - self.gameEvent.depart_from = self.game.theater.controlpoints[0] - - self.game.initiate_event(self.gameEvent) - waiting = QWaitingForMissionResultWindow(self.gameEvent, self.game) - waiting.show() - self.close() diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py new file mode 100644 index 00000000..21a44aa3 --- /dev/null +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -0,0 +1,208 @@ +"""Dialogs for creating and editing ATO packages.""" +import logging +from typing import Optional + +from PySide2.QtCore import QItemSelection, Signal +from PySide2.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, +) + +from game.game import Game +from gen.ato import Package +from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder +from qt_ui.models import AtoModel, 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 + + +class QPackageDialog(QDialog): + """Base package management dialog. + + The dialogs for creating a new package and editing an existing dialog are + very similar, and this implements the shared behavior. + """ + + #: Emitted when a change is made to the package. + package_changed = Signal() + + #: Emitted when a flight is added to the package. + flight_added = Signal(Flight) + + #: Emitted when a flight is removed from the package. + flight_removed = Signal(Flight) + + def __init__(self, game: Game, model: PackageModel) -> None: + super().__init__() + self.game = game + self.package_model = model + self.add_flight_dialog: Optional[QFlightCreator] = None + + self.setMinimumSize(1000, 440) + self.setWindowTitle( + f"Mission Package: {self.package_model.mission_target.name}" + ) + self.setWindowIcon(EVENT_ICONS["strike"]) + + self.layout = QVBoxLayout() + + self.summary_row = QHBoxLayout() + self.layout.addLayout(self.summary_row) + + self.package_type_label = QLabel("Package Type:") + self.package_type_text = QLabel(self.package_model.description) + # noinspection PyUnresolvedReferences + self.package_changed.connect(lambda: self.package_type_text.setText( + self.package_model.description + )) + self.summary_row.addWidget(self.package_type_label) + self.summary_row.addWidget(self.package_type_text) + + self.package_view = QFlightList(self.package_model) + self.package_view.selectionModel().selectionChanged.connect( + self.on_selection_changed + ) + self.layout.addWidget(self.package_view) + + self.button_layout = QHBoxLayout() + self.layout.addLayout(self.button_layout) + + self.add_flight_button = QPushButton("Add Flight") + self.add_flight_button.clicked.connect(self.on_add_flight) + self.button_layout.addWidget(self.add_flight_button) + + self.delete_flight_button = QPushButton("Delete Selected") + self.delete_flight_button.setProperty("style", "btn-danger") + self.delete_flight_button.clicked.connect(self.on_delete_flight) + self.delete_flight_button.setEnabled(False) + self.button_layout.addWidget(self.delete_flight_button) + + self.button_layout.addStretch() + + self.setLayout(self.layout) + + self.finished.connect(self.on_close) + + @staticmethod + def on_close(_result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() + + def on_selection_changed(self, selected: QItemSelection, + _deselected: QItemSelection) -> None: + """Updates the state of the delete button.""" + self.delete_flight_button.setEnabled(not selected.empty()) + + def on_add_flight(self) -> None: + """Opens the new flight dialog.""" + self.add_flight_dialog = QFlightCreator(self.game, + self.package_model.package) + self.add_flight_dialog.created.connect(self.add_flight) + self.add_flight_dialog.show() + + def add_flight(self, flight: Flight) -> None: + """Adds the new flight to the package.""" + self.package_model.add_flight(flight) + planner = FlightPlanBuilder(self.game, self.package_model.package, + is_player=True) + planner.populate_flight_plan(flight) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + # noinspection PyUnresolvedReferences + self.flight_added.emit(flight) + + def on_delete_flight(self) -> None: + """Removes the selected flight from the package.""" + flight = self.package_view.selected_item + if flight is None: + logging.error(f"Cannot delete flight when no flight is selected.") + return + self.package_model.delete_flight(flight) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + # noinspection PyUnresolvedReferences + self.flight_removed.emit(flight) + + +class QNewPackageDialog(QPackageDialog): + """Dialog window for creating a new package. + + New packages do not affect the ATO model until they are saved. + """ + + def __init__(self, game: Game, model: AtoModel, + target: MissionTarget) -> None: + super().__init__(game, PackageModel(Package(target))) + self.ato_model = model + + self.save_button = QPushButton("Save") + self.save_button.setProperty("style", "start-button") + self.save_button.clicked.connect(self.on_save) + self.button_layout.addWidget(self.save_button) + + self.delete_flight_button.clicked.connect(self.on_delete_flight) + + def on_save(self) -> None: + """Saves the created package. + + Empty packages may be created. They can be modified later, and will have + no effect if empty when the mission is generated. + """ + self.ato_model.add_package(self.package_model.package) + for flight in self.package_model.package.flights: + self.game.aircraft_inventory.claim_for_flight(flight) + self.close() + + +class QEditPackageDialog(QPackageDialog): + """Dialog window for editing an existing package. + + Changes to existing packages occur immediately. + """ + + def __init__(self, game: Game, model: AtoModel, + package: PackageModel) -> None: + super().__init__(game, package) + self.ato_model = model + + self.delete_button = QPushButton("Delete package") + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_layout.addWidget(self.delete_button) + + self.done_button = QPushButton("Done") + self.done_button.setProperty("style", "start-button") + self.done_button.clicked.connect(self.on_done) + self.button_layout.addWidget(self.done_button) + + # noinspection PyUnresolvedReferences + self.flight_added.connect(self.on_flight_added) + # noinspection PyUnresolvedReferences + self.flight_removed.connect(self.on_flight_removed) + + # TODO: Make the new package dialog do this too, return on cancel. + # Not claiming the aircraft when they are added to the planner means that + # inventory counts are not updated until after the new package is updated, + # so you can add an infinite number of aircraft to a new package in the UI, + # which will crash when the flight package is saved. + def on_flight_added(self, flight: Flight) -> None: + self.game.aircraft_inventory.claim_for_flight(flight) + + def on_flight_removed(self, flight: Flight) -> None: + self.game.aircraft_inventory.return_from_flight(flight) + + def on_done(self) -> None: + """Closes the window.""" + self.close() + + def on_delete(self) -> None: + """Removes the viewed package from the ATO.""" + # The ATO model returns inventory for us when deleting a package. + self.ato_model.delete_package(self.package_model.package) + self.close() diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py index 0dcc8a81..a7c45e51 100644 --- a/qt_ui/windows/mission/QPlannedFlightsView.py +++ b/qt_ui/windows/mission/QPlannedFlightsView.py @@ -1,37 +1,36 @@ -from PySide2.QtCore import QSize, QItemSelectionModel, QPoint +from PySide2.QtCore import QItemSelectionModel, QSize from PySide2.QtGui import QStandardItemModel -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtWidgets import QAbstractItemView, QListView -from gen.flights.ai_flight_planner import FlightPlanner +from qt_ui.models import GameModel from qt_ui.windows.mission.QFlightItem import QFlightItem +from theater.controlpoint import ControlPoint class QPlannedFlightsView(QListView): - def __init__(self, flight_planner: FlightPlanner): + def __init__(self, game_model: GameModel, cp: ControlPoint) -> None: super(QPlannedFlightsView, self).__init__() + self.game_model = game_model + self.cp = cp self.model = QStandardItemModel(self) self.setModel(self.model) - self.flightitems = [] + self.flight_items = [] self.setIconSize(QSize(91, 24)) self.setSelectionBehavior(QAbstractItemView.SelectItems) - if flight_planner: - self.set_flight_planner(flight_planner) + self.set_flight_planner() - def update_content(self): - for i, f in enumerate(self.flight_planner.flights): - self.flightitems[i].update(f) + def setup_content(self): + self.flight_items = [] + for package in self.game_model.ato_model.packages: + for flight in package.flights: + if flight.from_cp == self.cp: + item = QFlightItem(flight) + self.model.appendRow(item) + self.flight_items.append(item) + self.set_selected_flight(0) - def setup_content(self, row=0): - self.flightitems = [] - for i, f in enumerate(self.flight_planner.flights): - item = QFlightItem(f) - self.model.appendRow(item) - self.flightitems.append(item) - self.setSelectedFlight(row) - self.repaint() - - def setSelectedFlight(self, row): + def set_selected_flight(self, row): self.selectionModel().clearSelection() index = self.model.index(row, 0) if not index.isValid(): @@ -42,8 +41,6 @@ class QPlannedFlightsView(QListView): def clear_layout(self): self.model.removeRows(0, self.model.rowCount()) - def set_flight_planner(self, flight_planner: FlightPlanner, row=0): + def set_flight_planner(self) -> None: self.clear_layout() - self.flight_planner = flight_planner - if self.flight_planner: - self.setup_content(row) + self.setup_content() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 293ba75f..2c8c7dfe 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -1,122 +1,102 @@ -from typing import List +from typing import Optional -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QPushButton, QSpinBox, \ - QMessageBox -from dcs import Point -from dcs.unittype import UnitType +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import ( + QDialog, + QPushButton, + QVBoxLayout, +) +from dcs.planes import PlaneType from game import Game -from gen.flights.ai_flight_planner import FlightPlanner -from gen.flights.flight import Flight, FlightWaypoint, FlightType +from gen.ato import Package +from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox +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.QFlightTypeComboBox import QFlightTypeComboBox +from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector from theater import ControlPoint -PREDEFINED_WAYPOINT_CATEGORIES = [ - "Frontline (CAS AREA)", - "Building", - "Units", - "Airbase" -] - class QFlightCreator(QDialog): + created = Signal(Flight) + + def __init__(self, game: Game, package: Package) -> None: + super().__init__() - def __init__(self, game: Game, from_cp:ControlPoint, possible_aircraft_type:List[UnitType], flight_view=None): - super(QFlightCreator, self).__init__() self.game = game - self.from_cp = from_cp - self.flight_view = flight_view - self.planner = self.game.planners[from_cp.id] - self.available = self.planner.get_available_aircraft() + self.package = package - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setModal(True) self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) - self.select_type_aircraft = QComboBox() - for aircraft_type in self.planner.get_available_aircraft().keys(): - print(aircraft_type) - print(aircraft_type.name) - if self.available[aircraft_type] > 0: - self.select_type_aircraft.addItem(aircraft_type.id, userData=aircraft_type) - self.select_type_aircraft.setCurrentIndex(0) - - self.select_flight_type = QComboBox() - self.select_flight_type.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP) - self.select_flight_type.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP) - self.select_flight_type.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP) - self.select_flight_type.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION) - self.select_flight_type.addItem("CAS [Close Air Support]", userData=FlightType.CAS) - self.select_flight_type.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI) - self.select_flight_type.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD) - self.select_flight_type.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD) - self.select_flight_type.addItem("STRIKE [Strike]", userData=FlightType.STRIKE) - self.select_flight_type.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP) - self.select_flight_type.setCurrentIndex(0) - - self.select_count_of_aircraft = QSpinBox() - self.select_count_of_aircraft.setMinimum(1) - self.select_count_of_aircraft.setMaximum(4) - self.select_count_of_aircraft.setValue(2) - - aircraft_type = self.select_type_aircraft.currentData() - if aircraft_type is not None: - self.select_count_of_aircraft.setValue(min(self.available[aircraft_type], 2)) - self.select_count_of_aircraft.setMaximum(min(self.available[aircraft_type], 4)) - - self.add_button = QPushButton("Add") - self.add_button.clicked.connect(self.create_flight) - - self.init_ui() - - - def init_ui(self): layout = QVBoxLayout() - type_layout = QHBoxLayout() - type_layout.addWidget(QLabel("Type of Aircraft : ")) - type_layout.addStretch() - type_layout.addWidget(self.select_type_aircraft, alignment=Qt.AlignRight) + self.task_selector = QFlightTypeComboBox( + self.game.theater, package.target + ) + self.task_selector.setCurrentIndex(0) + layout.addLayout(QLabeledWidget("Task:", self.task_selector)) - count_layout = QHBoxLayout() - count_layout.addWidget(QLabel("Count : ")) - count_layout.addStretch() - count_layout.addWidget(self.select_count_of_aircraft, alignment=Qt.AlignRight) + self.aircraft_selector = QAircraftTypeSelector( + self.game.aircraft_inventory.available_types_for_player + ) + self.aircraft_selector.setCurrentIndex(0) + self.aircraft_selector.currentIndexChanged.connect( + self.on_aircraft_changed) + layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector)) - flight_type_layout = QHBoxLayout() - flight_type_layout.addWidget(QLabel("Task : ")) - flight_type_layout.addStretch() - flight_type_layout.addWidget(self.select_flight_type, alignment=Qt.AlignRight) + self.airfield_selector = QOriginAirfieldSelector( + self.game.aircraft_inventory, + [cp for cp in game.theater.controlpoints if cp.captured], + self.aircraft_selector.currentData() + ) + layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector)) + + self.flight_size_spinner = QFlightSizeSpinner() + layout.addLayout(QLabeledWidget("Count:", self.flight_size_spinner)) - layout.addLayout(type_layout) - layout.addLayout(count_layout) - layout.addLayout(flight_type_layout) layout.addStretch() - layout.addWidget(self.add_button, alignment=Qt.AlignRight) + + self.create_button = QPushButton("Create") + self.create_button.clicked.connect(self.create_flight) + layout.addWidget(self.create_button, alignment=Qt.AlignRight) self.setLayout(layout) - def create_flight(self): - aircraft_type = self.select_type_aircraft.currentData() - count = self.select_count_of_aircraft.value() + def verify_form(self) -> Optional[str]: + aircraft: PlaneType = self.aircraft_selector.currentData() + origin: ControlPoint = self.airfield_selector.currentData() + size: int = self.flight_size_spinner.value() + if not origin.captured: + return f"{origin.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." + if size > available: + return f"{origin.name} has only {available} {aircraft.id} available." + return None - if self.available[aircraft_type] < count: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("Not enough aircraft of this type are available. Only " + str(self.available[aircraft_type]) + " available.") - msg.setWindowTitle("Not enough aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() + def create_flight(self) -> None: + error = self.verify_form() + if error is not None: + self.error_box("Could not create flight", error) return - else: - flight = Flight(aircraft_type, count, self.from_cp, self.select_flight_type.currentData()) - self.planner.flights.append(flight) - self.planner.custom_flights.append(flight) - if self.flight_view is not None: - self.flight_view.set_flight_planner(self.planner, len(self.planner.flights)-1) - self.close() + task = self.task_selector.currentData() + aircraft = self.aircraft_selector.currentData() + origin = self.airfield_selector.currentData() + size = self.flight_size_spinner.value() + + flight = Flight(aircraft, size, origin, task) + flight.scheduled_in = self.package.delay + + # noinspection PyUnresolvedReferences + self.created.emit(flight) + self.close() + + def on_aircraft_changed(self, index: int) -> None: + new_aircraft = self.aircraft_selector.itemData(index) + self.airfield_selector.change_aircraft(new_aircraft) diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index 6e422b93..af48219c 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -1,42 +1,32 @@ from PySide2.QtCore import Signal -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QTabWidget -from gen.flights.flight import Flight from game import Game -from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import QFlightPayloadTab -from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import QGeneralFlightSettingsTab -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import QFlightWaypointTab +from gen.ato import Package +from gen.flights.flight import Flight +from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ + QFlightPayloadTab +from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \ + QGeneralFlightSettingsTab +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \ + QFlightWaypointTab class QFlightPlanner(QTabWidget): on_planned_flight_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner, selected_tab): - super(QFlightPlanner, self).__init__() + def __init__(self, package: Package, flight: Flight, game: Game): + super().__init__() - print(selected_tab) - - self.tabCount = 0 - if flight: - self.general_settings_tab = QGeneralFlightSettingsTab(flight, game, planner) - self.general_settings_tab.on_flight_settings_changed.connect(lambda: self.on_planned_flight_changed.emit()) - self.payload_tab = QFlightPayloadTab(flight, game) - self.waypoint_tab = QFlightWaypointTab(game, flight) - self.waypoint_tab.on_flight_changed.connect(lambda: self.on_planned_flight_changed.emit()) - self.addTab(self.general_settings_tab, "General Flight settings") - self.addTab(self.payload_tab, "Payload") - self.addTab(self.waypoint_tab, "Waypoints") - self.tabCount = 3 - self.setCurrentIndex(selected_tab) - else: - tabError = QFrame() - l = QGridLayout() - l.addWidget(QLabel("No flight selected")) - tabError.setLayout(l) - self.addTab(tabError, "No flight") - self.tabCount = 1 - - def clearTabs(self): - for i in range(self.tabCount): - self.removeTab(i) + self.general_settings_tab = QGeneralFlightSettingsTab(game, flight) + self.general_settings_tab.on_flight_settings_changed.connect( + lambda: self.on_planned_flight_changed.emit()) + self.payload_tab = QFlightPayloadTab(flight, game) + self.waypoint_tab = QFlightWaypointTab(game, package, flight) + self.waypoint_tab.on_flight_changed.connect( + lambda: self.on_planned_flight_changed.emit()) + self.addTab(self.general_settings_tab, "General Flight settings") + self.addTab(self.payload_tab, "Payload") + self.addTab(self.waypoint_tab, "Waypoints") + self.setCurrentIndex(0) diff --git a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py deleted file mode 100644 index 8a69d4cd..00000000 --- a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py +++ /dev/null @@ -1,43 +0,0 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QDialog, QPushButton - -from game import Game -from gen.flights.flight import Flight -from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox - - -class QAbstractMissionGenerator(QDialog): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list, title): - super(QAbstractMissionGenerator, self).__init__() - self.game = game - self.flight = flight - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setMinimumSize(400, 250) - self.setModal(True) - self.setWindowTitle(title) - self.setWindowIcon(EVENT_ICONS["strike"]) - self.flight_waypoint_list = flight_waypoint_list - self.planner = self.game.planners[self.flight.from_cp.id] - - self.selected_waypoints = [] - self.wpt_info = QFlightWaypointInfoBox() - - self.ok_button = QPushButton("Ok") - self.ok_button.clicked.connect(self.apply) - - def on_select_wpt_changed(self): - self.selected_waypoints = self.wpt_selection_box.get_selected_waypoints(False) - if self.selected_waypoints is None or len(self.selected_waypoints) <= 0: - self.ok_button.setDisabled(True) - else: - self.wpt_info.set_flight_waypoint(self.selected_waypoints[0]) - self.ok_button.setDisabled(False) - - def apply(self): - raise NotImplementedError() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py deleted file mode 100644 index c1f5591e..00000000 --- a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py +++ /dev/null @@ -1,52 +0,0 @@ -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout - -from game import Game -from gen.flights.flight import Flight, PredefinedWaypointCategory -from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QCAPMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCAPMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAP Generator") - - self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, True, True, False, False, True) - self.wpt_selection_box.setMinimumWidth(200) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.init_ui() - self.on_select_wpt_changed() - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAP mission on : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - - wpt = self.selected_waypoints[0] - if wpt.category == PredefinedWaypointCategory.FRONTLINE: - self.planner.generate_frontline_cap(self.flight, wpt.data[0], wpt.data[1]) - elif wpt.category == PredefinedWaypointCategory.ALLY_CP: - self.planner.generate_barcap(self.flight, wpt.data) - else: - return - - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py deleted file mode 100644 index cfae4e52..00000000 --- a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py +++ /dev/null @@ -1,65 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox -from dcs import Point - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QCASMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCASMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAS Generator") - - self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, False, True, False, False) - self.wpt_selection_box.setMinimumWidth(200) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.init_ui() - self.on_select_wpt_changed() - - def on_select_wpt_changed(self): - super(QCASMissionGenerator, self).on_select_wpt_changed() - wpts = self.wpt_selection_box.get_selected_waypoints() - - if len(wpts) > 0: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(Point(wpts[0].x, wpts[0].y)))) + " nm") - else: - self.distanceToTargetLabel.setText("??? nm") - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAS : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - self.planner.generate_cas(self.flight, self.selected_waypoints[0].data[0], self.selected_waypoints[0].data[1]) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py deleted file mode 100644 index 7221844c..00000000 --- a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py +++ /dev/null @@ -1,84 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QSEADTargetSelectionComboBox import QSEADTargetSelectionComboBox -from qt_ui.widgets.views.QSeadTargetInfoView import QSeadTargetInfoView -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QSEADMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSEADMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "SEAD/DEAD Generator") - - self.tgt_selection_box = QSEADTargetSelectionComboBox(self.game) - self.tgt_selection_box.setMinimumWidth(200) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.threatRangeLabel = QLabel("0 nm") - self.detectionRangeLabel = QLabel("0 nm") - self.seadTargetInfoView = QSeadTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - if target is not None: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.threatRangeLabel.setText(str(meter_to_nm(target.threat_range)) + " nm") - self.detectionRangeLabel.setText(str(meter_to_nm(target.detection_range)) + " nm") - self.seadTargetInfoView.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("SEAD/DEAD target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distThreatBox = QGroupBox("Infos :") - threatLayout = QVBoxLayout() - - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to site : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - - threatRangeLayout = QHBoxLayout() - threatRangeLayout.addWidget(QLabel("Site threat range : ")) - threatRangeLayout.addStretch() - threatRangeLayout.addWidget(self.threatRangeLabel, alignment=Qt.AlignRight) - - detectionRangeLayout = QHBoxLayout() - detectionRangeLayout.addWidget(QLabel("Site radar detection range: ")) - detectionRangeLayout.addStretch() - detectionRangeLayout.addWidget(self.detectionRangeLabel, alignment=Qt.AlignRight) - - threatLayout.addLayout(distToTarget) - threatLayout.addLayout(threatRangeLayout) - threatLayout.addLayout(detectionRangeLayout) - distThreatBox.setLayout(threatLayout) - - layout.addLayout(wpt_layout) - layout.addWidget(self.seadTargetInfoView) - layout.addWidget(distThreatBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - target = self.tgt_selection_box.get_selected_target() - self.planner.generate_sead(self.flight, target.location, target.radars) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py deleted file mode 100644 index 6da88e0b..00000000 --- a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py +++ /dev/null @@ -1,64 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -from gen.flights.flight import Flight -from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import QStrikeTargetSelectionComboBox -from qt_ui.widgets.views.QStrikeTargetInfoView import QStrikeTargetInfoView -from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator - - -class QSTRIKEMissionGenerator(QAbstractMissionGenerator): - - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSTRIKEMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "Strike Generator") - - self.tgt_selection_box = QStrikeTargetSelectionComboBox(self.game) - self.tgt_selection_box.setMinimumWidth(200) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - - self.distanceToTargetLabel = QLabel("0 nm") - self.strike_infos = QStrikeTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.strike_infos.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("Target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.strike_infos) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - self.flight.points = [] - target = self.tgt_selection_box.get_selected_target() - self.planner.generate_strike(self.flight, target.location) - self.flight_waypoint_list.update_list() - self.close() - - - - diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index 36a72dc4..4c8f3bac 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -6,12 +6,14 @@ class QFlightSlotEditor(QGroupBox): changed = Signal() - def __init__(self, flight, game, planner): + def __init__(self, flight, game): super(QFlightSlotEditor, self).__init__("Slots") self.flight = flight self.game = game - self.planner = planner - self.available = self.planner.get_available_aircraft() + inventory = self.game.aircraft_inventory.for_control_point( + flight.from_cp + ) + self.available = inventory.all_aircraft if self.flight.unit_type not in self.available: max = self.flight.count else: diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index cabd99cf..99f2b63f 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -12,18 +12,15 @@ from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTyp class QGeneralFlightSettingsTab(QFrame): on_flight_settings_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner): + def __init__(self, game: Game, flight: Flight): super(QGeneralFlightSettingsTab, self).__init__() self.flight = flight self.game = game - self.planner = planner - self.init_ui() - def init_ui(self): layout = QGridLayout() flight_info = QFlightTypeTaskInfo(self.flight) flight_departure = QFlightDepartureEditor(self.flight) - flight_slots = QFlightSlotEditor(self.flight, self.game, self.planner) + flight_slots = QFlightSlotEditor(self.flight, self.game) flight_start_type = QFlightStartType(self.flight) layout.addWidget(flight_info, 0, 0) layout.addWidget(flight_departure, 1, 0) @@ -35,5 +32,7 @@ class QGeneralFlightSettingsTab(QFrame): self.setLayout(layout) flight_start_type.setEnabled(self.flight.client_count > 0) - flight_slots.changed.connect(lambda: flight_start_type.setEnabled(self.flight.client_count > 0)) - flight_departure.changed.connect(lambda: self.on_flight_settings_changed.emit()) + flight_slots.changed.connect( + lambda: flight_start_type.setEnabled(self.flight.client_count > 0)) + flight_departure.changed.connect( + lambda: self.on_flight_settings_changed.emit()) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 69870d1c..5e9e57a2 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -1,75 +1,99 @@ +from typing import List, Optional + from PySide2.QtCore import Signal -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLayout +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, +) from game import Game -from gen.flights.flight import Flight -from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator -from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator -from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator -from qt_ui.windows.mission.flight.generator.QSTRIKEMissionGenerator import QSTRIKEMissionGenerator -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import QFlightWaypointList -from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import QPredefinedWaypointSelectionWindow +from gen.ato import Package +from gen.flights.flight import Flight, FlightType +from gen.flights.flightplan import FlightPlanBuilder +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \ + QFlightWaypointList +from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import \ + QPredefinedWaypointSelectionWindow +from theater import ControlPoint, FrontLine class QFlightWaypointTab(QFrame): on_flight_changed = Signal() - def __init__(self, game: Game, flight: Flight): + def __init__(self, game: Game, package: Package, flight: Flight): super(QFlightWaypointTab, self).__init__() - self.flight = flight self.game = game - self.planner = self.game.planners[self.flight.from_cp.id] + self.package = package + self.flight = flight + self.planner = FlightPlanBuilder(self.game, package, is_player=True) + + self.flight_waypoint_list: Optional[QFlightWaypointList] = None + self.ascend_waypoint: Optional[QPushButton] = None + self.descend_waypoint: Optional[QPushButton] = None + self.rtb_waypoint: Optional[QPushButton] = None + self.delete_selected: Optional[QPushButton] = None + self.open_fast_waypoint_button: Optional[QPushButton] = None + self.recreate_buttons: List[QPushButton] = [] self.init_ui() def init_ui(self): layout = QGridLayout() - rlayout = QVBoxLayout() + self.flight_waypoint_list = QFlightWaypointList(self.flight) - self.open_fast_waypoint_button = QPushButton("Add Waypoint") - self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) - - self.cas_generator = QPushButton("Gen. CAS") - self.cas_generator.clicked.connect(self.on_cas_generator) - - self.cap_generator = QPushButton("Gen. CAP") - self.cap_generator.clicked.connect(self.on_cap_generator) - - self.sead_generator = QPushButton("Gen. SEAD/DEAD") - self.sead_generator.clicked.connect(self.on_sead_generator) - - self.strike_generator = QPushButton("Gen. STRIKE") - self.strike_generator.clicked.connect(self.on_strike_generator) - - self.rtb_waypoint = QPushButton("Add RTB Waypoint") - self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) - - self.ascend_waypoint = QPushButton("Add Ascend Waypoint") - self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) - - self.descend_waypoint = QPushButton("Add Descend Waypoint") - self.descend_waypoint.clicked.connect(self.on_descend_waypoint) - - self.delete_selected = QPushButton("Delete Selected") - self.delete_selected.clicked.connect(self.on_delete_waypoint) - layout.addWidget(self.flight_waypoint_list, 0, 0) + rlayout = QVBoxLayout() + layout.addLayout(rlayout, 0, 1) + rlayout.addWidget(QLabel("Generator :")) rlayout.addWidget(QLabel("AI compatible")) - rlayout.addWidget(self.cas_generator) - rlayout.addWidget(self.cap_generator) - rlayout.addWidget(self.sead_generator) - rlayout.addWidget(self.strike_generator) + + self.recreate_buttons.clear() + recreate_types = [ + FlightType.CAS, + FlightType.CAP, + FlightType.ESCORT, + FlightType.SEAD, + FlightType.STRIKE + ] + for task in recreate_types: + def make_closure(arg): + def closure(): + return self.confirm_recreate(arg) + return closure + button = QPushButton(f"Recreate as {task.name}") + button.clicked.connect(make_closure(task)) + rlayout.addWidget(button) + self.recreate_buttons.append(button) + rlayout.addWidget(QLabel("Advanced : ")) rlayout.addWidget(QLabel("Do not use for AI flights")) + + self.ascend_waypoint = QPushButton("Add Ascend Waypoint") + self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) rlayout.addWidget(self.ascend_waypoint) + + self.descend_waypoint = QPushButton("Add Descend Waypoint") + self.descend_waypoint.clicked.connect(self.on_descend_waypoint) rlayout.addWidget(self.descend_waypoint) + + self.rtb_waypoint = QPushButton("Add RTB Waypoint") + self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) rlayout.addWidget(self.rtb_waypoint) - rlayout.addWidget(self.open_fast_waypoint_button) + + self.delete_selected = QPushButton("Delete Selected") + self.delete_selected.clicked.connect(self.on_delete_waypoint) rlayout.addWidget(self.delete_selected) + + self.open_fast_waypoint_button = QPushButton("Add Waypoint") + self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) + rlayout.addWidget(self.open_fast_waypoint_button) rlayout.addStretch() - layout.addLayout(rlayout, 0, 1) self.setLayout(layout) def on_delete_waypoint(self): @@ -102,25 +126,27 @@ class QFlightWaypointTab(QFrame): self.flight_waypoint_list.update_list() self.on_change() - def on_cas_generator(self): - self.subwindow = QCASMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_cap_generator(self): - self.subwindow = QCAPMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_sead_generator(self): - self.subwindow = QSEADMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_strike_generator(self): - self.subwindow = QSTRIKEMissionGenerator(self.game, self.flight, self.flight_waypoint_list) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() + def confirm_recreate(self, task: FlightType) -> None: + result = QMessageBox.question( + self, + "Regenerate flight?", + ("Changing the flight type will reset its flight plan. Do you want " + "to continue?"), + QMessageBox.No, + QMessageBox.Yes + ) + if result == QMessageBox.Yes: + # TODO: These should all be just CAP. + if task == FlightType.CAP: + if isinstance(self.package.target, FrontLine): + task = FlightType.TARCAP + elif isinstance(self.package.target, ControlPoint): + if self.package.target.is_fleet: + task = FlightType.BARCAP + self.flight.flight_type = task + self.planner.populate_flight_plan(self.flight) + self.flight_waypoint_list.update_list() + self.on_change() def on_change(self): self.flight_waypoint_list.update_list() diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 06c42d9f..5d3de21c 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -1,48 +1,62 @@ +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import List + from PySide2 import QtGui -from PySide2.QtCore import QSize, QItemSelectionModel -from PySide2.QtGui import QStandardItemModel, QStandardItem -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtCore import QItemSelectionModel +from PySide2.QtGui import QStandardItem, QStandardItemModel +from PySide2.QtWidgets import QAbstractItemView, QListView -from theater import caucasus, nevada, persiangulf, normandy, thechannel, syria import qt_ui.uiconstants as CONST +from theater import ConflictTheater -CAMPAIGNS = [ - ("Caucasus - Western Georgia", caucasus.WesternGeorgia, "Terrain_Caucasus"), - ("Caucasus - Russia Small", caucasus.RussiaSmall, "Terrain_Caucasus"), - ("Caucasus - North Caucasus", caucasus.NorthCaucasus, "Terrain_Caucasus"), - ("Caucasus - Full Map", caucasus.CaucasusTheater, "Terrain_Caucasus"), - ("Nevada - North Nevada", nevada.NevadaTheater, "Terrain_Nevada"), - ("Persian Gulf - Invasion of Iran", persiangulf.IranianCampaign, "Terrain_Persian_Gulf"), - ("Persian Gulf - Invasion of Iran [Lite]", persiangulf.IranInvasionLite, "Terrain_Persian_Gulf"), - ("Persian Gulf - Emirates", persiangulf.Emirates, "Terrain_Persian_Gulf"), - ("Persian Gulf - Desert War", persiangulf.DesertWar, "Terrain_Persian_Gulf"), - ("Persian Gulf - Full Map", persiangulf.PersianGulfTheater, "Terrain_Persian_Gulf"), - ("Syria - Golan heights battle", syria.GolanHeights, "Terrain_Syria"), - ("Syria - Invasion from Turkey", syria.TurkishInvasion, "Terrain_Syria"), - ("Syria - Syrian Civil War", syria.SyrianCivilWar, "Terrain_Syria"), - ("Syria - Inherent Resolve", syria.InherentResolve, "Terrain_Syria"), - ("Syria - Full Map", syria.SyriaFullMap, "Terrain_Syria"), +@dataclass(frozen=True) +class Campaign: + name: str + icon_name: str + theater: ConflictTheater - ("Normandy - Normandy", normandy.NormandyTheater, "Terrain_Normandy"), - ("Normandy - Normandy Small", normandy.NormandySmall, "Terrain_Normandy"), - ("The Channel - Battle of Britain", thechannel.BattleOfBritain, "Terrain_Channel"), - ("The Channel - Dunkirk", thechannel.Dunkirk, "Terrain_Channel"), -] + @classmethod + def from_json(cls, path: Path) -> Campaign: + with path.open() as campaign_file: + data = json.load(campaign_file) + + sanitized_theater = data["theater"].replace(" ", "") + return cls(data["name"], f"Terrain_{sanitized_theater}", + ConflictTheater.from_json(data)) + + +def load_campaigns() -> List[Campaign]: + campaign_dir = Path("resources\\campaigns") + campaigns = [] + for path in campaign_dir.iterdir(): + try: + logging.debug(f"Loading campaign from {path}...") + campaign = Campaign.from_json(path) + campaigns.append(campaign) + except RuntimeError: + logging.exception(f"Unable to load campaign from {path}") + + return sorted(campaigns, key=lambda x: x.name) class QCampaignItem(QStandardItem): - def __init__(self, text, theater, icon): + def __init__(self, campaign: Campaign) -> None: super(QCampaignItem, self).__init__() - self.theater = theater - self.setIcon(QtGui.QIcon(CONST.ICONS[icon])) + self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name])) self.setEditable(False) - self.setText(text) + self.setText(campaign.name) + class QCampaignList(QListView): - def __init__(self): + def __init__(self, campaigns: List[Campaign]) -> None: super(QCampaignList, self).__init__() self.model = QStandardItemModel(self) self.setModel(self.model) @@ -50,12 +64,12 @@ class QCampaignList(QListView): self.setMinimumHeight(350) self.campaigns = [] self.setSelectionBehavior(QAbstractItemView.SelectItems) - self.setup_content() + self.setup_content(campaigns) - def setup_content(self): - for i, campaign in enumerate(CAMPAIGNS): + def setup_content(self, campaigns: List[Campaign]) -> None: + for campaign in campaigns: self.campaigns.append(campaign) - item = QCampaignItem(*campaign) + item = QCampaignItem(campaign) self.model.appendRow(item) self.setSelectedCampaign(0) self.repaint() diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 3daace57..8a706ec7 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -2,27 +2,34 @@ from __future__ import unicode_literals import datetime import logging +from typing import List from PySide2 import QtGui, QtWidgets -from PySide2.QtCore import QPoint, QItemSelectionModel -from PySide2.QtWidgets import QHBoxLayout, QVBoxLayout +from PySide2.QtCore import QItemSelectionModel, QPoint +from PySide2.QtWidgets import QVBoxLayout from dcs.task import CAP, CAS import qt_ui.uiconstants as CONST -from game import db, Game +from game import Game, db from game.settings import Settings from gen import namegen -from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS -from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel +from qt_ui.windows.newgame.QCampaignList import ( + Campaign, + QCampaignList, + load_campaigns, +) +from theater import ConflictTheater, start_generator class NewGameWizard(QtWidgets.QWizard): def __init__(self, parent=None): super(NewGameWizard, self).__init__(parent) + self.campaigns = load_campaigns() + self.addPage(IntroPage()) self.addPage(FactionSelection()) - self.addPage(TheaterConfiguration()) + self.addPage(TheaterConfiguration(self.campaigns)) self.addPage(MiscOptions()) self.addPage(ConclusionPage()) @@ -43,8 +50,9 @@ class NewGameWizard(QtWidgets.QWizard): selectedCampaign = self.field("selectedCampaign") if selectedCampaign is None: - selectedCampaign = CAMPAIGNS[0] - conflictTheater = selectedCampaign[1]() + selectedCampaign = self.campaigns[0] + + conflictTheater = selectedCampaign.theater timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] midGame = self.field("midGame") @@ -83,7 +91,7 @@ class NewGameWizard(QtWidgets.QWizard): print("Enemy name : " + enemy_name) print("Player name : " + player_name) print("Midgame : " + str(midgame)) - start_generator.generate_inital_units(conflictTheater, enemy_name, True, multiplier) + start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier) print("-- Initial units generated") game = Game(player_name=player_name, @@ -231,8 +239,8 @@ class FactionSelection(QtWidgets.QWizardPage): class TheaterConfiguration(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super(TheaterConfiguration, self).__init__(parent) + def __init__(self, campaigns: List[Campaign], parent=None) -> None: + super().__init__(parent) self.setTitle("Theater configuration") self.setSubTitle("\nChoose a terrain and time period for this game.") @@ -242,37 +250,8 @@ class TheaterConfiguration(QtWidgets.QWizardPage): self.setPixmap(QtWidgets.QWizard.WatermarkPixmap, QtGui.QPixmap('./resources/ui/wizard/watermark3.png')) - # Terrain selection - terrainGroup = QtWidgets.QGroupBox("Terrain") - terrainCaucasusSmall = QtWidgets.QRadioButton("Caucasus - Western Georgia") - terrainCaucasusSmall.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainRussia = QtWidgets.QRadioButton("Caucasus - Russia Small") - terrainRussia.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainCaucasus = QtWidgets.QRadioButton("Caucasus - Full map [NOT RECOMMENDED]") - terrainCaucasus.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainCaucasusNorth = QtWidgets.QRadioButton("Caucasus - North") - terrainCaucasusNorth.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - - terrainPg = QtWidgets.QRadioButton("Persian Gulf - Full Map [NOT RECOMMENDED]") - terrainPg.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainIran = QtWidgets.QRadioButton("Persian Gulf - Invasion of Iran") - terrainIran.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainEmirates = QtWidgets.QRadioButton("Persian Gulf - Emirates") - terrainEmirates.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainNttr = QtWidgets.QRadioButton("Nevada - North Nevada") - terrainNttr.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Nevada"])) - terrainNormandy = QtWidgets.QRadioButton("Normandy") - terrainNormandy.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Normandy"])) - terrainNormandySmall = QtWidgets.QRadioButton("Normandy Small") - terrainNormandySmall.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Normandy"])) - terrainChannel = QtWidgets.QRadioButton("The Channel : Start in Dunkirk") - terrainChannel.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Channel"])) - terrainChannelComplete = QtWidgets.QRadioButton("The Channel : Battle of Britain") - terrainChannelComplete.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Channel"])) - terrainCaucasusSmall.setChecked(True) - # List of campaigns - campaignList = QCampaignList() + campaignList = QCampaignList(campaigns) self.registerField("selectedCampaign", campaignList) def on_campaign_selected(): @@ -284,8 +263,6 @@ class TheaterConfiguration(QtWidgets.QWizardPage): campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) on_campaign_selected() - - # Campaign settings mapSettingsGroup = QtWidgets.QGroupBox("Map Settings") invertMap = QtWidgets.QCheckBox() diff --git a/qt_ui/windows/preferences/QLiberationPreferences.py b/qt_ui/windows/preferences/QLiberationPreferences.py index 074c1c17..bc392561 100644 --- a/qt_ui/windows/preferences/QLiberationPreferences.py +++ b/qt_ui/windows/preferences/QLiberationPreferences.py @@ -1,16 +1,21 @@ import os -from PySide2 import QtWidgets -from PySide2.QtCore import QFile from PySide2.QtGui import Qt -from PySide2.QtWidgets import QFrame, QLineEdit, QGridLayout, QVBoxLayout, QLabel, QPushButton, \ - QFileDialog, QMessageBox, QDialog, QComboBox, QApplication -import qt_ui.uiconstants as CONST -import sys +from PySide2.QtWidgets import ( + QComboBox, + QFileDialog, + QFrame, + QGridLayout, + QLabel, + QLineEdit, + QMessageBox, + QPushButton, + QVBoxLayout, +) -import userdata -from userdata import liberation_install, liberation_theme -from userdata.liberation_theme import get_theme_index, set_theme_index +import qt_ui.uiconstants as CONST +from qt_ui import liberation_install, liberation_theme +from qt_ui.liberation_theme import get_theme_index, set_theme_index class QLiberationPreferences(QFrame): diff --git a/resources/campaigns/battle_of_britain.json b/resources/campaigns/battle_of_britain.json new file mode 100644 index 00000000..38f5ab0f --- /dev/null +++ b/resources/campaigns/battle_of_britain.json @@ -0,0 +1,86 @@ +{ + "name": "The Channel - Battle of Britain", + "theater": "The Channel", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json new file mode 100644 index 00000000..e49f7081 --- /dev/null +++ b/resources/campaigns/desert_war.json @@ -0,0 +1,61 @@ +{ + "name": "Persian Gulf - Desert War", + "theater": "Persian Gulf", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json new file mode 100644 index 00000000..d3cf11b0 --- /dev/null +++ b/resources/campaigns/dunkirk.json @@ -0,0 +1,77 @@ +{ + "name": "The Channel - Dunkirk", + "theater": "The Channel", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/emirates.json b/resources/campaigns/emirates.json new file mode 100644 index 00000000..1e032e63 --- /dev/null +++ b/resources/campaigns/emirates.json @@ -0,0 +1,106 @@ +{ + "name": "Persian Gulf - Emirates", + "theater": "Persian Gulf", + "player_points": [ + { + "type": "airbase", + "id": "Fujairah Intl", + "radials": [ + 180, + 225, + 270, + 315, + 0 + ], + "size": 1000, + "importance": 1, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -79770, + "y": 49430, + "captured_invert": true + }, + { + "type": "carrier", + "id": 1001, + "x": -61770, + "y": 69039, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Al Dhafra AB", + "size": 2000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Ain International Airport", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Maktoum Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Minhad AB", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sharjah Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Ras Al Khaimah", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Al Ain International Airport", + "Al Dhafra AB" + ], + [ + "Al Dhafra AB", + "Al Maktoum Intl" + ], + [ + "Al Ain International Airport", + "Fujairah Intl" + ], + [ + "Al Ain International Airport", + "Al Maktoum Intl" + ], + [ + "Al Maktoum Intl", + "Al Minhad AB" + ], + [ + "Al Minhad AB", + "Sharjah Intl" + ], + [ + "Ras Al Khaimah", + "Sharjah Intl" + ], + [ + "Fujairah Intl", + "Sharjah Intl" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/full_map.json b/resources/campaigns/full_map.json new file mode 100644 index 00000000..f88963ea --- /dev/null +++ b/resources/campaigns/full_map.json @@ -0,0 +1,183 @@ +{ + "name": "Syria - Full Map", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -151000, + "y": -106000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -131000, + "y": -161000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Rene Mouawad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Minakh", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Hatay", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "King Hussein Air College", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Al-Dumayr", + "Khalkhalah" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Palmyra" + ], + [ + "Al Qusayr", + "Rene Mouawad" + ], + [ + "Bassel Al-Assad", + "Rene Mouawad" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hatay" + ], + [ + "Palmyra", + "Tabqa" + ], + [ + "Jirah", + "Tabqa" + ], + [ + "Aleppo", + "Jirah" + ], + [ + "Aleppo", + "Minakh" + ], + [ + "Hatay", + "Minakh" + ], + [ + "Incirlik", + "Minakh" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/golan_heights_battle.json b/resources/campaigns/golan_heights_battle.json new file mode 100644 index 00000000..f498b3bb --- /dev/null +++ b/resources/campaigns/golan_heights_battle.json @@ -0,0 +1,81 @@ +{ + "name": "Syria - Golan heights battle", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -280000, + "y": -238000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -237000, + "y": -89800, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Mezzeh", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2, + "captured_invert": true + } + ], + "links": [ + [ + "Khalkhalah", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Khalkhalah", + "Marj Ruhayyil" + ], + [ + "Marj Ruhayyil", + "Mezzeh" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json new file mode 100644 index 00000000..b968bc77 --- /dev/null +++ b/resources/campaigns/inherent_resolve.json @@ -0,0 +1,82 @@ +{ + "name": "Syria - Inherent Resolve", + "theater": "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json new file mode 100644 index 00000000..de0f6189 --- /dev/null +++ b/resources/campaigns/invasion_from_turkey.json @@ -0,0 +1,85 @@ +{ + "name": "Syria - Invasion from Turkey", + "theater": "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json new file mode 100644 index 00000000..888d29f5 --- /dev/null +++ b/resources/campaigns/invasion_of_iran.json @@ -0,0 +1,141 @@ +{ + "name": "Persian Gulf - Invasion of Iran", + "theater": "Persian Gulf", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json new file mode 100644 index 00000000..aa31a7fa --- /dev/null +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -0,0 +1,75 @@ +{ + "name": "Persian Gulf - Invasion of Iran [Lite]", + "theater": "Persian Gulf", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json new file mode 100644 index 00000000..8c71716e --- /dev/null +++ b/resources/campaigns/normandy.json @@ -0,0 +1,83 @@ +{ + "name": "Normandy - Normandy", + "theater": "Normandy", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json new file mode 100644 index 00000000..a0bff78e --- /dev/null +++ b/resources/campaigns/normandy_small.json @@ -0,0 +1,53 @@ +{ + "name": "Normandy - Normandy Small", + "theater": "Normandy", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_caucasus.json b/resources/campaigns/north_caucasus.json new file mode 100644 index 00000000..0da83e72 --- /dev/null +++ b/resources/campaigns/north_caucasus.json @@ -0,0 +1,99 @@ +{ + "name": "Caucasus - North Caucasus", + "theater": "Caucasus", + "player_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Vaziani", + "size": 600, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875, + "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 + }, + { + "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.3 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "Kutaisi", + "Vaziani" + ], + [ + "Beslan", + "Vaziani" + ], + [ + "Beslan", + "Mozdok" + ], + [ + "Beslan", + "Nalchik" + ], + [ + "Mozdok", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_nevada.json b/resources/campaigns/north_nevada.json new file mode 100644 index 00000000..d1687a7c --- /dev/null +++ b/resources/campaigns/north_nevada.json @@ -0,0 +1,71 @@ +{ + "name": "Nevada - North Nevada", + "theater": "Nevada", + "player_points": [ + { + "type": "airbase", + "id": "Nellis AFB", + "size": 2000, + "importance": 1.4 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Tonopah Test Range Airfield", + "size": 600, + "importance": 1, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Lincoln County", + "size": 600, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Groom Lake AFB", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Creech AFB", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Mesquite", + "size": 1000, + "importance": 1.3 + } + ], + "links": [ + [ + "Lincoln County", + "Tonopah Test Range Airfield" + ], + [ + "Groom Lake AFB", + "Tonopah Test Range Airfield" + ], + [ + "Lincoln County", + "Mesquite" + ], + [ + "Groom Lake AFB", + "Mesquite" + ], + [ + "Creech AFB", + "Groom Lake AFB" + ], + [ + "Creech AFB", + "Nellis AFB" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json new file mode 100644 index 00000000..341d90d0 --- /dev/null +++ b/resources/campaigns/russia_small.json @@ -0,0 +1,37 @@ +{ + "name": "Caucasus - Russia Small", + "theater": "Caucasus", + "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" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/syrian_civil_war.json b/resources/campaigns/syrian_civil_war.json new file mode 100644 index 00000000..0f041d39 --- /dev/null +++ b/resources/campaigns/syrian_civil_war.json @@ -0,0 +1,91 @@ +{ + "name": "Syria - Syrian Civil War", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": 18537, + "y": -52000, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": 116000, + "y": -30000, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2, + "captured_invert": true + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + } + ], + "links": [ + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Palmyra" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/western_georgia.json b/resources/campaigns/western_georgia.json new file mode 100644 index 00000000..b0e64464 --- /dev/null +++ b/resources/campaigns/western_georgia.json @@ -0,0 +1,111 @@ +{ + "name": "Caucasus - Western Georgia", + "theater": "Caucasus", + "player_points": [ + { + "type": "airbase", + "id": "Kobuleti", + "radials": [ + 0, + 45, + 90, + 135, + 180, + 225, + 315 + ], + "size": 600, + "importance": 1.1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875, + "captured_invert": true + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875, + "captured_invert": true + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Senaki-Kolkhi", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sukhumi-Babushara", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Gudauta", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Sochi-Adler", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 2000, + "importance": 1.4, + "captured_invert": true + } + ], + "links": [ + [ + "Kutaisi", + "Senaki-Kolkhi" + ], + [ + "Kobuleti", + "Senaki-Kolkhi" + ], + [ + "Senaki-Kolkhi", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sochi-Adler" + ] + ] +} \ No newline at end of file diff --git a/theater/__init__.py b/theater/__init__.py index 282ea4f3..209a6646 100644 --- a/theater/__init__.py +++ b/theater/__init__.py @@ -1,3 +1,5 @@ -from .controlpoint import * -from .conflicttheater import * from .base import * +from .conflicttheater import * +from .controlpoint import * +from .frontline import FrontLine +from .missiontarget import MissionTarget diff --git a/theater/base.py b/theater/base.py index b294dd38..4ca5dec7 100644 --- a/theater/base.py +++ b/theater/base.py @@ -1,14 +1,15 @@ -import logging -import typing -import math import itertools +import logging +import math +import typing +from typing import Dict, Type -from dcs.planes import * -from dcs.vehicles import * -from dcs.task import * +from dcs.planes import PlaneType +from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task +from dcs.unittype import UnitType, VehicleType +from dcs.vehicles import AirDefence, Armor from game import db -from gen import aaa STRENGTH_AA_ASSEMBLE_MIN = 0.2 PLANES_SCRAMBLE_MIN_BASE = 2 @@ -21,16 +22,15 @@ BASE_MIN_STRENGTH = 0 class Base: aircraft = {} # type: typing.Dict[PlaneType, int] - armor = {} # type: typing.Dict[Armor, int] + armor = {} # type: typing.Dict[VehicleType, int] aa = {} # type: typing.Dict[AirDefence, int] strength = 1 # type: float - commision_points = {} def __init__(self): self.aircraft = {} self.armor = {} self.aa = {} - self.commision_points = {} + self.commision_points: Dict[Type, float] = {} self.strength = 1 @property @@ -55,17 +55,19 @@ class Base: def all_units(self): return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) - def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict: + def _find_best_unit(self, available_units: Dict[UnitType, int], + for_type: Task, count: int) -> Dict[UnitType, int]: if count <= 0: logging.warning("{}: no units for {}".format(self, for_type)) return {} - sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]] + sorted_units = [key for key in available_units if + key in db.UNIT_BY_TASK[for_type]] sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) - result = {} + result: Dict[UnitType, int] = {} for unit_type in sorted_units: - existing_count = dict[unit_type] # type: int + existing_count = available_units[unit_type] # type: int if not existing_count: continue diff --git a/theater/caucasus.py b/theater/caucasus.py deleted file mode 100644 index 1ebad24d..00000000 --- a/theater/caucasus.py +++ /dev/null @@ -1,212 +0,0 @@ -from dcs import mapping -from dcs.terrain import caucasus - -from .conflicttheater import * -from .landmap import * - - -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), - } - - - - def __init__(self, load_ground_objects=True): - super(CaucasusTheater, self).__init__() - - self.vaziani = ControlPoint.from_airport(caucasus.Vaziani, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.senaki = ControlPoint.from_airport(caucasus.Senaki_Kolkhi, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.kobuleti = ControlPoint.from_airport(caucasus.Kobuleti, COAST_A_E, SIZE_SMALL, 1.1) - self.batumi = ControlPoint.from_airport(caucasus.Batumi, COAST_DL_E, SIZE_SMALL, 1.3) - self.sukhumi = ControlPoint.from_airport(caucasus.Sukhumi_Babushara, COAST_DR_E, SIZE_REGULAR, 1.2) - self.gudauta = ControlPoint.from_airport(caucasus.Gudauta, COAST_DR_E, SIZE_REGULAR, 1.2) - self.sochi = ControlPoint.from_airport(caucasus.Sochi_Adler, COAST_DR_E, SIZE_BIG, IMPORTANCE_HIGH) - self.gelendzhik = ControlPoint.from_airport(caucasus.Gelendzhik, COAST_DR_E, SIZE_BIG, 1.1) - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.krasnodar = ControlPoint.from_airport(caucasus.Krasnodar_Center, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.krymsk = ControlPoint.from_airport(caucasus.Krymsk, LAND, SIZE_LARGE, 1.2) - self.anapa = ControlPoint.from_airport(caucasus.Anapa_Vityazevo, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.beslan = ControlPoint.from_airport(caucasus.Beslan, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.nalchik = ControlPoint.from_airport(caucasus.Nalchik, LAND, SIZE_REGULAR, 1.1) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-305810.6875, 406399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.vaziani.frontline_offset = 0.5 - self.vaziani.base.strength = 1 - - self.add_controlpoint(self.vaziani, connected_to=[self.kutaisi, self.beslan]) - self.add_controlpoint(self.beslan, connected_to=[self.vaziani, self.mozdok, self.nalchik]) - self.add_controlpoint(self.nalchik, connected_to=[self.beslan, self.mozdok, self.mineralnye]) - self.add_controlpoint(self.mozdok, connected_to=[self.nalchik, self.beslan, self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.nalchik, self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye, self.krasnodar]) - - self.add_controlpoint(self.kutaisi, connected_to=[self.vaziani, self.senaki]) - self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi, self.kutaisi]) - self.add_controlpoint(self.kobuleti, connected_to=[self.batumi, self.senaki]) - self.add_controlpoint(self.batumi, connected_to=[self.kobuleti]) - self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta, self.senaki]) - self.add_controlpoint(self.gudauta, connected_to=[self.sochi, self.sukhumi]) - self.add_controlpoint(self.sochi, connected_to=[self.gudauta, self.gelendzhik]) - - self.add_controlpoint(self.gelendzhik, connected_to=[self.sochi, self.krymsk]) - self.add_controlpoint(self.krymsk, connected_to=[self.anapa, self.krasnodar, self.gelendzhik]) - self.add_controlpoint(self.anapa, connected_to=[self.krymsk]) - self.add_controlpoint(self.krasnodar, connected_to=[self.krymsk, self.maykop]) - - self.add_controlpoint(self.carrier_1) - self.add_controlpoint(self.lha) - - self.carrier_1.captured = True - self.carrier_1.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - - self.batumi.captured = True - self.anapa.captured_invert = True - - -""" -A smaller version of the caucasus map in western georgia. -Ideal for smaller scale campaign -""" -class WesternGeorgia(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), - } - - - def __init__(self, load_ground_objects=True): - super(WesternGeorgia, self).__init__() - - self.kobuleti = ControlPoint.from_airport(caucasus.Kobuleti, COAST_A_E, SIZE_SMALL, 1.1) - self.senaki = ControlPoint.from_airport(caucasus.Senaki_Kolkhi, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.sukhumi = ControlPoint.from_airport(caucasus.Sukhumi_Babushara, COAST_DR_E, SIZE_REGULAR, 1.2) - self.gudauta = ControlPoint.from_airport(caucasus.Gudauta, COAST_DR_E, SIZE_REGULAR, 1.2) - self.sochi = ControlPoint.from_airport(caucasus.Sochi_Adler, COAST_DR_E, SIZE_BIG, IMPORTANCE_HIGH) - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-285810.6875, 496399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.add_controlpoint(self.kutaisi, connected_to=[self.senaki]) - self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi, self.kutaisi]) - self.add_controlpoint(self.kobuleti, connected_to=[self.senaki]) - self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta, self.senaki]) - self.add_controlpoint(self.gudauta, connected_to=[self.sochi, self.sukhumi]) - self.add_controlpoint(self.sochi, connected_to=[self.gudauta]) - self.add_controlpoint(self.carrier_1) - self.add_controlpoint(self.lha) - - self.carrier_1.captured = True - self.carrier_1.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - self.kobuleti.captured = True - self.sochi.captured_invert = True - - -""" -Georgian Theather [inverted starting position] -Ideal for smaller scale campaign -""" -class RussiaSmall(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), - } - - def __init__(self, load_ground_objects=True): - super(RussiaSmall, self).__init__() - - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - - self.add_controlpoint(self.mozdok, connected_to=[self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye]) - - self.mozdok.captured = True - self.maykop.captured_invert = True - - -class NorthCaucasus(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), - } - - def __init__(self, load_ground_objects=True): - super(NorthCaucasus, self).__init__() - - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.vaziani = ControlPoint.from_airport(caucasus.Vaziani, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.beslan = ControlPoint.from_airport(caucasus.Beslan, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.nalchik = ControlPoint.from_airport(caucasus.Nalchik, LAND, SIZE_REGULAR, 1.1) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-285810.6875, 496399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.vaziani.frontline_offset = 0.5 - self.vaziani.base.strength = 1 - - self.add_controlpoint(self.kutaisi, connected_to=[self.vaziani]) - self.add_controlpoint(self.vaziani, connected_to=[self.beslan, self.kutaisi]) - self.add_controlpoint(self.beslan, connected_to=[self.vaziani, self.mozdok, self.nalchik]) - self.add_controlpoint(self.nalchik, connected_to=[self.beslan, self.mozdok, self.mineralnye]) - self.add_controlpoint(self.mozdok, connected_to=[self.nalchik, self.beslan, self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.nalchik, self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye]) - self.add_controlpoint(self.carrier_1, connected_to=[]) - self.add_controlpoint(self.lha, connected_to=[]) - - self.carrier_1.captured = True - self.vaziani.captured = True - self.kutaisi.captured = True - - self.carrier_1.captured_invert = True - self.maykop.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - self.mineralnye.captured_invert = True diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 621f106a..c9013062 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,10 +1,21 @@ -import typing +from __future__ import annotations + +import json +from typing import Any, Dict, Iterator, List, Optional, Tuple -import dcs 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 poly_contains +from .landmap import Landmap, load_landmap, poly_contains SIZE_TINY = 150 SIZE_SMALL = 600 @@ -32,7 +43,7 @@ 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_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] @@ -45,26 +56,28 @@ COAST_DR_W = [135, 180, 225, 315] class ConflictTheater: - terrain = None # type: dcs.terrain.Terrain - controlpoints = None # type: typing.Collection[ControlPoint] + terrain: Terrain - reference_points = None # type: typing.Dict - overview_image = None # type: str - landmap = None # type: landmap.Landmap + reference_points: Dict[Tuple[float, float], Tuple[float, float]] + overview_image: str + landmap: Optional[Landmap] """ land_poly = None # type: Polygon """ - daytime_map = None # type: typing.Dict[str, typing.Tuple[int, int]] + daytime_map: Dict[str, Tuple[int, int]] def __init__(self): - self.controlpoints = [] + 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: [ControlPoint] = []): + 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) @@ -106,15 +119,165 @@ class ConflictTheater: return True - def player_points(self) -> typing.Collection[ControlPoint]: + def player_points(self) -> List[ControlPoint]: return [point for point in self.controlpoints if point.captured] - def conflicts(self, from_player=True) -> typing.Collection[typing.Tuple[ControlPoint, ControlPoint]]: + def conflicts(self, from_player=True) -> Iterator[Tuple[ControlPoint, ControlPoint]]: 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 (cp, connected_point) + yield cp, connected_point - def enemy_points(self) -> typing.Collection[ControlPoint]: + 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\\nev_landmap.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 index 2aba30ca..6f520bd1 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -1,13 +1,20 @@ import re -import typing +from typing import Dict, List from enum import Enum -from dcs.mapping import * -from dcs.terrain import Airport -from dcs.ships import CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock +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 TheaterGroundObject @@ -19,37 +26,28 @@ class ControlPointType(Enum): FOB = 5 # A FOB (ground units only) -class ControlPoint: +class ControlPoint(MissionTarget): - id = 0 position = None # type: Point name = None # type: str - full_name = None # type: str - base = None # type: theater.base.Base - at = None # type: db.StartPosition allow_sea_units = True - connected_points = None # type: typing.List[ControlPoint] - ground_objects = None # type: typing.List[TheaterGroundObject] - captured = False has_frontline = True frontline_offset = 0.0 - cptype: ControlPointType = None alt = 0 - def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float, - has_frontline=True, cptype=ControlPointType.AIRBASE): - import theater.base - + 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): self.id = id self.name = " ".join(re.split(r" |-", name)[:2]) self.full_name = name - self.position = position + self.position: Point = position self.at = at - self.ground_objects = [] - self.ships = [] + self.ground_objects: List[TheaterGroundObject] = [] self.size = size self.importance = importance @@ -57,14 +55,14 @@ class ControlPoint: self.captured_invert = False self.has_frontline = has_frontline self.radials = radials - self.connected_points = [] - self.base = theater.base.Base() + self.connected_points: List[ControlPoint] = [] + self.base: Base = Base() self.cptype = cptype - self.stances = {} + self.stances: Dict[int, CombatStance] = {} self.airport = None @classmethod - def from_airport(cls, airport: Airport, radials: typing.Collection[int], size: int, importance: float, has_frontline=True): + 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() @@ -122,7 +120,7 @@ class ControlPoint: return self.cptype in [ControlPointType.LHA_GROUP] @property - def sea_radials(self) -> typing.Collection[int]: + def sea_radials(self) -> List[int]: # TODO: fix imports all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ] result = [] @@ -189,11 +187,11 @@ class ControlPoint: def is_connected(self, to) -> bool: return to in self.connected_points - def find_radial(self, heading: int, ignored_radial: int = None): + 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 = math.fabs(radial - heading) + delta = abs(radial - heading) if delta < closest_radial_delta: closest_radial = radial closest_radial_delta = delta @@ -207,3 +205,5 @@ class ControlPoint: found.append(g) return found + def is_friendly(self, to_player: bool) -> bool: + return self.captured == to_player diff --git a/theater/frontline.py b/theater/frontline.py new file mode 100644 index 00000000..c71ec4e3 --- /dev/null +++ b/theater/frontline.py @@ -0,0 +1,45 @@ +"""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 + + +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: + 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 + + @property + def name(self) -> str: + a = self.control_point_a.name + b = self.control_point_b.name + return f"Front line {a}/{b}" + + @property + def position(self) -> Point: + a = self.control_point_a.position + b = self.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 = (self.control_point_a.base.strength - self.control_point_b.base.strength) / 1.0 + position = middle_point.point_from_heading(attack_heading, + strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE) + return position diff --git a/theater/landmap.py b/theater/landmap.py index c5384da7..6eaaf5fe 100644 --- a/theater/landmap.py +++ b/theater/landmap.py @@ -1,11 +1,11 @@ import pickle -import typing +from typing import Collection, Optional, Tuple -Zone = typing.Collection[typing.Tuple[float, float]] -Landmap = typing.Tuple[typing.Collection[Zone], typing.Collection[Zone]] +Zone = Collection[Tuple[float, float]] +Landmap = Tuple[Collection[Zone], Collection[Zone]] -def load_landmap(filename: str) -> Landmap: +def load_landmap(filename: str) -> Optional[Landmap]: try: with open(filename, "rb") as f: return pickle.load(f) @@ -30,7 +30,7 @@ def poly_contains(x, y, poly): p1x, p1y = p2x, p2y return inside -def poly_centroid(poly) -> typing.Tuple[float, float]: +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) diff --git a/theater/missiontarget.py b/theater/missiontarget.py new file mode 100644 index 00000000..b0a30aa0 --- /dev/null +++ b/theater/missiontarget.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dcs.mapping import Point + + +class MissionTarget(ABC): + # TODO: These should just be required objects to the constructor + # The TheatherGroundObject class is difficult to modify because it's + # generated data that's pickled ahead of time. + @property + @abstractmethod + def name(self) -> str: + """The name of the mission target.""" + + @property + @abstractmethod + def position(self) -> Point: + """The location of the mission target.""" + + 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/nevada.py b/theater/nevada.py deleted file mode 100644 index a7b7d030..00000000 --- a/theater/nevada.py +++ /dev/null @@ -1,42 +0,0 @@ -from dcs.terrain import nevada -from dcs import mapping - -from .landmap import * -from .conflicttheater import * -from .base import * - - -class NevadaTheater(ConflictTheater): - terrain = dcs.terrain.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\\nev_landmap.p") - daytime_map = { - "dawn": (4, 6), - "day": (6, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(NevadaTheater, self).__init__() - - self.tonopah_test_range = ControlPoint.from_airport(nevada.Tonopah_Test_Range_Airfield, LAND, SIZE_SMALL,IMPORTANCE_LOW) - self.lincoln_conty = ControlPoint.from_airport(nevada.Lincoln_County, LAND, SIZE_SMALL, 1.2) - self.groom_lake = ControlPoint.from_airport(nevada.Groom_Lake_AFB, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.mesquite = ControlPoint.from_airport(nevada.Mesquite, LAND, SIZE_REGULAR, 1.3) - self.creech = ControlPoint.from_airport(nevada.Creech_AFB, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.nellis = ControlPoint.from_airport(nevada.Nellis_AFB, LAND, SIZE_BIG, IMPORTANCE_HIGH) - - self.add_controlpoint(self.tonopah_test_range, connected_to=[self.lincoln_conty, self.groom_lake]) - self.add_controlpoint(self.lincoln_conty, connected_to=[self.tonopah_test_range, self.mesquite]) - self.add_controlpoint(self.groom_lake, connected_to=[self.mesquite, self.creech, self.tonopah_test_range]) - - self.add_controlpoint(self.creech, connected_to=[self.groom_lake, self.nellis]) - self.add_controlpoint(self.mesquite, connected_to=[self.lincoln_conty, self.groom_lake]) - self.add_controlpoint(self.nellis, connected_to=[self.creech]) - - self.nellis.captured = True - self.tonopah_test_range.captured_invert = True - diff --git a/theater/normandy.py b/theater/normandy.py deleted file mode 100644 index dd67e9a6..00000000 --- a/theater/normandy.py +++ /dev/null @@ -1,83 +0,0 @@ -from dcs.terrain import normandy - -from .conflicttheater import * -from .landmap import * - - -class NormandyTheater(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(NormandyTheater, self).__init__() - - self.needOarPoint = ControlPoint.from_airport(normandy.Needs_Oar_Point, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.chailey = ControlPoint.from_airport(normandy.Chailey, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.deuxjumeaux = ControlPoint.from_airport(normandy.Deux_Jumeaux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lignerolles = ControlPoint.from_airport(normandy.Lignerolles, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.carpiquet = ControlPoint.from_airport(normandy.Carpiquet, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lessay = ControlPoint.from_airport(normandy.Lessay, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.maupertus = ControlPoint.from_airport(normandy.Maupertus, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.evreux = ControlPoint.from_airport(normandy.Evreux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.add_controlpoint(self.chailey, connected_to=[self.needOarPoint]) - self.add_controlpoint(self.needOarPoint, connected_to=[self.chailey]) - - self.add_controlpoint(self.deuxjumeaux, connected_to=[self.lignerolles]) - self.add_controlpoint(self.lignerolles, connected_to=[self.deuxjumeaux, self.lessay, self.carpiquet]) - self.add_controlpoint(self.lessay, connected_to=[self.lignerolles, self.maupertus]) - self.add_controlpoint(self.carpiquet, connected_to=[self.lignerolles, self.evreux]) - self.add_controlpoint(self.maupertus, connected_to=[self.lessay]) - self.add_controlpoint(self.evreux, connected_to=[self.carpiquet]) - - self.deuxjumeaux.captured = True - self.chailey.captured = True - self.needOarPoint.captured = True - - self.evreux.captured_invert = True - - -class NormandySmall(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(NormandySmall, self).__init__() - - self.needOarPoint = ControlPoint.from_airport(normandy.Needs_Oar_Point, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.deuxjumeaux = ControlPoint.from_airport(normandy.Deux_Jumeaux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lignerolles = ControlPoint.from_airport(normandy.Lignerolles, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.carpiquet = ControlPoint.from_airport(normandy.Carpiquet, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.evreux = ControlPoint.from_airport(normandy.Evreux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.add_controlpoint(self.needOarPoint, connected_to=[self.needOarPoint]) - - self.add_controlpoint(self.deuxjumeaux, connected_to=[self.lignerolles]) - self.add_controlpoint(self.lignerolles, connected_to=[self.deuxjumeaux, self.carpiquet]) - self.add_controlpoint(self.carpiquet, connected_to=[self.lignerolles, self.evreux]) - self.add_controlpoint(self.evreux, connected_to=[self.carpiquet]) - - self.deuxjumeaux.captured = True - self.needOarPoint.captured = True - - self.evreux.captured_invert = True diff --git a/theater/persiangulf.py b/theater/persiangulf.py deleted file mode 100644 index 0960c947..00000000 --- a/theater/persiangulf.py +++ /dev/null @@ -1,298 +0,0 @@ -from dcs.terrain import persiangulf -from dcs import mapping - -from .conflicttheater import * -from .base import * -from .landmap import load_landmap - - -class PersianGulfTheater(ConflictTheater): - terrain = dcs.terrain.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), - } - - - def __init__(self): - super(PersianGulfTheater, self).__init__() - - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, 1.1) - self.sir_abu_nuayr = ControlPoint.from_airport(persiangulf.Sir_Abu_Nuayr, [0, 330], SIZE_SMALL, 1.1,has_frontline=False) - self.dubai = ControlPoint.from_airport(persiangulf.Dubai_Intl, COAST_DL_E, SIZE_LARGE, IMPORTANCE_MEDIUM) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, 1.0) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, 1.0) - self.khasab = ControlPoint.from_airport(persiangulf.Khasab, LAND, SIZE_SMALL, IMPORTANCE_MEDIUM) - self.sirri = ControlPoint.from_airport(persiangulf.Sirri_Island, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_LOW,has_frontline=False) - self.abu_musa = ControlPoint.from_airport(persiangulf.Abu_Musa_Island_Airport, LAND, SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_island = ControlPoint.from_airport(persiangulf.Tunb_Island_AFB, [0, 270, 330], SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_kochak = ControlPoint.from_airport(persiangulf.Tunb_Kochak, [135, 180], SIZE_SMALL, 1.1,has_frontline=False) - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL,IMPORTANCE_HIGH) - self.qeshm = ControlPoint.from_airport(persiangulf.Qeshm_Island, [270, 315, 0, 45, 90, 135, 180], SIZE_SMALL,1.1, has_frontline=False) - self.havadarya = ControlPoint.from_airport(persiangulf.Havadarya, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_HIGH) - self.bandar_abbas = ControlPoint.from_airport(persiangulf.Bandar_Abbas_Intl, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_MEDIUM) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.liwa = ControlPoint.from_airport(persiangulf.Liwa_Airbase, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.bandar_e_jask = ControlPoint.from_airport(persiangulf.Bandar_e_Jask_airfield, LAND, SIZE_TINY,IMPORTANCE_LOW) - self.west_carrier = ControlPoint.carrier("West carrier", Point(-69043.813952358, -159916.65947136), 1001) - self.east_carrier = ControlPoint.carrier("East carrier", Point(59514.324335475, 28165.517980635), 1002) - - self.add_controlpoint(self.liwa, connected_to=[self.al_dhafra]) - self.add_controlpoint(self.al_dhafra, connected_to=[self.liwa, self.al_maktoum, self.al_ain]) - self.add_controlpoint(self.al_ain, connected_to=[self.al_dhafra, self.al_maktoum]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_dhafra, self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum, self.dubai]) - self.add_controlpoint(self.dubai, connected_to=[self.al_minhad, self.sharjah, self.fujairah]) - self.add_controlpoint(self.sharjah, connected_to=[self.dubai, self.ras_al_khaimah]) - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.sharjah, self.khasab]) - self.add_controlpoint(self.fujairah, connected_to=[self.dubai, self.khasab]) - self.add_controlpoint(self.khasab, connected_to=[self.ras_al_khaimah, self.fujairah]) - - self.add_controlpoint(self.sir_abu_nuayr, connected_to=[]) - self.add_controlpoint(self.sirri, connected_to=[]) - self.add_controlpoint(self.abu_musa, connected_to=[]) - self.add_controlpoint(self.tunb_kochak, connected_to=[]) - - self.add_controlpoint(self.tunb_island, connected_to=[]) - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar, self.qeshm]) - self.add_controlpoint(self.qeshm, connected_to=[self.bandar_lengeh, self.havadarya]) - self.add_controlpoint(self.havadarya, connected_to=[self.lar, self.qeshm, self.bandar_abbas]) - self.add_controlpoint(self.bandar_abbas, connected_to=[self.havadarya, self.kerman]) - - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.kerman, connected_to=[self.lar, self.shiraz, self.bandar_abbas]) - self.add_controlpoint(self.lar, connected_to=[self.havadarya, self.shiraz, self.kerman]) - - self.add_controlpoint(self.west_carrier) - self.add_controlpoint(self.east_carrier) - - self.west_carrier.captured = True - self.east_carrier.captured = True - self.liwa.captured = True - - self.west_carrier.captured_invert = True - self.east_carrier.captured_invert = True - self.shiraz.captured_invert = True - - -class IranianCampaign(ConflictTheater): - - terrain = dcs.terrain.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), - } - - def __init__(self): - super(IranianCampaign, self).__init__() - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, 1.1) - self.sir_abu_nuayr = ControlPoint.from_airport(persiangulf.Sir_Abu_Nuayr, [0, 330], SIZE_SMALL, 1.1,has_frontline=False) - self.dubai = ControlPoint.from_airport(persiangulf.Dubai_Intl, COAST_DL_E, SIZE_LARGE, IMPORTANCE_MEDIUM) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, 1.0) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, 1.0) - self.khasab = ControlPoint.from_airport(persiangulf.Khasab, LAND, SIZE_SMALL, IMPORTANCE_MEDIUM) - self.sirri = ControlPoint.from_airport(persiangulf.Sirri_Island, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_LOW,has_frontline=False) - self.abu_musa = ControlPoint.from_airport(persiangulf.Abu_Musa_Island_Airport, LAND, SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_island = ControlPoint.from_airport(persiangulf.Tunb_Island_AFB, [0, 270, 330], SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_kochak = ControlPoint.from_airport(persiangulf.Tunb_Kochak, [135, 180], SIZE_SMALL, 1.1,has_frontline=False) - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL,IMPORTANCE_HIGH) - self.qeshm = ControlPoint.from_airport(persiangulf.Qeshm_Island, [270, 315, 0, 45, 90, 135, 180], SIZE_SMALL,1.1, has_frontline=False) - self.havadarya = ControlPoint.from_airport(persiangulf.Havadarya, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_HIGH) - self.bandar_abbas = ControlPoint.from_airport(persiangulf.Bandar_Abbas_Intl, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.bandar_e_jask = ControlPoint.from_airport(persiangulf.Bandar_e_Jask_airfield, LAND, SIZE_TINY,IMPORTANCE_LOW) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_MEDIUM) - - self.east_carrier = ControlPoint.carrier("East carrier", Point(59514.324335475, 28165.517980635), 1001) - self.west_carrier = ControlPoint.lha("Tarawa", Point(-27500.813952358, -147000.65947136), 1002) - - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.khasab]) - self.add_controlpoint(self.khasab, connected_to=[self.ras_al_khaimah]) - - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar]) - self.add_controlpoint(self.qeshm, connected_to=[]) - self.add_controlpoint(self.havadarya, connected_to=[self.lar, self.bandar_abbas]) - self.add_controlpoint(self.bandar_abbas, connected_to=[self.havadarya, self.jiroft]) - - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.jiroft, connected_to=[self.kerman, self.bandar_abbas]) - self.add_controlpoint(self.kerman, connected_to=[self.lar, self.shiraz, self.jiroft]) - self.add_controlpoint(self.lar, connected_to=[self.bandar_lengeh, self.havadarya, self.shiraz, self.kerman]) - - self.add_controlpoint(self.east_carrier) - self.add_controlpoint(self.west_carrier) - - self.east_carrier.captured = True - self.west_carrier.captured = True - self.al_dhafra.captured = True - self.ras_al_khaimah.captured = True - self.khasab.captured = True - self.qeshm.captured = True - self.havadarya.captured = True - self.bandar_abbas.captured = True - - self.shiraz.captured_invert = True - - -class Emirates(ConflictTheater): - terrain = dcs.terrain.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), - } - - - def __init__(self): - super(Emirates, self).__init__() - - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_MEDIUM) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, IMPORTANCE_LOW) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_LOW) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_LOW) - - self.east_carrier = ControlPoint.carrier("Carrier", Point(-61770, 69039), 1001) - self.tarawa_carrier = ControlPoint.lha("LHA Carrier", Point(-79770, 49430), 1002) - - self.add_controlpoint(self.al_dhafra, connected_to=[self.al_ain, self.al_maktoum]) - self.add_controlpoint(self.al_ain, connected_to=[self.fujairah, self.al_maktoum, self.al_dhafra]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_dhafra, self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum, self.sharjah]) - self.add_controlpoint(self.sharjah, connected_to=[self.al_minhad, self.ras_al_khaimah, self.fujairah]) - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.sharjah]) - self.add_controlpoint(self.fujairah, connected_to=[self.sharjah, self.al_ain]) - - self.add_controlpoint(self.tarawa_carrier) - self.add_controlpoint(self.east_carrier) - - self.tarawa_carrier.captured = True - self.east_carrier.captured = True - self.fujairah.captured = True - - self.tarawa_carrier.captured_invert = True - self.east_carrier.captured_invert = True - self.fujairah.captured_invert = True - - -class DesertWar(ConflictTheater): - terrain = dcs.terrain.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), - } - - - def __init__(self): - super(DesertWar, self).__init__() - - self.liwa = ControlPoint.from_airport(persiangulf.Liwa_Airbase, LAND, SIZE_BIG, IMPORTANCE_MEDIUM) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(-124000, -303000), 1001) - self.tarawa_carrier = ControlPoint.lha("LHA Carrier", Point(-164000, -257000), 1002) - - self.add_controlpoint(self.liwa, connected_to=[self.al_ain]) - self.add_controlpoint(self.al_ain, connected_to=[self.al_maktoum, self.liwa]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum]) - - self.add_controlpoint(self.tarawa_carrier) - self.add_controlpoint(self.carrier) - - self.tarawa_carrier.captured = True - self.carrier.captured = True - self.liwa.captured = True - - self.tarawa_carrier.captured_invert = True - self.carrier.captured_invert = True - self.al_ain.captured_invert = True - - -class IranInvasionLite(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(IranInvasionLite, self).__init__() - - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.carrier = ControlPoint.carrier("Carrier", Point(72000.324335475, -376000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-27500.813952358, -147000.65947136), 1002) - - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar]) - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.jiroft, connected_to=[self.kerman]) - self.add_controlpoint(self.kerman, connected_to=[self.shiraz, self.jiroft]) - self.add_controlpoint(self.lar, connected_to=[self.bandar_lengeh, self.shiraz]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.carrier.captured = True - self.lha.captured = True - - self.shiraz.captured_invert = True - self.bandar_lengeh.captured = True - diff --git a/theater/start_generator.py b/theater/start_generator.py index b029ae92..b14f1b99 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -1,19 +1,35 @@ +import logging import math import pickle import random import typing -import logging +from dcs.mapping import Point +from dcs.task import CAP, CAS, PinpointStrike +from dcs.vehicles import AirDefence + +from game import db from game.data.building_data import DEFAULT_AVAILABLE_BUILDINGS from game.settings import Settings -from gen import namegen, TheaterGroundObject +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.fleet.ship_group_generator import ( + generate_carrier_group, + generate_lha_group, + generate_ship_group, +) from gen.missiles.missiles_group_generator import generate_missile_group -from gen.sam.sam_group_generator import generate_anti_air_group, generate_shorad_group -from theater import ControlPointType -from theater.base import * -from theater.conflicttheater import * +from gen.sam.sam_group_generator import ( + generate_anti_air_group, + generate_shorad_group, +) +from theater import ( + ConflictTheater, + ControlPoint, + ControlPointType, + TheaterGroundObject, +) +from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW UNIT_VARIETY = 3 UNIT_AMOUNT_FACTOR = 16 @@ -27,7 +43,7 @@ COUNT_BY_TASK = { } -def generate_inital_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float): +def generate_initial_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float): for cp in theater.enemy_points(): if cp.captured: continue diff --git a/theater/syria.py b/theater/syria.py deleted file mode 100644 index 1465f58e..00000000 --- a/theater/syria.py +++ /dev/null @@ -1,225 +0,0 @@ -from dcs.terrain import syria - -from .conflicttheater import * -from .landmap import * - - -class SyriaTheater(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(SyriaTheater, self).__init__() - - -class GolanHeights(SyriaTheater): - - def __init__(self): - super(GolanHeights, self).__init__() - - self.ramatDavid = ControlPoint.from_airport(syria.Ramat_David, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.khalkhala.allow_sea_units = False - self.ramatDavid.allow_sea_units = False - self.kinghussein.allow_sea_units = False - - self.marjruhayyil = ControlPoint.from_airport(syria.Marj_Ruhayyil, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.mezzeh = ControlPoint.from_airport(syria.Mezzeh, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.carrier = ControlPoint.carrier("Carrier", Point(-280000, -238000), 1001) - self.lha = ControlPoint.lha("LHA Carrier", Point(-237000, -89800), 1002) - - self.add_controlpoint(self.ramatDavid, connected_to=[self.khalkhala]) - self.add_controlpoint(self.khalkhala, connected_to=[self.ramatDavid, self.kinghussein, self.marjruhayyil]) - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala]) - self.add_controlpoint(self.marjruhayyil, connected_to=[self.khalkhala, self.mezzeh, self.aldumayr]) - self.add_controlpoint(self.mezzeh, connected_to=[self.marjruhayyil]) - self.add_controlpoint(self.aldumayr, connected_to=[self.marjruhayyil]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.ramatDavid.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.aldumayr.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class TurkishInvasion(SyriaTheater): - - def __init__(self): - super(TurkishInvasion, self).__init__() - - self.hatay = ControlPoint.from_airport(syria.Hatay, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.minakh = ControlPoint.from_airport(syria.Minakh, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.kuweires = ControlPoint.from_airport(syria.Kuweires, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(133000, -54000), 1001) - self.lha = ControlPoint.lha("LHA", Point(155000, -19000), 1002) - - self.add_controlpoint(self.incirlik, connected_to=[]) - self.add_controlpoint(self.hatay, connected_to=[self.minakh]) - self.add_controlpoint(self.minakh, connected_to=[self.aleppo, self.hatay]) - self.add_controlpoint(self.aleppo, connected_to=[self.kuweires, self.minakh]) - self.add_controlpoint(self.kuweires, connected_to=[self.jirah, self.aleppo]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa, self.kuweires]) - self.add_controlpoint(self.tabqa, connected_to=[self.jirah]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.incirlik.captured = True - self.hatay.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.tabqa.captured_invert = True - - -class SyrianCivilWar(SyriaTheater): - - def __init__(self): - super(SyrianCivilWar, self).__init__() - - self.basselAlAssad = ControlPoint.from_airport(syria.Bassel_Al_Assad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.marjruhayyil = ControlPoint.from_airport(syria.Marj_Ruhayyil, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.hama = ControlPoint.from_airport(syria.Hama, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.alqusair= ControlPoint.from_airport(syria.Al_Qusayr, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(18537, -52000), 1001) - self.lha = ControlPoint.lha("LHA", Point(116000, -30000), 1002) - - self.add_controlpoint(self.basselAlAssad, connected_to=[self.hama]) - self.add_controlpoint(self.marjruhayyil, connected_to=[self.aldumayr]) - - self.add_controlpoint(self.hama, connected_to=[self.basselAlAssad, self.aleppo, self.alqusair]) - self.add_controlpoint(self.aleppo, connected_to=[self.hama]) - self.add_controlpoint(self.alqusair, connected_to=[self.hama, self.aldumayr, self.palmyra]) - self.add_controlpoint(self.palmyra, connected_to=[self.alqusair]) - self.add_controlpoint(self.aldumayr, connected_to=[self.alqusair, self.marjruhayyil]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.basselAlAssad.captured = True - self.marjruhayyil.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.aleppo.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class InherentResolve(SyriaTheater): - - def __init__(self): - super(InherentResolve, self).__init__() - - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(-210000, -200000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-131000, -161000), 1002) - - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala]) - self.add_controlpoint(self.incirlik, connected_to=[self.incirlik]) - self.add_controlpoint(self.khalkhala, connected_to=[self.kinghussein, self.palmyra]) - self.add_controlpoint(self.palmyra, connected_to=[self.khalkhala, self.tabqa]) - self.add_controlpoint(self.tabqa, connected_to=[self.palmyra, self.jirah]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.kinghussein.captured = True - self.incirlik.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.jirah.captured_invert = True - self.incirlik.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class SyriaFullMap(SyriaTheater): - - def __init__(self): - super(SyriaFullMap, self).__init__() - - self.ramatDavid = ControlPoint.from_airport(syria.Ramat_David, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.hama = ControlPoint.from_airport(syria.Hama, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.alqusair= ControlPoint.from_airport(syria.Al_Qusayr, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.basselAlAssad = ControlPoint.from_airport(syria.Bassel_Al_Assad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.renemouawad = ControlPoint.from_airport(syria.Rene_Mouawad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.minakh = ControlPoint.from_airport(syria.Minakh, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.hatay = ControlPoint.from_airport(syria.Hatay, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - - - self.carrier = ControlPoint.carrier("Carrier", Point(-151000, -106000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-131000, -161000), 1002) - - self.add_controlpoint(self.ramatDavid, connected_to=[self.kinghussein]) - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala, self.ramatDavid]) - self.add_controlpoint(self.khalkhala, connected_to=[self.kinghussein, self.aldumayr]) - self.add_controlpoint(self.aldumayr, connected_to=[self.khalkhala, self.alqusair]) - self.add_controlpoint(self.alqusair, connected_to=[self.hama, self.aldumayr, self.palmyra, self.renemouawad]) - self.add_controlpoint(self.renemouawad, connected_to=[self.alqusair, self.basselAlAssad]) - self.add_controlpoint(self.hama, connected_to=[self.aleppo, self.alqusair, self.basselAlAssad]) - self.add_controlpoint(self.basselAlAssad, connected_to=[self.hama, self.hatay, self.renemouawad]) - self.add_controlpoint(self.palmyra, connected_to=[self.tabqa, self.alqusair]) - self.add_controlpoint(self.tabqa, connected_to=[self.palmyra, self.jirah]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa, self.aleppo]) - self.add_controlpoint(self.aleppo, connected_to=[self.hama, self.jirah, self.minakh]) - self.add_controlpoint(self.minakh, connected_to=[self.hatay, self.aleppo, self.incirlik]) - self.add_controlpoint(self.hatay, connected_to=[self.minakh, self.basselAlAssad]) - self.add_controlpoint(self.incirlik, connected_to=[self.minakh]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.ramatDavid.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.incirlik.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 42fc69f1..659e366e 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,5 +1,13 @@ -from dcs.mapping import Point import uuid +from typing import List, TYPE_CHECKING + +from dcs.mapping import Point +from dcs.unitgroup import Group + +if TYPE_CHECKING: + from .conflicttheater import ConflictTheater + from .controlpoint import ControlPoint +from .missiontarget import MissionTarget NAME_BY_CATEGORY = { "power": "Power plant", @@ -59,7 +67,7 @@ CATEGORY_MAP = { } -class TheaterGroundObject: +class TheaterGroundObject(MissionTarget): cp_id = 0 group_id = 0 object_id = 0 @@ -68,7 +76,7 @@ class TheaterGroundObject: airbase_group = False heading = 0 position = None # type: Point - groups = [] + groups: List[Group] = [] obj_name = "" sea_object = False uuid = uuid.uuid1() @@ -93,3 +101,15 @@ class TheaterGroundObject: def matches_string_identifier(self, id): return self.string_identifier == id + + @property + def name(self) -> str: + return self.obj_name + + def parent_control_point( + self, theater: "ConflictTheater") -> "ControlPoint": + """Searches the theater for the parent control point.""" + for cp in theater.controlpoints: + if cp.id == self.cp_id: + return cp + raise RuntimeError("Could not find matching control point in theater") diff --git a/theater/thechannel.py b/theater/thechannel.py deleted file mode 100644 index b37bbca3..00000000 --- a/theater/thechannel.py +++ /dev/null @@ -1,109 +0,0 @@ -from dcs.terrain import thechannel - -from .conflicttheater import * -from .landmap import * - - -class Dunkirk(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(Dunkirk, self).__init__() - - self.abeville = ControlPoint.from_airport(thechannel.Abbeville_Drucat, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.detling = ControlPoint.from_airport(thechannel.Detling, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.stomer = ControlPoint.from_airport(thechannel.Saint_Omer_Longuenesse, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.dunkirk = ControlPoint.from_airport(thechannel.Dunkirk_Mardyck, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.hawkinge = ControlPoint.from_airport(thechannel.Hawkinge, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.highhalden = ControlPoint.from_airport(thechannel.High_Halden, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lympne = ControlPoint.from_airport(thechannel.Lympne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.manston = ControlPoint.from_airport(thechannel.Manston, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.merville = ControlPoint.from_airport(thechannel.Merville_Calonne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - - # England - self.add_controlpoint(self.hawkinge, connected_to=[self.lympne, self.manston]) - self.add_controlpoint(self.lympne, connected_to=[self.hawkinge]) - self.add_controlpoint(self.manston, connected_to=[self.hawkinge]) - - # France - self.add_controlpoint(self.dunkirk, connected_to=[self.stomer]) - self.add_controlpoint(self.stomer, connected_to=[self.dunkirk, self.merville, self.abeville]) - self.add_controlpoint(self.merville, connected_to=[self.stomer]) - self.add_controlpoint(self.abeville, connected_to=[self.stomer]) - - #self.detling.captured = True - self.hawkinge.captured = True - self.dunkirk.captured = True - #self.highhalden.captured = True - self.lympne.captured = True - self.manston.captured = True - - self.manston.captured_invert = True - self.dunkirk.captured_invert = True - self.stomer.captured_invert = True - self.merville.captured_invert = True - self.abeville.captured_invert = True - - -class BattleOfBritain(ConflictTheater): - terrain = dcs.terrain.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), - } - - def __init__(self): - super(BattleOfBritain, self).__init__() - - self.abeville = ControlPoint.from_airport(thechannel.Abbeville_Drucat, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.detling = ControlPoint.from_airport(thechannel.Detling, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.stomer = ControlPoint.from_airport(thechannel.Saint_Omer_Longuenesse, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.dunkirk = ControlPoint.from_airport(thechannel.Dunkirk_Mardyck, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.hawkinge = ControlPoint.from_airport(thechannel.Hawkinge, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.highhalden = ControlPoint.from_airport(thechannel.High_Halden, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lympne = ControlPoint.from_airport(thechannel.Lympne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.manston = ControlPoint.from_airport(thechannel.Manston, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.merville = ControlPoint.from_airport(thechannel.Merville_Calonne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - # England - self.add_controlpoint(self.hawkinge, connected_to=[self.lympne, self.manston]) - self.add_controlpoint(self.lympne, connected_to=[self.hawkinge, self.highhalden]) - self.add_controlpoint(self.manston, connected_to=[self.hawkinge]) - self.add_controlpoint(self.highhalden, connected_to=[self.lympne]) - - # France - self.add_controlpoint(self.dunkirk, connected_to=[self.stomer]) - self.add_controlpoint(self.stomer, connected_to=[self.dunkirk, self.merville, self.abeville]) - self.add_controlpoint(self.merville, connected_to=[self.stomer]) - self.add_controlpoint(self.abeville, connected_to=[self.stomer]) - - #self.detling.captured = True - self.hawkinge.captured = True - #self.dunkirk.captured = True - self.highhalden.captured = True - self.lympne.captured = True - self.manston.captured = True - - self.dunkirk.captured_invert = True - self.stomer.captured_invert = True - self.merville.captured_invert = True - self.abeville.captured_invert = True diff --git a/userdata/__init__.py b/userdata/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/userdata/logging_config.py b/userdata/logging_config.py deleted file mode 100644 index 5febea7d..00000000 --- a/userdata/logging_config.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging -import os -from logging.handlers import RotatingFileHandler - - -def init_logging(version_string): - if not os.path.isdir("./logs"): - os.mkdir("logs") - - logging.basicConfig(level="DEBUG") - logger = logging.getLogger() - - formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') - - handler = RotatingFileHandler('./logs/liberation.log', 'a', 5000000, 1) - handler.setLevel(logging.INFO) - handler.setFormatter(formatter) - - stream_handler = logging.StreamHandler() - stream_handler.setLevel(logging.DEBUG) - stream_handler.setFormatter(formatter) - - logger.addHandler(stream_handler) - logger.addHandler(handler) - - logger.info("DCS Liberation {}".format(version_string)) diff --git a/userdata/state.py b/userdata/state.py deleted file mode 100644 index 846f5893..00000000 --- a/userdata/state.py +++ /dev/null @@ -1,10 +0,0 @@ - - -class RunningMissionState: - - killed_aircrafts = [] - killed_ground_units = [] - weapons_fired = [] - - def __init__(self): - pass \ No newline at end of file