From b73a18b7b90ceb1392d0c09bc6918b62a1f093cb Mon Sep 17 00:00:00 2001 From: MetalStormGhost Date: Sat, 16 Sep 2023 14:36:59 +0300 Subject: [PATCH] Implemented spawning of Pretense cargo aircraft. To support that, implemented a separate flight plan called PretenseCargoFlightPlan. Also, will now automatically generate transport squadrons for factions which don't have pre-defined squadrons for it, but have access to transport aircraft. --- .../ato/flightplans/flightplanbuildertypes.py | 2 + game/ato/flightplans/pretensecargo.py | 99 +++++++++++++ game/ato/flightstate/navigating.py | 7 +- game/ato/flighttype.py | 4 + game/pretense/pretenseaircraftgenerator.py | 135 +++++++++++++++--- 5 files changed, 225 insertions(+), 22 deletions(-) create mode 100644 game/ato/flightplans/pretensecargo.py diff --git a/game/ato/flightplans/flightplanbuildertypes.py b/game/ato/flightplans/flightplanbuildertypes.py index 5047e45c..4561eeec 100644 --- a/game/ato/flightplans/flightplanbuildertypes.py +++ b/game/ato/flightplans/flightplanbuildertypes.py @@ -18,6 +18,7 @@ from .ocaaircraft import OcaAircraftFlightPlan from .ocarunway import OcaRunwayFlightPlan from .packagerefueling import PackageRefuelingFlightPlan from .planningerror import PlanningError +from .pretensecargo import PretenseCargoFlightPlan from .sead import SeadFlightPlan from .seadsweep import SeadSweepFlightPlan from .strike import StrikeFlightPlan @@ -60,6 +61,7 @@ class FlightPlanBuilderTypes: FlightType.TRANSPORT: AirliftFlightPlan.builder_type(), FlightType.FERRY: FerryFlightPlan.builder_type(), FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(), + FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(), } try: return builder_dict[flight.flight_type] diff --git a/game/ato/flightplans/pretensecargo.py b/game/ato/flightplans/pretensecargo.py new file mode 100644 index 00000000..f980b5f7 --- /dev/null +++ b/game/ato/flightplans/pretensecargo.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from datetime import timedelta +from typing import TYPE_CHECKING, Type + +from game.utils import feet +from .ferry import FerryLayout +from .ibuilder import IBuilder +from .planningerror import PlanningError +from .standard import StandardFlightPlan, StandardLayout +from .waypointbuilder import WaypointBuilder + +if TYPE_CHECKING: + from ..flightwaypoint import FlightWaypoint + + +PRETENSE_CARGO_FLIGHT_DISTANCE = 50000 + + +class PretenseCargoFlightPlan(StandardFlightPlan[FerryLayout]): + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + + @property + def tot_waypoint(self) -> FlightWaypoint: + return self.layout.arrival + + def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: + # TOT planning isn't really useful for ferries. They're behind the front + # lines so no need to wait for escorts or for other missions to complete. + return None + + def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: + return None + + @property + def mission_departure_time(self) -> timedelta: + return self.package.time_over_target + + +class Builder(IBuilder[PretenseCargoFlightPlan, FerryLayout]): + def layout(self) -> FerryLayout: + # Find the spawn location for off-map transport planes + distance_to_flot = 0 + heading_from_flot = 0.0 + offmap_transport_cp_id = self.flight.departure.id + for front_line_cp in self.coalition.game.theater.controlpoints: + for front_line in self.coalition.game.theater.conflicts(): + if front_line_cp.captured == self.flight.coalition.player: + if ( + front_line_cp.position.distance_to_point(front_line.position) + > distance_to_flot + ): + distance_to_flot = front_line_cp.position.distance_to_point( + front_line.position + ) + heading_from_flot = front_line.position.heading_between_point( + front_line_cp.position + ) + offmap_transport_cp_id = front_line_cp.id + offmap_transport_cp = self.coalition.game.theater.find_control_point_by_id( + offmap_transport_cp_id + ) + offmap_transport_spawn = offmap_transport_cp.position.point_from_heading( + heading_from_flot, PRETENSE_CARGO_FLIGHT_DISTANCE + ) + + altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter + altitude = ( + feet(1500) + if altitude_is_agl + else self.flight.unit_type.preferred_patrol_altitude + ) + + builder = WaypointBuilder(self.flight, self.coalition) + ferry_layout = FerryLayout( + departure=builder.join(offmap_transport_spawn), + nav_to=builder.nav_path( + offmap_transport_spawn, + self.flight.arrival.position, + altitude, + altitude_is_agl, + ), + arrival=builder.land(self.flight.arrival), + divert=builder.divert(self.flight.divert), + bullseye=builder.bullseye(), + nav_from=[], + ) + ferry_layout.departure = builder.join(offmap_transport_spawn) + ferry_layout.nav_to.append(builder.join(offmap_transport_spawn)) + ferry_layout.nav_from.append(builder.join(offmap_transport_spawn)) + print(ferry_layout) + return ferry_layout + + def build(self) -> PretenseCargoFlightPlan: + return PretenseCargoFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightstate/navigating.py b/game/ato/flightstate/navigating.py index 3932dbf3..39acfb06 100644 --- a/game/ato/flightstate/navigating.py +++ b/game/ato/flightstate/navigating.py @@ -29,11 +29,8 @@ class Navigating(InFlight): events.update_flight_position(self.flight, self.estimate_position()) def progress(self) -> float: - # if next waypoint is very close, assume we reach it immediately to avoid divide - # by zero error - if self.total_time_to_next_waypoint.total_seconds() < 1: - return 1.0 - + if self.total_time_to_next_waypoint.total_seconds() == 0.0: + return 99.9 return ( self.elapsed_time.total_seconds() / self.total_time_to_next_waypoint.total_seconds() diff --git a/game/ato/flighttype.py b/game/ato/flighttype.py index 615aaa5b..b8eeb1c9 100644 --- a/game/ato/flighttype.py +++ b/game/ato/flighttype.py @@ -58,6 +58,9 @@ class FlightType(Enum): FERRY = "Ferry" AIR_ASSAULT = "Air Assault" SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD + PRETENSE_CARGO = ( + "Cargo Transport" # Flight type for Pretense campaign AI cargo planes + ) def __str__(self) -> str: return self.value @@ -121,5 +124,6 @@ class FlightType(Enum): FlightType.SWEEP: AirEntity.FIGHTER, FlightType.TARCAP: AirEntity.FIGHTER, FlightType.TRANSPORT: AirEntity.UTILITY, + FlightType.PRETENSE_CARGO: AirEntity.UTILITY, FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING, }.get(self, AirEntity.UNSPECIFIED) diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py index 82135e57..60dc0c3a 100644 --- a/game/pretense/pretenseaircraftgenerator.py +++ b/game/pretense/pretenseaircraftgenerator.py @@ -5,20 +5,16 @@ import random from datetime import datetime from functools import cached_property from typing import Any, Dict, List, TYPE_CHECKING, Tuple +from uuid import UUID from dcs import Point -from dcs.action import AITaskPush -from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse from dcs.country import Country from dcs.mission import Mission -from dcs.terrain.terrain import NoParkingSlotError -from dcs.triggers import TriggerOnce, Event -from dcs.unit import Skill from dcs.unitgroup import FlyingGroup, StaticGroup from game.ato.airtaaskingorder import AirTaskingOrder from game.ato.flight import Flight -from game.ato.flightstate import Completed, WaitingForStart +from game.ato.flightstate import Completed, WaitingForStart, Navigating from game.ato.flighttype import FlightType from game.ato.package import Package from game.ato.starttype import StartType @@ -32,10 +28,9 @@ from game.radio.tacan import TacanRegistry from game.runways import RunwayData from game.settings import Settings from game.theater.controlpoint import ( - Airfield, ControlPoint, - Fob, OffMapSpawn, + ParkingType, ) from game.unitmap import UnitMap from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter @@ -44,7 +39,9 @@ from game.data.weapons import WeaponType if TYPE_CHECKING: from game import Game - from game.squadrons import Squadron + + +PRETENSE_SQUADRON_DEF_RETRIES = 100 class PretenseAircraftGenerator: @@ -108,6 +105,8 @@ class PretenseAircraftGenerator: ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData], ) -> None: + from game.squadrons import Squadron + """Adds aircraft to the mission for every flight in the ATO. Aircraft generation is done by walking the ATO and spawning each flight in turn. @@ -125,6 +124,89 @@ class PretenseAircraftGenerator: num_of_strike = 0 num_of_cap = 0 + # Find locations for off-map transport planes + distance_to_flot = 0 + offmap_transport_cp_id = cp.id + parking_type = ParkingType( + fixed_wing=True, fixed_wing_stol=True, rotary_wing=False + ) + for front_line_cp in self.game.theater.controlpoints: + for front_line in self.game.theater.conflicts(): + if front_line_cp.captured == cp.captured: + if ( + front_line_cp.total_aircraft_parking(parking_type) > 0 + and front_line_cp.position.distance_to_point( + front_line.position + ) + > distance_to_flot + ): + distance_to_flot = front_line_cp.position.distance_to_point( + front_line.position + ) + offmap_transport_cp_id = front_line_cp.id + offmap_transport_cp = self.game.theater.find_control_point_by_id( + offmap_transport_cp_id + ) + + # Ensure that the faction has at least one transport helicopter and one cargo plane squadron + autogenerate_transport_helicopter_squadron = True + autogenerate_cargo_plane_squadron = True + for aircraft_type in cp.coalition.air_wing.squadrons: + for squadron in cp.coalition.air_wing.squadrons[aircraft_type]: + mission_types = squadron.auto_assignable_mission_types + if squadron.aircraft.helicopter and ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + autogenerate_transport_helicopter_squadron = False + elif not squadron.aircraft.helicopter and ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + autogenerate_cargo_plane_squadron = False + + if autogenerate_transport_helicopter_squadron: + flight_type = FlightType.AIR_ASSAULT + squadron_def = ( + cp.coalition.air_wing.squadron_def_generator.generate_for_task( + flight_type, offmap_transport_cp + ) + ) + squadron = Squadron.create_from( + squadron_def, + flight_type, + 2, + offmap_transport_cp, + cp.coalition, + self.game, + ) + cp.coalition.air_wing.squadrons[squadron.aircraft] = list() + cp.coalition.air_wing.add_squadron(squadron) + if autogenerate_cargo_plane_squadron: + flight_type = FlightType.TRANSPORT + squadron_def = ( + cp.coalition.air_wing.squadron_def_generator.generate_for_task( + flight_type, offmap_transport_cp + ) + ) + for retries in range(PRETENSE_SQUADRON_DEF_RETRIES): + if squadron_def.aircraft.helicopter: + squadron_def = ( + cp.coalition.air_wing.squadron_def_generator.generate_for_task( + flight_type, offmap_transport_cp + ) + ) + squadron = Squadron.create_from( + squadron_def, + flight_type, + 2, + offmap_transport_cp, + cp.coalition, + self.game, + ) + cp.coalition.air_wing.squadrons[squadron.aircraft] = list() + cp.coalition.air_wing.add_squadron(squadron) + for squadron in cp.squadrons: # Intentionally don't spawn anything at OffMapSpawns in Pretense if isinstance(squadron.location, OffMapSpawn): @@ -134,11 +216,16 @@ class PretenseAircraftGenerator: squadron.untasked_aircraft += 1 package = Package(cp, squadron.flight_db, auto_asap=False) mission_types = squadron.auto_assignable_mission_types - if ( + if squadron.aircraft.helicopter and ( FlightType.TRANSPORT in mission_types or FlightType.AIR_ASSAULT in mission_types ): flight_type = FlightType.AIR_ASSAULT + elif not squadron.aircraft.helicopter and ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + flight_type = FlightType.TRANSPORT elif ( FlightType.SEAD in mission_types or FlightType.SEAD_SWEEP in mission_types @@ -170,13 +257,27 @@ class PretenseAircraftGenerator: num_of_cap += 1 else: flight_type = random.choice(list(mission_types)) - flight = Flight( - package, squadron, 1, flight_type, StartType.COLD, divert=cp - ) - flight.state = WaitingForStart( - flight, self.game.settings, self.game.conditions.start_time - ) - package.add_flight(flight) + + if flight_type == FlightType.TRANSPORT: + flight = Flight( + package, + squadron, + 1, + FlightType.PRETENSE_CARGO, + StartType.IN_FLIGHT, + divert=cp, + ) + package.add_flight(flight) + flight.state = Navigating(flight, self.game.settings, waypoint_index=1) + else: + flight = Flight( + package, squadron, 1, flight_type, StartType.COLD, divert=cp + ) + package.add_flight(flight) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + ato.add_package(package) self._reserve_frequencies_and_tacan(ato)