mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
2b06d8a096
commit
3f16c0378a
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user