From 6c7b62b8b19a317f2901bf3b31531c5e0061f47c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 10 Aug 2021 17:58:47 -0700 Subject: [PATCH] Move MizCampaignLoader out of conflicttheater.py. Also removes the useless `size` and `importance` fields from `ControlPoint`. --- game/theater/conflicttheater.py | 491 +----------------- game/theater/controlpoint.py | 27 +- game/theater/mizcampaignloader.py | 466 +++++++++++++++++ ...bu_dhabi.json => battle_of_abu_dhabi.yaml} | 0 4 files changed, 469 insertions(+), 515 deletions(-) create mode 100644 game/theater/mizcampaignloader.py rename resources/campaigns/{battle_of_abu_dhabi.json => battle_of_abu_dhabi.yaml} (100%) diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index bdae60e6..c87d8c77 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -1,27 +1,11 @@ from __future__ import annotations -import itertools import math from dataclasses import dataclass -from functools import cached_property from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING -from dcs import Mission -from dcs.countries import ( - CombinedJointTaskForcesBlue, - CombinedJointTaskForcesRed, -) -from dcs.country import Country from dcs.mapping import Point -from dcs.planes import F_15C -from dcs.ships import ( - HandyWind, - Stennis, - USS_Arleigh_Burke_IIa, - LHA_Tarawa, -) -from dcs.statics import Fortification, Warehouse from dcs.terrain import ( caucasus, nevada, @@ -31,496 +15,25 @@ from dcs.terrain import ( thechannel, marianaislands, ) -from dcs.terrain.terrain import Airport, Terrain -from dcs.unitgroup import ( - ShipGroup, - StaticGroup, - VehicleGroup, - PlaneGroup, -) -from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed +from dcs.terrain.terrain import Terrain from pyproj import CRS, Transformer from shapely import geometry, ops from .controlpoint import ( - Airfield, - Carrier, ControlPoint, - Fob, - Lha, MissionTarget, - OffMapSpawn, ) +from .mizcampaignloader import MizCampaignLoader from .seasonalconditions import SeasonalConditions from .frontline import FrontLine from .landmap import Landmap, load_landmap, poly_contains from .latlon import LatLon from .projections import TransverseMercator -from ..point_with_heading import PointWithHeading -from ..positioned import Positioned from ..profiling import logged_duration -from ..scenery_group import SceneryGroup -from ..utils import Distance, Heading, meters if TYPE_CHECKING: from . import TheaterGroundObject -SIZE_TINY = 150 -SIZE_SMALL = 600 -SIZE_REGULAR = 1000 -SIZE_BIG = 2000 -SIZE_LARGE = 3000 - -IMPORTANCE_LOW = 1 -IMPORTANCE_MEDIUM = 1.2 -IMPORTANCE_HIGH = 1.4 - - -class MizCampaignLoader: - BLUE_COUNTRY = CombinedJointTaskForcesBlue() - RED_COUNTRY = CombinedJointTaskForcesRed() - - OFF_MAP_UNIT_TYPE = F_15C.id - - CV_UNIT_TYPE = Stennis.id - LHA_UNIT_TYPE = LHA_Tarawa.id - FRONT_LINE_UNIT_TYPE = Armor.M_113.id - SHIPPING_LANE_UNIT_TYPE = HandyWind.id - - FOB_UNIT_TYPE = Unarmed.SKP_11.id - FARP_HELIPAD = "SINGLE_HELIPAD" - - OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id - SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id - MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id - COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id - - # Multiple options for air defenses so campaign designers can more accurately see - # the coverage of their IADS for the expected type. - LONG_RANGE_SAM_UNIT_TYPES = { - AirDefence.Patriot_ln.id, - AirDefence.S_300PS_5P85C_ln.id, - AirDefence.S_300PS_5P85D_ln.id, - } - - MEDIUM_RANGE_SAM_UNIT_TYPES = { - AirDefence.Hawk_ln.id, - AirDefence.S_75M_Volhov.id, - AirDefence._5p73_s_125_ln.id, - } - - SHORT_RANGE_SAM_UNIT_TYPES = { - AirDefence.M1097_Avenger.id, - AirDefence.Rapier_fsa_launcher.id, - AirDefence._2S6_Tunguska.id, - AirDefence.Strela_1_9P31.id, - } - - AAA_UNIT_TYPES = { - AirDefence.Flak18.id, - AirDefence.Vulcan.id, - AirDefence.ZSU_23_4_Shilka.id, - } - - EWR_UNIT_TYPE = AirDefence._1L13_EWR.id - - ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id - - FACTORY_UNIT_TYPE = Fortification.Workshop_A.id - - AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id - - STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id - - def __init__(self, miz: Path, theater: ConflictTheater) -> None: - self.theater = theater - self.mission = Mission() - with logged_duration("Loading miz"): - self.mission.load_file(str(miz)) - self.control_point_id = itertools.count(1000) - - # If there are no red carriers there usually aren't red units. Make sure - # both countries are initialized so we don't have to deal with None. - if self.mission.country(self.BLUE_COUNTRY.name) is None: - self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY) - if self.mission.country(self.RED_COUNTRY.name) is None: - self.mission.coalition["red"].add_country(self.RED_COUNTRY) - - @staticmethod - def control_point_from_airport(airport: Airport) -> ControlPoint: - - # The wiki says this is a legacy property and to just use regular. - size = SIZE_REGULAR - - # The importance is taken from the periodicity of the airport's - # warehouse divided by 10. 30 is the default, and out of range (valid - # values are between 1.0 and 1.4). If it is used, pick the default - # importance. - if airport.periodicity == 30: - importance = IMPORTANCE_MEDIUM - else: - importance = airport.periodicity / 10 - - cp = Airfield(airport, size, importance) - cp.captured = airport.is_blue() - - # Use the unlimited aircraft option to determine if an airfield should - # be owned by the player when the campaign is "inverted". - cp.captured_invert = airport.unlimited_aircrafts - - return cp - - def country(self, blue: bool) -> Country: - country = self.mission.country( - self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name - ) - # Should be guaranteed because we initialized them. - assert country - return country - - @property - def blue(self) -> Country: - return self.country(blue=True) - - @property - def red(self) -> Country: - return self.country(blue=False) - - def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]: - for group in self.country(blue).plane_group: - if group.units[0].type == self.OFF_MAP_UNIT_TYPE: - yield group - - def carriers(self, blue: bool) -> Iterator[ShipGroup]: - for group in self.country(blue).ship_group: - if group.units[0].type == self.CV_UNIT_TYPE: - yield group - - def lhas(self, blue: bool) -> Iterator[ShipGroup]: - for group in self.country(blue).ship_group: - if group.units[0].type == self.LHA_UNIT_TYPE: - yield group - - def fobs(self, blue: bool) -> Iterator[VehicleGroup]: - for group in self.country(blue).vehicle_group: - if group.units[0].type == self.FOB_UNIT_TYPE: - yield group - - @property - def ships(self) -> Iterator[ShipGroup]: - for group in self.red.ship_group: - if group.units[0].type == self.SHIP_UNIT_TYPE: - yield group - - @property - def offshore_strike_targets(self) -> Iterator[StaticGroup]: - for group in self.red.static_group: - if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: - yield group - - @property - def missile_sites(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: - yield group - - @property - def coastal_defenses(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: - yield group - - @property - def long_range_sams(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES: - yield group - - @property - def medium_range_sams(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES: - yield group - - @property - def short_range_sams(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES: - yield group - - @property - def aaa(self) -> Iterator[VehicleGroup]: - for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): - if group.units[0].type in self.AAA_UNIT_TYPES: - yield group - - @property - def ewrs(self) -> Iterator[VehicleGroup]: - for group in self.red.vehicle_group: - if group.units[0].type in self.EWR_UNIT_TYPE: - yield group - - @property - def armor_groups(self) -> Iterator[VehicleGroup]: - for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): - if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE: - yield group - - @property - def helipads(self) -> Iterator[StaticGroup]: - for group in self.blue.static_group: - if group.units[0].type == self.FARP_HELIPAD: - yield group - - @property - def factories(self) -> Iterator[StaticGroup]: - for group in self.blue.static_group: - if group.units[0].type in self.FACTORY_UNIT_TYPE: - yield group - - @property - def ammunition_depots(self) -> Iterator[StaticGroup]: - for group in itertools.chain(self.blue.static_group, self.red.static_group): - if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE: - yield group - - @property - def strike_targets(self) -> Iterator[StaticGroup]: - for group in itertools.chain(self.blue.static_group, self.red.static_group): - if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE: - yield group - - @property - def scenery(self) -> List[SceneryGroup]: - return SceneryGroup.from_trigger_zones(self.mission.triggers._zones) - - @cached_property - def control_points(self) -> Dict[int, ControlPoint]: - control_points = {} - for airport in self.mission.terrain.airport_list(): - if airport.is_blue() or airport.is_red(): - control_point = self.control_point_from_airport(airport) - control_points[control_point.id] = control_point - - for blue in (False, True): - for group in self.off_map_spawns(blue): - control_point = OffMapSpawn( - next(self.control_point_id), str(group.name), group.position - ) - control_point.captured = blue - control_point.captured_invert = group.late_activation - control_points[control_point.id] = control_point - for ship in self.carriers(blue): - # TODO: Name the carrier. - control_point = Carrier( - "carrier", ship.position, next(self.control_point_id) - ) - control_point.captured = blue - control_point.captured_invert = ship.late_activation - control_points[control_point.id] = control_point - for ship in self.lhas(blue): - # TODO: Name the LHA.db - control_point = Lha("lha", ship.position, next(self.control_point_id)) - control_point.captured = blue - control_point.captured_invert = ship.late_activation - control_points[control_point.id] = control_point - for fob in self.fobs(blue): - control_point = Fob( - str(fob.name), fob.position, next(self.control_point_id) - ) - control_point.captured = blue - control_point.captured_invert = fob.late_activation - control_points[control_point.id] = control_point - - return control_points - - @property - def front_line_path_groups(self) -> Iterator[VehicleGroup]: - for group in self.country(blue=True).vehicle_group: - if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: - yield group - - @property - def shipping_lane_groups(self) -> Iterator[ShipGroup]: - for group in self.country(blue=True).ship_group: - if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: - yield group - - def add_supply_routes(self) -> None: - for group in self.front_line_path_groups: - # The unit will have its first waypoint at the source CP and the final - # waypoint at the destination CP. Each waypoint defines the path of the - # cargo ship. - waypoints = [p.position for p in group.points] - origin = self.theater.closest_control_point(waypoints[0]) - if origin is None: - raise RuntimeError( - f"No control point near the first waypoint of {group.name}" - ) - destination = self.theater.closest_control_point(waypoints[-1]) - if destination is None: - raise RuntimeError( - f"No control point near the final waypoint of {group.name}" - ) - - self.control_points[origin.id].create_convoy_route(destination, waypoints) - self.control_points[destination.id].create_convoy_route( - origin, list(reversed(waypoints)) - ) - - def add_shipping_lanes(self) -> None: - for group in self.shipping_lane_groups: - # The unit will have its first waypoint at the source CP and the final - # waypoint at the destination CP. Each waypoint defines the path of the - # cargo ship. - waypoints = [p.position for p in group.points] - origin = self.theater.closest_control_point(waypoints[0]) - if origin is None: - raise RuntimeError( - f"No control point near the first waypoint of {group.name}" - ) - destination = self.theater.closest_control_point(waypoints[-1]) - if destination is None: - raise RuntimeError( - f"No control point near the final waypoint of {group.name}" - ) - - self.control_points[origin.id].create_shipping_lane(destination, waypoints) - self.control_points[destination.id].create_shipping_lane( - origin, list(reversed(waypoints)) - ) - - def objective_info( - self, near: Positioned, allow_naval: bool = False - ) -> Tuple[ControlPoint, Distance]: - closest = self.theater.closest_control_point(near.position, allow_naval) - distance = meters(closest.position.distance_to_point(near.position)) - return closest, distance - - def add_preset_locations(self) -> None: - for static in self.offshore_strike_targets: - closest, distance = self.objective_info(static) - closest.preset_locations.offshore_strike_locations.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) - - for ship in self.ships: - closest, distance = self.objective_info(ship, allow_naval=True) - closest.preset_locations.ships.append( - PointWithHeading.from_point( - ship.position, Heading.from_degrees(ship.units[0].heading) - ) - ) - - for group in self.missile_sites: - closest, distance = self.objective_info(group) - closest.preset_locations.missile_sites.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.coastal_defenses: - closest, distance = self.objective_info(group) - closest.preset_locations.coastal_defenses.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.long_range_sams: - closest, distance = self.objective_info(group) - closest.preset_locations.long_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.medium_range_sams: - closest, distance = self.objective_info(group) - closest.preset_locations.medium_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.short_range_sams: - closest, distance = self.objective_info(group) - closest.preset_locations.short_range_sams.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.aaa: - closest, distance = self.objective_info(group) - closest.preset_locations.aaa.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.ewrs: - closest, distance = self.objective_info(group) - closest.preset_locations.ewrs.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for group in self.armor_groups: - closest, distance = self.objective_info(group) - closest.preset_locations.armor_groups.append( - PointWithHeading.from_point( - group.position, Heading.from_degrees(group.units[0].heading) - ) - ) - - for static in self.helipads: - closest, distance = self.objective_info(static) - closest.helipads.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) - - for static in self.factories: - closest, distance = self.objective_info(static) - closest.preset_locations.factories.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) - - for static in self.ammunition_depots: - closest, distance = self.objective_info(static) - closest.preset_locations.ammunition_depots.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) - - for static in self.strike_targets: - closest, distance = self.objective_info(static) - closest.preset_locations.strike_locations.append( - PointWithHeading.from_point( - static.position, Heading.from_degrees(static.units[0].heading) - ) - ) - - for scenery_group in self.scenery: - closest, distance = self.objective_info(scenery_group) - closest.preset_locations.scenery.append(scenery_group) - - def populate_theater(self) -> None: - for control_point in self.control_points.values(): - self.theater.add_controlpoint(control_point) - self.add_preset_locations() - self.add_supply_routes() - self.add_shipping_lanes() - @dataclass class ReferencePoint: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 12786d32..f0c5286b 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -293,8 +293,6 @@ class ControlPoint(MissionTarget, ABC): name: str, position: Point, at: db.StartingPosition, - size: int, - importance: float, has_frontline: bool = True, cptype: ControlPointType = ControlPointType.AIRBASE, ) -> None: @@ -307,9 +305,6 @@ class ControlPoint(MissionTarget, ABC): self.preset_locations = PresetLocations() self.helipads: List[PointWithHeading] = [] - # TODO: Should be Airbase specific. - self.size = size - self.importance = importance self.captured = False self.captured_invert = False # TODO: Should be Airbase specific. @@ -791,16 +786,12 @@ class ControlPoint(MissionTarget, ABC): class Airfield(ControlPoint): - def __init__( - self, airport: Airport, size: int, importance: float, has_frontline: bool = True - ) -> None: + def __init__(self, airport: Airport, has_frontline: bool = True) -> None: super().__init__( airport.id, airport.name, airport.position, airport, - size, - importance, has_frontline, cptype=ControlPointType.AIRBASE, ) @@ -966,15 +957,11 @@ class NavalControlPoint(ControlPoint, ABC): class Carrier(NavalControlPoint): def __init__(self, name: str, at: Point, cp_id: int): - import game.theater.conflicttheater - super().__init__( cp_id, name, at, at, - game.theater.conflicttheater.SIZE_SMALL, - 1, has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, ) @@ -1010,15 +997,11 @@ class Carrier(NavalControlPoint): class Lha(NavalControlPoint): def __init__(self, name: str, at: Point, cp_id: int): - import game.theater.conflicttheater - super().__init__( cp_id, name, at, at, - game.theater.conflicttheater.SIZE_SMALL, - 1, has_frontline=False, cptype=ControlPointType.LHA_GROUP, ) @@ -1047,15 +1030,11 @@ class OffMapSpawn(ControlPoint): return True def __init__(self, cp_id: int, name: str, position: Point): - from . import IMPORTANCE_MEDIUM, SIZE_REGULAR - super().__init__( cp_id, name, position, at=position, - size=SIZE_REGULAR, - importance=IMPORTANCE_MEDIUM, has_frontline=False, cptype=ControlPointType.OFF_MAP, ) @@ -1104,15 +1083,11 @@ class OffMapSpawn(ControlPoint): class Fob(ControlPoint): def __init__(self, name: str, at: Point, cp_id: int): - import game.theater.conflicttheater - super().__init__( cp_id, name, at, at, - game.theater.conflicttheater.SIZE_SMALL, - 1, has_frontline=True, cptype=ControlPointType.FOB, ) diff --git a/game/theater/mizcampaignloader.py b/game/theater/mizcampaignloader.py new file mode 100644 index 00000000..a2565029 --- /dev/null +++ b/game/theater/mizcampaignloader.py @@ -0,0 +1,466 @@ +from __future__ import annotations + +import itertools +from functools import cached_property +from pathlib import Path +from typing import Iterator, List, Dict, Tuple, TYPE_CHECKING + +from dcs import Mission +from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed +from dcs.country import Country +from dcs.planes import F_15C +from dcs.ships import Stennis, LHA_Tarawa, HandyWind, USS_Arleigh_Burke_IIa +from dcs.statics import Fortification, Warehouse +from dcs.terrain import Airport +from dcs.unitgroup import PlaneGroup, ShipGroup, VehicleGroup, StaticGroup +from dcs.vehicles import Armor, Unarmed, MissilesSS, AirDefence + +from game.point_with_heading import PointWithHeading +from game.positioned import Positioned +from game.profiling import logged_duration +from game.scenery_group import SceneryGroup +from game.utils import Distance, meters, Heading +from .controlpoint import ( + Airfield, + Carrier, + ControlPoint, + Fob, + Lha, + OffMapSpawn, +) + +if TYPE_CHECKING: + from .conflicttheater import ConflictTheater + + +class MizCampaignLoader: + BLUE_COUNTRY = CombinedJointTaskForcesBlue() + RED_COUNTRY = CombinedJointTaskForcesRed() + + OFF_MAP_UNIT_TYPE = F_15C.id + + CV_UNIT_TYPE = Stennis.id + LHA_UNIT_TYPE = LHA_Tarawa.id + FRONT_LINE_UNIT_TYPE = Armor.M_113.id + SHIPPING_LANE_UNIT_TYPE = HandyWind.id + + FOB_UNIT_TYPE = Unarmed.SKP_11.id + FARP_HELIPAD = "SINGLE_HELIPAD" + + OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id + SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id + MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id + COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id + + # Multiple options for air defenses so campaign designers can more accurately see + # the coverage of their IADS for the expected type. + LONG_RANGE_SAM_UNIT_TYPES = { + AirDefence.Patriot_ln.id, + AirDefence.S_300PS_5P85C_ln.id, + AirDefence.S_300PS_5P85D_ln.id, + } + + MEDIUM_RANGE_SAM_UNIT_TYPES = { + AirDefence.Hawk_ln.id, + AirDefence.S_75M_Volhov.id, + AirDefence._5p73_s_125_ln.id, + } + + SHORT_RANGE_SAM_UNIT_TYPES = { + AirDefence.M1097_Avenger.id, + AirDefence.Rapier_fsa_launcher.id, + AirDefence._2S6_Tunguska.id, + AirDefence.Strela_1_9P31.id, + } + + AAA_UNIT_TYPES = { + AirDefence.Flak18.id, + AirDefence.Vulcan.id, + AirDefence.ZSU_23_4_Shilka.id, + } + + EWR_UNIT_TYPE = AirDefence._1L13_EWR.id + + ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id + + FACTORY_UNIT_TYPE = Fortification.Workshop_A.id + + AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id + + STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id + + def __init__(self, miz: Path, theater: ConflictTheater) -> None: + self.theater = theater + self.mission = Mission() + with logged_duration("Loading miz"): + self.mission.load_file(str(miz)) + self.control_point_id = itertools.count(1000) + + # If there are no red carriers there usually aren't red units. Make sure + # both countries are initialized so we don't have to deal with None. + if self.mission.country(self.BLUE_COUNTRY.name) is None: + self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY) + if self.mission.country(self.RED_COUNTRY.name) is None: + self.mission.coalition["red"].add_country(self.RED_COUNTRY) + + @staticmethod + def control_point_from_airport(airport: Airport) -> ControlPoint: + cp = Airfield(airport) + cp.captured = airport.is_blue() + + # Use the unlimited aircraft option to determine if an airfield should + # be owned by the player when the campaign is "inverted". + cp.captured_invert = airport.unlimited_aircrafts + + return cp + + def country(self, blue: bool) -> Country: + country = self.mission.country( + self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name + ) + # Should be guaranteed because we initialized them. + assert country + return country + + @property + def blue(self) -> Country: + return self.country(blue=True) + + @property + def red(self) -> Country: + return self.country(blue=False) + + def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]: + for group in self.country(blue).plane_group: + if group.units[0].type == self.OFF_MAP_UNIT_TYPE: + yield group + + def carriers(self, blue: bool) -> Iterator[ShipGroup]: + for group in self.country(blue).ship_group: + if group.units[0].type == self.CV_UNIT_TYPE: + yield group + + def lhas(self, blue: bool) -> Iterator[ShipGroup]: + for group in self.country(blue).ship_group: + if group.units[0].type == self.LHA_UNIT_TYPE: + yield group + + def fobs(self, blue: bool) -> Iterator[VehicleGroup]: + for group in self.country(blue).vehicle_group: + if group.units[0].type == self.FOB_UNIT_TYPE: + yield group + + @property + def ships(self) -> Iterator[ShipGroup]: + for group in self.red.ship_group: + if group.units[0].type == self.SHIP_UNIT_TYPE: + yield group + + @property + def offshore_strike_targets(self) -> Iterator[StaticGroup]: + for group in self.red.static_group: + if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: + yield group + + @property + def missile_sites(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: + yield group + + @property + def coastal_defenses(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: + yield group + + @property + def long_range_sams(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES: + yield group + + @property + def medium_range_sams(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES: + yield group + + @property + def short_range_sams(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES: + yield group + + @property + def aaa(self) -> Iterator[VehicleGroup]: + for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): + if group.units[0].type in self.AAA_UNIT_TYPES: + yield group + + @property + def ewrs(self) -> Iterator[VehicleGroup]: + for group in self.red.vehicle_group: + if group.units[0].type in self.EWR_UNIT_TYPE: + yield group + + @property + def armor_groups(self) -> Iterator[VehicleGroup]: + for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): + if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE: + yield group + + @property + def helipads(self) -> Iterator[StaticGroup]: + for group in self.blue.static_group: + if group.units[0].type == self.FARP_HELIPAD: + yield group + + @property + def factories(self) -> Iterator[StaticGroup]: + for group in self.blue.static_group: + if group.units[0].type in self.FACTORY_UNIT_TYPE: + yield group + + @property + def ammunition_depots(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE: + yield group + + @property + def strike_targets(self) -> Iterator[StaticGroup]: + for group in itertools.chain(self.blue.static_group, self.red.static_group): + if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE: + yield group + + @property + def scenery(self) -> List[SceneryGroup]: + return SceneryGroup.from_trigger_zones(self.mission.triggers._zones) + + @cached_property + def control_points(self) -> Dict[int, ControlPoint]: + control_points = {} + for airport in self.mission.terrain.airport_list(): + if airport.is_blue() or airport.is_red(): + control_point = self.control_point_from_airport(airport) + control_points[control_point.id] = control_point + + for blue in (False, True): + for group in self.off_map_spawns(blue): + control_point = OffMapSpawn( + next(self.control_point_id), str(group.name), group.position + ) + control_point.captured = blue + control_point.captured_invert = group.late_activation + control_points[control_point.id] = control_point + for ship in self.carriers(blue): + # TODO: Name the carrier. + control_point = Carrier( + "carrier", ship.position, next(self.control_point_id) + ) + control_point.captured = blue + control_point.captured_invert = ship.late_activation + control_points[control_point.id] = control_point + for ship in self.lhas(blue): + # TODO: Name the LHA.db + control_point = Lha("lha", ship.position, next(self.control_point_id)) + control_point.captured = blue + control_point.captured_invert = ship.late_activation + control_points[control_point.id] = control_point + for fob in self.fobs(blue): + control_point = Fob( + str(fob.name), fob.position, next(self.control_point_id) + ) + control_point.captured = blue + control_point.captured_invert = fob.late_activation + control_points[control_point.id] = control_point + + return control_points + + @property + def front_line_path_groups(self) -> Iterator[VehicleGroup]: + for group in self.country(blue=True).vehicle_group: + if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: + yield group + + @property + def shipping_lane_groups(self) -> Iterator[ShipGroup]: + for group in self.country(blue=True).ship_group: + if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: + yield group + + def add_supply_routes(self) -> None: + for group in self.front_line_path_groups: + # The unit will have its first waypoint at the source CP and the final + # waypoint at the destination CP. Each waypoint defines the path of the + # cargo ship. + waypoints = [p.position for p in group.points] + origin = self.theater.closest_control_point(waypoints[0]) + if origin is None: + raise RuntimeError( + f"No control point near the first waypoint of {group.name}" + ) + destination = self.theater.closest_control_point(waypoints[-1]) + if destination is None: + raise RuntimeError( + f"No control point near the final waypoint of {group.name}" + ) + + self.control_points[origin.id].create_convoy_route(destination, waypoints) + self.control_points[destination.id].create_convoy_route( + origin, list(reversed(waypoints)) + ) + + def add_shipping_lanes(self) -> None: + for group in self.shipping_lane_groups: + # The unit will have its first waypoint at the source CP and the final + # waypoint at the destination CP. Each waypoint defines the path of the + # cargo ship. + waypoints = [p.position for p in group.points] + origin = self.theater.closest_control_point(waypoints[0]) + if origin is None: + raise RuntimeError( + f"No control point near the first waypoint of {group.name}" + ) + destination = self.theater.closest_control_point(waypoints[-1]) + if destination is None: + raise RuntimeError( + f"No control point near the final waypoint of {group.name}" + ) + + self.control_points[origin.id].create_shipping_lane(destination, waypoints) + self.control_points[destination.id].create_shipping_lane( + origin, list(reversed(waypoints)) + ) + + def objective_info( + self, near: Positioned, allow_naval: bool = False + ) -> Tuple[ControlPoint, Distance]: + closest = self.theater.closest_control_point(near.position, allow_naval) + distance = meters(closest.position.distance_to_point(near.position)) + return closest, distance + + def add_preset_locations(self) -> None: + for static in self.offshore_strike_targets: + closest, distance = self.objective_info(static) + closest.preset_locations.offshore_strike_locations.append( + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) + ) + + for ship in self.ships: + closest, distance = self.objective_info(ship, allow_naval=True) + closest.preset_locations.ships.append( + PointWithHeading.from_point( + ship.position, Heading.from_degrees(ship.units[0].heading) + ) + ) + + for group in self.missile_sites: + closest, distance = self.objective_info(group) + closest.preset_locations.missile_sites.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.coastal_defenses: + closest, distance = self.objective_info(group) + closest.preset_locations.coastal_defenses.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.long_range_sams: + closest, distance = self.objective_info(group) + closest.preset_locations.long_range_sams.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.medium_range_sams: + closest, distance = self.objective_info(group) + closest.preset_locations.medium_range_sams.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.short_range_sams: + closest, distance = self.objective_info(group) + closest.preset_locations.short_range_sams.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.aaa: + closest, distance = self.objective_info(group) + closest.preset_locations.aaa.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.ewrs: + closest, distance = self.objective_info(group) + closest.preset_locations.ewrs.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for group in self.armor_groups: + closest, distance = self.objective_info(group) + closest.preset_locations.armor_groups.append( + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) + ) + + for static in self.helipads: + closest, distance = self.objective_info(static) + closest.helipads.append( + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) + ) + + for static in self.factories: + closest, distance = self.objective_info(static) + closest.preset_locations.factories.append( + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) + ) + + for static in self.ammunition_depots: + closest, distance = self.objective_info(static) + closest.preset_locations.ammunition_depots.append( + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) + ) + + for static in self.strike_targets: + closest, distance = self.objective_info(static) + closest.preset_locations.strike_locations.append( + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) + ) + + for scenery_group in self.scenery: + closest, distance = self.objective_info(scenery_group) + closest.preset_locations.scenery.append(scenery_group) + + def populate_theater(self) -> None: + for control_point in self.control_points.values(): + self.theater.add_controlpoint(control_point) + self.add_preset_locations() + self.add_supply_routes() + self.add_shipping_lanes() diff --git a/resources/campaigns/battle_of_abu_dhabi.json b/resources/campaigns/battle_of_abu_dhabi.yaml similarity index 100% rename from resources/campaigns/battle_of_abu_dhabi.json rename to resources/campaigns/battle_of_abu_dhabi.yaml