From 0e1dfb8ccbe81819ce8411c6cf11400240598f42 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 24 Sep 2020 19:32:10 -0700 Subject: [PATCH] Implement CAP and CAS for front lines. --- gen/flights/ai_flight_planner.py | 44 +++++----- qt_ui/widgets/map/QFrontLine.py | 82 +++++++++++++++++++ qt_ui/widgets/map/QLiberationMap.py | 9 +- .../windows/mission/flight/QFlightCreator.py | 24 ++++-- theater/__init__.py | 6 +- theater/frontline.py | 27 ++++++ theater/missiontarget.py | 7 -- 7 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 qt_ui/widgets/map/QFrontLine.py create mode 100644 theater/frontline.py diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index f6a665e4..29deb1f4 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -24,9 +24,7 @@ from gen.flights.flight import ( FlightWaypoint, FlightWaypointType, ) -from theater.controlpoint import ControlPoint -from theater.missiontarget import MissionTarget -from theater.theatergroundobject import TheaterGroundObject +from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject MISSION_DURATION = 80 @@ -119,9 +117,8 @@ class FlightPlanner: ) if len(self._get_cas_locations()) > 0: - enemy_cp = random.choice(self._get_cas_locations()) - location = enemy_cp - self.generate_frontline_cap(flight, flight.from_cp, enemy_cp) + location = random.choice(self._get_cas_locations()) + self.generate_frontline_cap(flight, location) else: location = flight.from_cp self.generate_barcap(flight, flight.from_cp) @@ -143,7 +140,7 @@ class FlightPlanner: self.doctrine["CAS_EVERY_X_MINUTES"] + 5) location = random.choice(cas_locations) - self.generate_cas(flight, flight.from_cp, location) + self.generate_cas(flight, location) self.plan_legacy_mission(flight, location) def commission_sead(self) -> None: @@ -196,14 +193,15 @@ class FlightPlanner: self.generate_strike(flight, location) self.plan_legacy_mission(flight, location) - def _get_cas_locations(self): + def _get_cas_locations(self) -> List[FrontLine]: return self._get_cas_locations_for_cp(self.from_cp) - def _get_cas_locations_for_cp(self, for_cp): + @staticmethod + def _get_cas_locations_for_cp(for_cp: ControlPoint) -> List[FrontLine]: cas_locations = [] for cp in for_cp.connected_points: if cp.captured != for_cp.captured: - cas_locations.append(cp) + cas_locations.append(FrontLine(for_cp, cp)) return cas_locations def compute_strike_targets(self): @@ -428,17 +426,17 @@ class FlightPlanner: rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb) + def generate_frontline_cap(self, flight: Flight, + front_line: FrontLine) -> None: + """Generate a CAP flight plan for the given front line. - def generate_frontline_cap(self, flight, ally_cp, enemy_cp): - """ - Generate a cap flight for the frontline between ally_cp and enemy cp in order to ensure air superiority and - protect friendly CAP airbase :param flight: Flight to setup - :param ally_cp: CP to protect - :param enemy_cp: Enemy connected cp + :param front_line: Front line to protect. """ + ally_cp, enemy_cp = front_line.control_points flight.flight_type = FlightType.CAP - patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1]) + patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], + self.doctrine["PATROL_ALT_RANGE"][1]) # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector(ally_cp, enemy_cp, self.game.theater) @@ -579,19 +577,21 @@ class FlightPlanner: rtb = self.generate_rtb_waypoint(flight.from_cp) flight.points.append(rtb) + def generate_cas(self, flight: Flight, front_line: FrontLine) -> None: + """Generate a CAS flight plan for the given target. - def generate_cas(self, flight, from_cp, location): - """ - Generate a CAS flight at a given location :param flight: Flight to setup - :param location: Location of the CAS targets + :param front_line: Front line containing CAS targets. """ + from_cp, location = front_line.control_points is_helo = hasattr(flight.unit_type, "helicopter") and flight.unit_type.helicopter cap_alt = 1000 flight.points = [] flight.flight_type = FlightType.CAS - ingress, heading, distance = Conflict.frontline_vector(from_cp, location, self.game.theater) + ingress, heading, distance = Conflict.frontline_vector( + from_cp, location, self.game.theater + ) center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py new file mode 100644 index 00000000..f1425893 --- /dev/null +++ b/qt_ui/widgets/map/QFrontLine.py @@ -0,0 +1,82 @@ +"""Common base for objects drawn on the game map.""" +from typing import Optional + +from PySide2.QtCore import Qt +from PySide2.QtGui import QPen +from PySide2.QtWidgets import ( + QAction, + QGraphicsLineItem, + QGraphicsSceneContextMenuEvent, + QGraphicsSceneHoverEvent, + QGraphicsSceneMouseEvent, + QMenu, +) + +import qt_ui.uiconstants as const +from qt_ui.dialogs import Dialog +from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog +from theater.missiontarget import MissionTarget + + +class QFrontLine(QGraphicsLineItem): + """Base class for objects drawn on the game map. + + Game map objects have an on_click behavior that triggers on left click, and + change the mouse cursor on hover. + """ + + def __init__(self, x1: float, y1: float, x2: float, y2: float, + mission_target: MissionTarget) -> None: + super().__init__(x1, y1, x2, y2) + self.mission_target = mission_target + self.new_package_dialog: Optional[QNewPackageDialog] = None + self.setAcceptHoverEvents(True) + + pen = QPen(brush=const.COLORS["bright_red"]) + pen.setColor(const.COLORS["orange"]) + pen.setWidth(8) + self.setPen(pen) + + def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): + self.setCursor(Qt.PointingHandCursor) + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + if event.button() == Qt.LeftButton: + self.on_click() + + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: + menu = QMenu("Menu") + + object_details_action = QAction(self.object_dialog_text) + object_details_action.triggered.connect(self.on_click) + menu.addAction(object_details_action) + + 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()) + + @property + def object_dialog_text(self) -> str: + """Text to for the object's dialog in the context menu. + + Right clicking a map object will open a context menu and the first item + will open the details dialog for this object. This menu action has the + same behavior as the on_click event. + + Return: + The text that should be displayed for the menu item. + """ + return "Details" + + def on_click(self) -> None: + """The action to take when this map object is left-clicked. + + Typically this should open a details view of the object. + """ + raise NotImplementedError + + def open_new_package_dialog(self) -> None: + """Opens the dialog for planning a new mission package.""" + Dialog.open_new_package_dialog(self.mission_target) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 19b3303a..a36382b5 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -21,8 +21,9 @@ from qt_ui.models import GameModel from qt_ui.widgets.map.QLiberationScene import QLiberationScene from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject +from qt_ui.widgets.map.QFrontLine import QFrontLine from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from theater import ControlPoint +from theater import ControlPoint, FrontLine class QLiberationMap(QGraphicsView): @@ -245,10 +246,8 @@ class QLiberationMap(QGraphicsView): p1 = point_from_heading(pos2[0], pos2[1], h+180, 25) p2 = point_from_heading(pos2[0], pos2[1], h, 25) - frontline_pen = QPen(brush=CONST.COLORS["bright_red"]) - frontline_pen.setColor(CONST.COLORS["orange"]) - frontline_pen.setWidth(8) - scene.addLine(p1[0], p1[1], p2[0], p2[1], pen=frontline_pen) + scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1], + FrontLine(cp, connected_cp))) else: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 00df380d..87693dfc 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -4,7 +4,6 @@ from typing import Optional from PySide2.QtCore import Qt, Signal from PySide2.QtWidgets import ( QDialog, - QMessageBox, QPushButton, QVBoxLayout, ) @@ -20,7 +19,7 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector -from theater import ControlPoint, TheaterGroundObject +from theater import ControlPoint, FrontLine, TheaterGroundObject class QFlightCreator(QDialog): @@ -109,9 +108,6 @@ class QFlightCreator(QDialog): def populate_flight_plan(self, flight: Flight, task: FlightType) -> None: # TODO: Flesh out mission types. - # Probably most important to add, since it's a regression, is CAS. Right - # now it's not possible to frag a package on a front line though, and - # that's the only location where CAS missions are valid. if task == FlightType.ANTISHIP: logging.error("Anti-ship flight plan generation not implemented") elif task == FlightType.BAI: @@ -121,7 +117,7 @@ class QFlightCreator(QDialog): elif task == FlightType.CAP: self.generate_cap(flight) elif task == FlightType.CAS: - logging.error("CAS flight plan generation not implemented") + self.generate_cas(flight) elif task == FlightType.DEAD: self.generate_sead(flight) elif task == FlightType.ELINT: @@ -147,14 +143,26 @@ class QFlightCreator(QDialog): "Troop transport flight plan generation not implemented" ) + def generate_cas(self, flight: Flight) -> None: + if not isinstance(self.package.target, FrontLine): + logging.error( + "Could not create flight plan: CAS missions only valid for " + "front lines" + ) + return + self.planner.generate_cas(flight, self.package.target) + def generate_cap(self, flight: Flight) -> None: - if not isinstance(self.package.target, ControlPoint): + if isinstance(self.package.target, TheaterGroundObject): logging.error( "Could not create flight plan: CAP missions for strike targets " "not implemented" ) return - self.planner.generate_barcap(flight, self.package.target) + if isinstance(self.package.target, FrontLine): + self.planner.generate_frontline_cap(flight, self.package.target) + else: + self.planner.generate_barcap(flight, self.package.target) def generate_sead(self, flight: Flight) -> None: self.planner.generate_sead(flight, self.package.target) diff --git a/theater/__init__.py b/theater/__init__.py index 282ea4f3..209a6646 100644 --- a/theater/__init__.py +++ b/theater/__init__.py @@ -1,3 +1,5 @@ -from .controlpoint import * -from .conflicttheater import * from .base import * +from .conflicttheater import * +from .controlpoint import * +from .frontline import FrontLine +from .missiontarget import MissionTarget diff --git a/theater/frontline.py b/theater/frontline.py new file mode 100644 index 00000000..6350e1ab --- /dev/null +++ b/theater/frontline.py @@ -0,0 +1,27 @@ +"""Battlefield front lines.""" +from typing import Tuple + +from . import ControlPoint, MissionTarget + + +class FrontLine(MissionTarget): + """Defines a front line location between two control points. + + Front lines are the area where ground combat happens. + """ + + def __init__(self, control_point_a: ControlPoint, + control_point_b: ControlPoint) -> None: + self.control_point_a = control_point_a + self.control_point_b = control_point_b + + @property + def control_points(self) -> Tuple[ControlPoint, ControlPoint]: + """Returns a tuple of the two control points.""" + return self.control_point_a, self.control_point_b + + @property + def name(self) -> str: + a = self.control_point_a.name + b = self.control_point_b.name + return f"Front line {a}/{b}" diff --git a/theater/missiontarget.py b/theater/missiontarget.py index fdb37eec..41c90ef9 100644 --- a/theater/missiontarget.py +++ b/theater/missiontarget.py @@ -1,7 +1,5 @@ from abc import ABC, abstractmethod -from dcs.mapping import Point - class MissionTarget(ABC): # TODO: These should just be required objects to the constructor @@ -11,8 +9,3 @@ class MissionTarget(ABC): @abstractmethod def name(self) -> str: """The name of the mission target.""" - - @property - @abstractmethod - def position(self) -> Point: - """The position of the mission target."""