diff --git a/game/game.py b/game/game.py index 0226de89..b9aead14 100644 --- a/game/game.py +++ b/game/game.py @@ -5,6 +5,7 @@ from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats from gen.ato import AirTaskingOrder 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 .settings import Settings @@ -204,6 +205,7 @@ class Game: return event and event.name and event.name == self.player_name def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): + ObjectiveDistanceCache.set_theater(self.theater) logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) diff --git a/game/operation/operation.py b/game/operation/operation.py index c86efacb..86a63870 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -185,20 +185,16 @@ class Operation: self.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) - ato = self.game.blue_ato - else: - country = self.current_mission.country(self.game.enemy_country) - ato = self.game.red_ato - self.airgen.generate_flights( - cp, - country, - ato, - self.groundobjectgen.runways - ) + self.airgen.generate_flights( + self.current_mission.country(self.game.player_country), + self.game.blue_ato, + self.groundobjectgen.runways + ) + self.airgen.generate_flights( + self.current_mission.country(self.game.enemy_country), + self.game.red_ato, + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] diff --git a/gen/aircraft.py b/gen/aircraft.py index 04301de7..f4581989 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -757,7 +757,7 @@ class AircraftConflictGenerator: for parking_slot in cp.airport.parking_slots: parking_slot.unit_id = None - def generate_flights(self, cp, country, ato: AirTaskingOrder, + def generate_flights(self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]) -> None: self.clear_parking_slots() @@ -768,7 +768,8 @@ class AircraftConflictGenerator: logging.info("Flight not generated: culled") continue logging.info(f"Generating flight: {flight.unit_type}") - group = self.generate_planned_flight(cp, country, flight) + 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) 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/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 6e97e91d..e65dc74a 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -19,6 +19,10 @@ from gen.flights.ai_flight_planner_db import ( SEAD_CAPABLE, STRIKE_CAPABLE, ) +from gen.flights.closestairfields import ( + ClosestAirfields, + ObjectiveDistanceCache, +) from gen.flights.flight import ( Flight, FlightType, @@ -37,29 +41,6 @@ if TYPE_CHECKING: from game.inventory import GlobalAircraftInventory -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 - - @dataclass(frozen=True) class ProposedFlight: """A flight outline proposed by the mission planner. @@ -208,11 +189,6 @@ class ObjectiveFinder: def __init__(self, game: Game, is_player: bool) -> None: self.game = game self.is_player = is_player - # TODO: Cache globally at startup to avoid generating twice per turn? - self.closest_airfields: Dict[str, ClosestAirfields] = { - t.name: ClosestAirfields(t, self.game.theater.controlpoints) - for t in self.all_possible_targets() - } def enemy_sams(self) -> Iterator[TheaterGroundObject]: """Iterates over all enemy SAM sites.""" @@ -303,7 +279,7 @@ class ObjectiveFinder: CP. """ for cp in self.friendly_control_points(): - airfields_in_proximity = self.closest_airfields[cp.name] + airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_threat_range = airfields_in_proximity.airfields_within( self.AIRFIELD_THREAT_RANGE ) @@ -336,7 +312,7 @@ class ObjectiveFinder: def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" - return self.closest_airfields[location.name] + return ObjectiveDistanceCache.get_closest_airfields(location) class CoalitionMissionPlanner: 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/flightplan.py b/gen/flights/flightplan.py index 0c631b31..a6e787a4 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -11,13 +11,15 @@ import logging import random from typing import List, Optional, TYPE_CHECKING -from game.data.doctrine import Doctrine, MODERN_DOCTRINE -from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint -from ..conflictgen import Conflict -from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject -from game.utils import nm_to_meter from dcs.unit import Unit +from game.data.doctrine import Doctrine, MODERN_DOCTRINE +from game.utils import nm_to_meter +from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType +from .waypointbuilder import WaypointBuilder +from ..conflictgen import Conflict + if TYPE_CHECKING: from game import Game @@ -103,108 +105,54 @@ class FlightPlanBuilder: # TODO: Stop clobbering flight type. 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_altitude - ) - ingress_point.pretty_name = "INGRESS on " + location.name - ingress_point.description = "INGRESS on " + location.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 = ( - f"STRIKE [{location.name}] : {u.type} #{j}" - ) - point.pretty_name = ( - f"STRIKE [{location.name}] : {u.type} #{j}" - ) - point.name = f"{location.name} #{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 - ) - for building in buildings: - if building.is_dead: - continue - - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - building.position.x, - building.position.y, - 0 - ) - point.description = ( - f"STRIKE on {building.obj_name} {building.category} " - f"[{building.dcs_identifier}]" - ) - point.pretty_name = ( - f"STRIKE on {building.obj_name} {building.category} " - f"[{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.name - point.pretty_name = "STRIKE on " + location.name - point.name = location.name - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) + egress_heading = heading - 180 - 25 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_altitude - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.name - egress_point.description = "EGRESS from " + location.name - flight.points.append(egress_point) - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.ingress_strike(ingress_pos, location) - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + 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(egress_pos, location) + builder.rtb(flight.from_cp) + + flight.points = builder.build() def generate_barcap(self, flight: Flight, location: MissionTarget) -> None: """Generate a BARCAP flight at a given location. @@ -239,39 +187,11 @@ class FlightPlanBuilder: 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(location) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + 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_frontline_cap(self, flight: Flight, location: MissionTarget) -> None: @@ -309,40 +229,11 @@ class FlightPlanBuilder: 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 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) + 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, location: MissionTarget, custom_targets: Optional[List[Unit]] = None) -> None: @@ -359,33 +250,30 @@ class FlightPlanBuilder: if custom_targets is None: custom_targets = [] - 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_altitude - ) - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS on " + location.name - ingress_point.description = "INGRESS on " + location.name - flight.points.append(ingress_point) - if len(custom_targets) > 0: + egress_heading = heading - 180 - 25 + egress_pos = location.position.point_from_heading( + egress_heading, self.doctrine.ingress_egress_distance + ) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.ingress_sead(ingress_pos, 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, @@ -395,55 +283,19 @@ class FlightPlanBuilder: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + target.type - point.pretty_name = "DEAD on " + location.name - point.only_for_player = True + builder.dead_point(target, location.name, location) else: - point.description = "SEAD on " + location.name - point.pretty_name = "SEAD on " + location.name - point.only_for_player = True - flight.points.append(point) - ingress_point.targets.append(location) - ingress_point.targetGroup = location + builder.sead_point(target, location.name, location) else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 - ) - point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + location.name - point.pretty_name = "DEAD on " + location.name - point.only_for_player = True + builder.dead_area(location) else: - point.description = "SEAD on " + location.name - point.pretty_name = "SEAD on " + location.name - point.only_for_player = True - ingress_point.targets.append(location) - ingress_point.targetGroup = location - flight.points.append(point) + builder.sead_area(location) - 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_altitude - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.name - egress_point.description = "EGRESS from " + location.name - flight.points.append(egress_point) + builder.egress(egress_pos, location) + builder.rtb(flight.from_cp) - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + flight.points = builder.build() def generate_cas(self, flight: Flight, location: MissionTarget) -> None: """Generate a CAS flight plan for the given target. @@ -455,89 +307,36 @@ class FlightPlanBuilder: if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - from_cp, location = location.control_points is_helo = getattr(flight.unit_type, "helicopter", False) - cap_alt = 1000 - flight.points = [] + cap_alt = 500 if is_helo else 1000 flight.flight_type = FlightType.CAS ingress, heading, distance = Conflict.frontline_vector( - from_cp, location, self.game.theater + 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) - ascend = self.generate_ascend_point(flight.from_cp) - if is_helo: - cap_alt = 500 - ascend.alt = 500 - flight.points.append(ascend) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp, is_helo) + builder.ingress_cas(ingress, location) + builder.cas(center, cap_alt) + builder.egress(egress, location) + builder.rtb(flight.from_cp, is_helo) - 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) + 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. """ - ascend_heading = departure.heading - pos_ascend = departure.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 + builder = WaypointBuilder(self.doctrine) + builder.ascent(departure) + return builder.build()[0] def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: """Generate approach/descend point. @@ -545,21 +344,9 @@ class FlightPlanBuilder: Args: arrival: Arrival airfield or carrier. """ - ascend_heading = arrival.heading - descend = arrival.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 + builder = WaypointBuilder(self.doctrine) + builder.descent(arrival) + return builder.build()[0] @staticmethod def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint: @@ -568,15 +355,6 @@ class FlightPlanBuilder: Args: arrival: Arrival airfield or carrier. """ - rtb = arrival.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 + builder = WaypointBuilder(self.doctrine) + builder.land(arrival) + return builder.build()[0] diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py new file mode 100644 index 00000000..7ef4a30e --- /dev/null +++ b/gen/flights/waypointbuilder.py @@ -0,0 +1,270 @@ +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 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. + 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. + 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. + self.ingress_point.targetGroup = target + + def dead_area(self, target: MissionTarget) -> None: + self._target_area(f"DEAD on {target.name}", target) + # TODO: Seems fishy. + 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)