From 28e00055ab7d6756fc4b25a432a90b51da7f9713 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 16 Nov 2020 00:32:50 -0800 Subject: [PATCH] Differentiate BARCAP and TARCAP. Previously the only difference between these was the objective type: TARCAP was for front lines and BARCAP was for everything else. Now BARCAP is for friendly areas and TARCAP is for enemy areas. The practical difference between the two is that a TARCAP package is like the old front line CAP in that it will adjust its patrol time to match the package if it can, and it will also arrive two minutes ahead of the rest of the package to clear the area if needed. --- changelog.md | 1 + gen/flights/ai_flight_planner.py | 6 +- gen/flights/flightplan.py | 125 ++++++++++-------- qt_ui/widgets/combos/QFlightTypeComboBox.py | 4 +- .../flight/waypoints/QFlightWaypointTab.py | 6 +- theater/frontline.py | 3 + theater/missiontarget.py | 4 + theater/theatergroundobject.py | 3 + 8 files changed, 93 insertions(+), 59 deletions(-) diff --git a/changelog.md b/changelog.md index bbb34464..4f71438e 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ # Features/Improvements * **[Flight Planner]** Added fighter sweep missions. +* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package. # 2.2.1 diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index ce68be2d..008c344e 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -496,7 +496,11 @@ class CoalitionMissionPlanner: error = random.randint(-margin, margin) yield timedelta(minutes=max(0, time + error)) - dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION) + dca_types = { + FlightType.BARCAP, + FlightType.INTERCEPTION, + FlightType.TARCAP, + } non_dca_packages = [p for p in self.ato.packages if p.primary_task not in dca_types] diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index ed6561f5..b16732d0 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -340,9 +340,10 @@ class CasFlightPlan(PatrollingFlightPlan): @dataclass(frozen=True) -class FrontLineCapFlightPlan(PatrollingFlightPlan): +class TarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint land: FlightWaypoint + lead_time: timedelta @property def waypoints(self) -> List[FlightWaypoint]: @@ -353,6 +354,10 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan): self.land, ] + @property + def tot_offset(self) -> timedelta: + return -self.lead_time + def depart_time_for_waypoint( self, waypoint: FlightWaypoint) -> Optional[timedelta]: if waypoint == self.patrol_end: @@ -363,8 +368,8 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan): def patrol_start_time(self) -> timedelta: start = self.package.escort_start_time if start is not None: - return start - return super().patrol_start_time + return start + self.tot_offset + return super().patrol_start_time + self.tot_offset @property def patrol_end_time(self) -> timedelta: @@ -374,6 +379,10 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan): return super().patrol_end_time +# TODO: Remove when breaking save compat. +FrontLineCapFlightPlan = TarCapFlightPlan + + @dataclass(frozen=True) class StrikeFlightPlan(FormationFlightPlan): takeoff: FlightWaypoint @@ -635,7 +644,7 @@ class FlightPlanBuilder: elif task == FlightType.SWEEP: return self.generate_sweep(flight) elif task == FlightType.TARCAP: - return self.generate_frontline_cap(flight) + return self.generate_tarcap(flight) elif task == FlightType.TROOP_TRANSPORT: logging.error( "Troop transport flight plan generation not implemented" @@ -704,47 +713,12 @@ class FlightPlanBuilder: if isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) + start, end = self.racetrack_for_objective(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 the mission is a BARCAP of an enemy airfield, find the *next* - # closest enemy airfield. - if airfield == self.package.target: - continue - if airfield.captured != self.is_player: - closest_airfield = airfield - break - else: - raise PlanningError("Could not find any enemy airfields") - - heading = location.position.heading_between_point( - closest_airfield.position - ) - - min_distance_from_enemy = nm_to_meter(20) - distance_to_airfield = int(closest_airfield.position.distance_to_point( - self.package.target.position - )) - distance_to_no_fly = distance_to_airfield - min_distance_from_enemy - min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, - distance_to_no_fly) - max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, - distance_to_no_fly) - - end = location.position.point_from_heading( - heading, - random.randint(min_cap_distance, max_cap_distance) - ) - 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.game.conditions, flight, self.doctrine) start, end = builder.race_track(start, end, patrol_alt) descent, land = builder.rtb(flight.from_cp) @@ -788,20 +762,48 @@ class FlightPlanBuilder: land=land ) - def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan: - """Generate a CAP flight plan for the given front line. + def racetrack_for_objective(self, + location: MissionTarget) -> Tuple[Point, Point]: + closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) + for airfield in closest_cache.closest_airfields: + # If the mission is a BARCAP of an enemy airfield, find the *next* + # closest enemy airfield. + if airfield == self.package.target: + continue + if airfield.captured != self.is_player: + closest_airfield = airfield + break + else: + raise PlanningError("Could not find any enemy airfields") - Args: - flight: The flight to generate the flight plan for. - """ - location = self.package.target + heading = location.position.heading_between_point( + closest_airfield.position + ) - if not isinstance(location, FrontLine): - raise InvalidObjectiveLocation(flight.flight_type, location) + min_distance_from_enemy = nm_to_meter(20) + distance_to_airfield = int(closest_airfield.position.distance_to_point( + self.package.target.position + )) + distance_to_no_fly = distance_to_airfield - min_distance_from_enemy + min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, + distance_to_no_fly) + max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, + distance_to_no_fly) - ally_cp, enemy_cp = location.control_points - patrol_alt = random.randint(self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude) + end = location.position.point_from_heading( + heading, + random.randint(min_cap_distance, max_cap_distance) + ) + diameter = random.randint( + self.doctrine.cap_min_track_length, + self.doctrine.cap_max_track_length + ) + start = end.point_from_heading(heading - 180, diameter) + return start, end + + def racetrack_for_frontline(self, + front_line: FrontLine) -> Tuple[Point, Point]: + ally_cp, enemy_cp = front_line.control_points # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector( @@ -822,14 +824,33 @@ class FlightPlanBuilder: orbit0p = orbit_center.point_from_heading(heading, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius) + return orbit0p, orbit1p + + def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan: + """Generate a CAP flight plan for the given front line. + + Args: + flight: The flight to generate the flight plan for. + """ + location = self.package.target + + patrol_alt = random.randint(self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude) + # Create points builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) + if isinstance(location, FrontLine): + orbit0p, orbit1p = self.racetrack_for_frontline(location) + else: + orbit0p, orbit1p = self.racetrack_for_objective(location) + start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) descent, land = builder.rtb(flight.from_cp) - return FrontLineCapFlightPlan( + return TarCapFlightPlan( package=self.package, flight=flight, + lead_time=timedelta(minutes=2), # Note that this duration only has an effect if there are no # flights in the package that have requested escort. If the package # requests an escort the CAP flight will remain on station for the diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index c1b42ccc..8adf0bbc 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -18,6 +18,7 @@ class QFlightTypeComboBox(QComboBox): """Combo box for selecting a flight task type.""" COMMON_ENEMY_MISSIONS = [ + FlightType.TARCAP, FlightType.ESCORT, FlightType.SEAD, FlightType.DEAD, @@ -50,7 +51,6 @@ class QFlightTypeComboBox(QComboBox): ] ENEMY_AIRBASE_MISSIONS = [ - FlightType.BARCAP, # TODO: FlightType.STRIKE ] + COMMON_ENEMY_MISSIONS @@ -60,13 +60,11 @@ class QFlightTypeComboBox(QComboBox): ] + COMMON_FRIENDLY_MISSIONS ENEMY_GROUND_OBJECT_MISSIONS = [ - FlightType.BARCAP, FlightType.STRIKE, ] + COMMON_ENEMY_MISSIONS FRONT_LINE_MISSIONS = [ FlightType.CAS, - FlightType.TARCAP, # TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.EVAC ] + COMMON_ENEMY_MISSIONS diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 43198d28..5f031622 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -149,10 +149,10 @@ class QFlightWaypointTab(QFrame): # departs, whereas BARCAP usually isn't part of a strike package and # has a fixed mission time. if task == FlightType.CAP: - if isinstance(self.package.target, FrontLine): - task = FlightType.TARCAP - else: + if self.package.target.is_friendly(to_player=True): task = FlightType.BARCAP + else: + task = FlightType.TARCAP self.flight.flight_type = task self.planner.populate_flight_plan(self.flight) self.flight_waypoint_list.update_list() diff --git a/theater/frontline.py b/theater/frontline.py index c70b3417..0ef17079 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -42,3 +42,6 @@ class FrontLine(MissionTarget): def control_points(self) -> Tuple[ControlPoint, ControlPoint]: """Returns a tuple of the two control points.""" return self.control_point_a, self.control_point_b + + def is_friendly(self, to_player: bool) -> bool: + return False diff --git a/theater/missiontarget.py b/theater/missiontarget.py index fb4da0f3..ea9ccec8 100644 --- a/theater/missiontarget.py +++ b/theater/missiontarget.py @@ -17,3 +17,7 @@ class MissionTarget: def distance_to(self, other: MissionTarget) -> int: """Computes the distance to the given mission target.""" return self.position.distance_to_point(other.position) + + def is_friendly(self, to_player: bool) -> bool: + """Returns True if the objective is in friendly territory.""" + raise NotImplementedError diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 0e8b3c87..293c392f 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -113,6 +113,9 @@ class TheaterGroundObject(MissionTarget): def faction_color(self) -> str: return "BLUE" if self.control_point.captured else "RED" + def is_friendly(self, to_player: bool) -> bool: + return not self.control_point.is_friendly(to_player) + class BuildingGroundObject(TheaterGroundObject): def __init__(self, name: str, category: str, group_id: int, object_id: int,