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,