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.
This commit is contained in:
Dan Albert 2020-11-16 00:32:50 -08:00
parent 8eef1eaa7c
commit 28e00055ab
8 changed files with 93 additions and 59 deletions

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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,