From 531625ff08ec08e03c7825c9be939c1153e6c891 Mon Sep 17 00:00:00 2001 From: Raffson Date: Fri, 20 Jan 2023 23:00:13 +0100 Subject: [PATCH] Designated CTLD zones for ControlPoints (AB/FOB/FARP) Resolves #46 --- changelog.md | 1 + game/ato/flightplans/_common_ctld.py | 20 +++++++++++++++++++ game/ato/flightplans/airassault.py | 17 ++++++++-------- game/ato/flightplans/airlift.py | 24 ++++++++++++++++------- game/ato/flightplans/waypointbuilder.py | 8 +++++++- game/campaignloader/mizcampaignloader.py | 25 ++++++++++++++++++++---- game/theater/controlpoint.py | 23 +++++++++++++++++----- game/theater/interfaces/CTLD.py | 7 +++++++ game/version.py | 5 ++++- 9 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 game/ato/flightplans/_common_ctld.py create mode 100644 game/theater/interfaces/CTLD.py diff --git a/changelog.md b/changelog.md index c4d5b035..cd19c19a 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ * **[UI]** Configurable ICLS for capable Carriers & LHAs. * **[UI]** Configurable LINK4 for Carriers. * **[Kneeboard]** Show package information in Support page +* **[Campaign Design]** Ability to define designated CTLD zones for Control Points (Airbases & FOBs/FARPs) ## Fixes * **[UI]** Removed deprecated options diff --git a/game/ato/flightplans/_common_ctld.py b/game/ato/flightplans/_common_ctld.py new file mode 100644 index 00000000..19e456e6 --- /dev/null +++ b/game/ato/flightplans/_common_ctld.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import random +from typing import Union, TYPE_CHECKING, Tuple + +if TYPE_CHECKING: + from dcs import Point + + from game.theater import ControlPoint + from game.theater.interfaces.CTLD import CTLD + + +def generate_random_ctld_point(cp: Union[ControlPoint, CTLD]) -> Point: + if isinstance(cp, CTLD) and cp.ctld_zones: + zone: Tuple[Point, float] = random.choice(cp.ctld_zones) + pos, radius = zone + return pos.random_point_within(radius) + elif isinstance(cp, CTLD) and isinstance(cp, ControlPoint): + return cp.position.random_point_within(2000, 200) + raise RuntimeError("Could not generate CTLD point") diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index 5e66d7cd..f4d373de 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from datetime import timedelta from typing import Iterator, TYPE_CHECKING, Type +from ._common_ctld import generate_random_ctld_point from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout from game.theater.controlpoint import ControlPointType from game.theater.missiontarget import MissionTarget @@ -13,8 +14,10 @@ from .planningerror import PlanningError from .uizonedisplay import UiZone, UiZoneDisplay from .waypointbuilder import WaypointBuilder from ..flightwaypoint import FlightWaypointType +from ...theater.interfaces.CTLD import CTLD if TYPE_CHECKING: + from dcs import Point from ..flightwaypoint import FlightWaypoint @@ -104,16 +107,10 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): pickup = None pickup_position = self.flight.departure.position else: - # TODO The calculation of the Pickup LZ is currently randomized. This - # leads to the problem that we can not gurantee that the LZ is clear of - # obstacles. This has to be improved in the future so that the Mission can - # be autoplanned. In the current state the User has to check the created - # Waypoints for the Pickup and Dropoff LZs are free of obstacles. - # Create a special pickup zone for Helos from Airbase / FOB pickup = builder.pickup_zone( MissionTarget( "Pickup Zone", - self.flight.departure.position.random_point_within(1200, 600), + self._generate_ctld_pickup(), ) ) pickup_position = pickup.position @@ -123,7 +120,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): assault_area.only_for_player = False assault_area.alt = feet(1000) - # TODO we can not gurantee a safe LZ for DropOff. See comment above. + # TODO: define CTLD dropoff zones in campaign miz? drop_off_zone = MissionTarget( "Dropoff zone", self.package.target.position.point_from_heading(heading, 1200), @@ -159,3 +156,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): def build(self) -> AirAssaultFlightPlan: return AirAssaultFlightPlan(self.flight, self.layout()) + + def _generate_ctld_pickup(self) -> Point: + assert isinstance(self.flight.departure, CTLD) + return generate_random_ctld_point(self.flight.departure) diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 1f143a35..17516f3b 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -5,15 +5,18 @@ from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type +from ._common_ctld import generate_random_ctld_point from game.theater.missiontarget import MissionTarget from game.utils import feet from .ibuilder import IBuilder from .planningerror import PlanningError from .standard import StandardFlightPlan, StandardLayout from .waypointbuilder import WaypointBuilder +from ...theater.interfaces.CTLD import CTLD if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint + from dcs import Point @dataclass(frozen=True) @@ -106,15 +109,10 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): if self.flight.is_helo: # Create CTLD Zones for Helo flights pickup_zone = builder.pickup_zone( - MissionTarget( - "Pickup Zone", cargo.origin.position.random_point_within(1000, 200) - ) + MissionTarget("Pickup Zone", self._generate_ctld_pickup()) ) drop_off_zone = builder.dropoff_zone( - MissionTarget( - "Dropoff zone", - cargo.next_stop.position.random_point_within(1000, 200), - ) + MissionTarget("Dropoff zone", self._generate_ctld_dropoff()) ) # Show the zone waypoints only to the player pickup_zone.only_for_player = True @@ -153,3 +151,15 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): def build(self) -> AirliftFlightPlan: return AirliftFlightPlan(self.flight, self.layout()) + + def _generate_ctld_pickup(self) -> Point: + cargo = self.flight.cargo + if cargo and cargo.origin and isinstance(cargo.origin, CTLD): + return generate_random_ctld_point(cargo.origin) + raise RuntimeError("Could not generate CTLD pickup") + + def _generate_ctld_dropoff(self) -> Point: + cargo = self.flight.cargo + if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD): + return generate_random_ctld_point(cargo.transport.destination) + raise RuntimeError("Could not generate CTLD dropoff") diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index 81f3a63c..2c5a72a4 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -14,6 +14,7 @@ from typing import ( from dcs.mapping import Point, Vector2 +from game.ato.flightplans._common_ctld import generate_random_ctld_point from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType from game.theater import ( @@ -23,6 +24,7 @@ from game.theater import ( TheaterGroundObject, TheaterUnit, ) +from game.theater.interfaces.CTLD import CTLD from game.utils import Distance, meters, nautical_miles, feet if TYPE_CHECKING: @@ -557,10 +559,14 @@ class WaypointBuilder: """Creates a cargo stop waypoint. This waypoint is used by AirLift as a landing and stopover waypoint """ + if isinstance(control_point, CTLD) and control_point.ctld_zones: + pos = generate_random_ctld_point(control_point) + else: + pos = control_point.position return FlightWaypoint( "CARGOSTOP", FlightWaypointType.CARGO_STOP, - control_point.position, + pos, meters(0), "RADIO", description=f"Stop for cargo at {control_point.name}", diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index 46b44deb..f8baebeb 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -32,6 +32,7 @@ from game.utils import Distance, meters if TYPE_CHECKING: from game.theater.conflicttheater import ConflictTheater + from dcs import Point class MizCampaignLoader: @@ -107,8 +108,12 @@ class MizCampaignLoader: if self.mission.country(self.RED_COUNTRY.name) is None: self.mission.coalition["red"].add_country(self.RED_COUNTRY) - def control_point_from_airport(self, airport: Airport) -> ControlPoint: - cp = Airfield(airport, self.theater, starts_blue=airport.is_blue()) + def control_point_from_airport( + self, airport: Airport, ctld_zones: List[Tuple[Point, float]] + ) -> ControlPoint: + cp = Airfield( + airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones + ) # Use the unlimited aircraft option to determine if an airfield should # be owned by the player when the campaign is "inverted". @@ -245,7 +250,8 @@ class MizCampaignLoader: 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) + ctld_zones = self.get_ctld_zones(airport.name) + control_point = self.control_point_from_airport(airport, ctld_zones) control_points[control_point.id] = control_point for blue in (False, True): @@ -268,8 +274,13 @@ class MizCampaignLoader: control_point.captured_invert = ship.late_activation control_points[control_point.id] = control_point for fob in self.fobs(blue): + ctld_zones = self.get_ctld_zones(fob.name) control_point = Fob( - str(fob.name), fob.position, self.theater, starts_blue=blue + str(fob.name), + fob.position, + self.theater, + starts_blue=blue, + ctld_zones=ctld_zones, ) control_point.captured_invert = fob.late_activation control_points[control_point.id] = control_point @@ -460,3 +471,9 @@ class MizCampaignLoader: self.add_preset_locations() self.add_supply_routes() self.add_shipping_lanes() + + def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]: + zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name] + for z in zones: + self.mission.triggers.zones().remove(z) + return [(z.position, z.radius) for z in zones] diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 976cbde5..3011a72d 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -41,6 +41,7 @@ from dcs.ships import ( Hms_invincible, ) from dcs.terrain.terrain import Airport, ParkingSlot +from dcs.triggers import TriggerZone from dcs.unitgroup import ShipGroup, StaticGroup from dcs.unittype import ShipType @@ -62,6 +63,7 @@ from game.theater.presetlocation import PresetLocation from game.utils import Distance, Heading, meters from .base import Base from .frontline import FrontLine +from .interfaces.CTLD import CTLD from .missiontarget import MissionTarget from .theatergroundobject import ( GenericCarrierGroundObject, @@ -1047,9 +1049,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): ... -class Airfield(ControlPoint): +class Airfield(ControlPoint, CTLD): def __init__( - self, airport: Airport, theater: ConflictTheater, starts_blue: bool + self, + airport: Airport, + theater: ConflictTheater, + starts_blue: bool, + ctld_zones: Optional[List[Tuple[Point, float]]] = None, ) -> None: super().__init__( airport.name, @@ -1061,6 +1067,7 @@ class Airfield(ControlPoint): ) self.airport = airport self._runway_status = RunwayStatus() + self.ctld_zones = ctld_zones @property def dcs_airport(self) -> Airport: @@ -1399,14 +1406,20 @@ class OffMapSpawn(ControlPoint): return ControlPointStatus.Functional -class Fob(ControlPoint, RadioFrequencyContainer): +class Fob(ControlPoint, RadioFrequencyContainer, CTLD): def __init__( - self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool - ): + self, + name: str, + at: Point, + theater: ConflictTheater, + starts_blue: bool, + ctld_zones: Optional[List[Tuple[Point, float]]] = None, + ) -> None: super().__init__( name, at, at, theater, starts_blue, cptype=ControlPointType.FOB ) self.name = name + self.ctld_zones = ctld_zones @property def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: diff --git a/game/theater/interfaces/CTLD.py b/game/theater/interfaces/CTLD.py new file mode 100644 index 00000000..cda84db0 --- /dev/null +++ b/game/theater/interfaces/CTLD.py @@ -0,0 +1,7 @@ +from typing import Optional, List, Tuple + +from dcs import Point + + +class CTLD: + ctld_zones: Optional[List[Tuple[Point, float]]] = None diff --git a/game/version.py b/game/version.py index 58b03430..859f5fab 100644 --- a/game/version.py +++ b/game/version.py @@ -173,5 +173,8 @@ VERSION = _build_version_string() #: * Campaign designers can now define more settings: #: `max_frontline_length: 25` (in km) #: `culling_exclusion_radius: 35` (in km) +#: +#: Version 10.6 +#: * Designated CTLD zones for ControlPoints (Airbases & FOBs/FARPs) -CAMPAIGN_FORMAT_VERSION = (10, 5) +CAMPAIGN_FORMAT_VERSION = (10, 6)