Add BAI planning against supply routes.

This currently is only supported for player flights. I have no idea how
to create an AI flight plan that won't just get them killed. AI-only BAI
missions against supply routes will warn the player on mission creation.
This commit is contained in:
Dan Albert 2021-04-19 23:09:39 -07:00
parent 2b06d8a096
commit 3f16c0378a
5 changed files with 238 additions and 2 deletions

View File

@ -6,6 +6,8 @@ from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Optional from typing import Dict, Iterator, List, Optional
from dcs import Point
from game.theater import FlightType, MissionTarget
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
@ -97,3 +99,25 @@ class SupplyRoute:
current = previous current = previous
path.reverse() path.reverse()
return path return path
class SupplyRouteLink(MissionTarget):
def __init__(self, a: ControlPoint, b: ControlPoint) -> None:
self.control_point_a = a
self.control_point_b = b
super().__init__(
f"Supply route between {a} and {b}",
Point((a.position.x + b.position.x) / 2, (a.position.y + b.position.y) / 2),
)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.BAI,
# TODO: Escort
# TODO: SEAD
# TODO: Recon
# TODO: TARCAP
]
def is_friendly(self, to_player: bool) -> bool:
return self.control_point_a.captured

View File

@ -30,6 +30,7 @@ from game.theater import (
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
) )
from game.theater.supplyroutes import SupplyRouteLink
from game.theater.theatergroundobject import EwrGroundObject from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
@ -466,6 +467,25 @@ class CasFlightPlan(PatrollingFlightPlan):
return self.patrol_end return self.patrol_end
@dataclass(frozen=True)
class ConvoyInterdictionFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield from [
self.patrol_start,
self.patrol_end,
]
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
@dataclass(frozen=True) @dataclass(frozen=True)
class TarCapFlightPlan(PatrollingFlightPlan): class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
@ -1014,7 +1034,7 @@ class FlightPlanBuilder:
hold_duration=timedelta(hours=4), hold_duration=timedelta(hours=4),
) )
def generate_bai(self, flight: Flight) -> StrikeFlightPlan: def generate_bai(self, flight: Flight) -> FlightPlan:
"""Generates a BAI flight plan. """Generates a BAI flight plan.
Args: Args:
@ -1022,6 +1042,9 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
if isinstance(location, SupplyRouteLink):
return self.generate_supply_route_bai(flight, location)
if not isinstance(location, TheaterGroundObject): if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
@ -1034,6 +1057,60 @@ class FlightPlanBuilder:
flight, location, FlightWaypointType.INGRESS_BAI, targets flight, location, FlightWaypointType.INGRESS_BAI, targets
) )
def generate_supply_route_bai(
self, flight: Flight, location: SupplyRouteLink
) -> ConvoyInterdictionFlightPlan:
"""Generates a BAI flight plan for attacking a supply route.
These flight plans are extremely rough because we do not know where the roads
are. For now they're mostly only usable by players. The flight plan includes a
start and end patrol point matching the end points of the convoy's route and a
30 minute time on station. It is up to the player to find the target.
Args:
flight: The flight to generate the flight plan for.
location: The supply route link to attack.
"""
origin = self.package_airfield()
a_dist = origin.distance_to(location.control_point_a)
b_dist = origin.distance_to(location.control_point_b)
if a_dist < b_dist:
near = location.control_point_a
far = location.control_point_b
else:
near = location.control_point_b
far = location.control_point_a
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.convoy_search(near, far, patrol_alt)
return ConvoyInterdictionFlightPlan(
self.package,
flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, near.position, patrol_alt
),
nav_from=builder.nav_path(
far.position, flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
# Not relevant because player only.
engagement_distance=meters(0),
patrol_duration=timedelta(minutes=30),
)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan: def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan. """Generates an anti-ship flight plan.

View File

@ -349,6 +349,63 @@ class WaypointBuilder:
self.race_track_end(end, altitude), self.race_track_end(end, altitude),
) )
@staticmethod
def convoy_search_start(
control_point: ControlPoint, altitude: Distance
) -> FlightWaypoint:
"""Creates a convoy search start waypoint.
Args:
control_point: Control point for the beginning of the search.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_BAI,
control_point.position.x,
control_point.position.y,
altitude,
)
waypoint.name = control_point.name
waypoint.description = "Beginning of convoy search area"
waypoint.pretty_name = "Search start"
return waypoint
@staticmethod
def convoy_search_end(
control_point: ControlPoint, altitude: Distance
) -> FlightWaypoint:
"""Creates a convoy search start waypoint.
Args:
control_point: Control point for the beginning of the search.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
control_point.position.x,
control_point.position.y,
altitude,
)
waypoint.name = control_point.name
waypoint.description = "End of convoy search area"
waypoint.pretty_name = "Search end"
return waypoint
def convoy_search(
self, start: ControlPoint, end: ControlPoint, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a convoy search path.
Args:
start: The beginning convoy search waypoint.
end: The ending convoy search waypoint.
altitude: The convoy search altitude.
"""
return (
self.convoy_search_start(start, altitude),
self.convoy_search_end(end, altitude),
)
@staticmethod @staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint: def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point. """Creates an circular orbit point.

View File

@ -16,6 +16,8 @@ import qt_ui.uiconstants as CONST
from game import Game from game import Game
from game.event.airwar import AirWarEvent from game.event.airwar import AirWarEvent
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import FlightType
from gen.flights.flightplan import ConvoyInterdictionFlightPlan
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
@ -199,6 +201,36 @@ class QTopPanel(QFrame):
) )
return result == QMessageBox.Yes return result == QMessageBox.Yes
def ato_has_ai_convoy_interdiction(self) -> bool:
for package in self.game.blue_ato.packages:
for flight in package.flights:
if (
isinstance(flight.flight_plan, ConvoyInterdictionFlightPlan)
and not flight.client_count
):
return True
return False
def confirm_ai_convoy_interdiction_launch(self) -> bool:
result = QMessageBox.question(
self,
"Continue with AI convoy interdiction missions?",
(
"AI only convoy interdiction missions were planned. AI behavior for "
"these missions has not been developed so they will probably get "
"themselves killed. Continuing is not recommended.<br />"
"<br />"
"To remove AI convoy interdiction missions, delete any BAI flights "
"that are planned against supply route objectives.<br />"
"<br />"
"Click 'Yes' to continue with AI only convoy interdiction missions."
"<br /><br />Click 'No' to cancel and revise your flight planning."
),
QMessageBox.No,
QMessageBox.Yes,
)
return result == QMessageBox.Yes
def confirm_negative_start_time(self, negative_starts: List[Package]) -> bool: def confirm_negative_start_time(self, negative_starts: List[Package]) -> bool:
formatted = "<br />".join( formatted = "<br />".join(
[f"{p.primary_task} {p.target.name}" for p in negative_starts] [f"{p.primary_task} {p.target.name}" for p in negative_starts]
@ -241,6 +273,12 @@ class QTopPanel(QFrame):
if not self.ato_has_clients() and not self.confirm_no_client_launch(): if not self.ato_has_clients() and not self.confirm_no_client_launch():
return return
if (
self.ato_has_ai_convoy_interdiction()
and not self.confirm_ai_convoy_interdiction_launch()
):
return
negative_starts = self.negative_start_packages() negative_starts = self.negative_start_packages()
if negative_starts: if negative_starts:
if not self.confirm_negative_start_time(negative_starts): if not self.confirm_negative_start_time(negative_starts):

View File

@ -3,10 +3,19 @@ from typing import List, Optional
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QColor, QPen from PySide2.QtGui import QColor, QPen
from PySide2.QtWidgets import QGraphicsItem, QGraphicsLineItem from PySide2.QtWidgets import (
QAction,
QGraphicsItem,
QGraphicsLineItem,
QGraphicsSceneContextMenuEvent,
QGraphicsSceneHoverEvent,
QMenu,
)
from game.theater import ControlPoint from game.theater import ControlPoint
from game.theater.supplyroutes import SupplyRouteLink
from game.transfers import RoadTransferOrder from game.transfers import RoadTransferOrder
from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import COLORS from qt_ui.uiconstants import COLORS
@ -28,6 +37,7 @@ class SupplyRouteSegment(QGraphicsLineItem):
self.convoys = convoys self.convoys = convoys
self.setPen(self.make_pen()) self.setPen(self.make_pen())
self.setToolTip(self.make_tooltip()) self.setToolTip(self.make_tooltip())
self.setAcceptHoverEvents(True)
@cached_property @cached_property
def convoy_size(self) -> int: def convoy_size(self) -> int:
@ -71,3 +81,33 @@ class SupplyRouteSegment(QGraphicsLineItem):
@property @property
def has_convoys(self) -> bool: def has_convoys(self) -> bool:
return bool(self.convoys) return bool(self.convoys)
@property
def targetable(self) -> bool:
return self.convoys and not self.control_point_a.captured
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
# Can only plan missions against enemy supply routes that have convoys.
if not self.targetable:
super().contextMenuEvent(event)
return
menu = QMenu("Menu")
new_package_action = QAction(f"New package")
new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action)
menu.exec_(event.screenPos())
def open_new_package_dialog(self) -> None:
"""Opens the dialog for planning a new mission package."""
Dialog.open_new_package_dialog(
SupplyRouteLink(self.control_point_a, self.control_point_b)
)
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
if self.targetable:
self.setCursor(Qt.PointingHandCursor)
else:
super().hoverEnterEvent(event)