From 6ce82be46b655a044723bfc3d95d73dd107f19c3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 18:22:20 -0700 Subject: [PATCH] Set up split/join points. --- game/data/doctrine.py | 13 ++ game/game.py | 4 +- gen/ato.py | 9 + gen/flights/ai_flight_planner.py | 4 +- gen/flights/flight.py | 2 + gen/flights/flightplan.py | 156 +++++++++++------- gen/flights/waypointbuilder.py | 24 +++ qt_ui/dialogs.py | 9 +- qt_ui/widgets/QTopPanel.py | 4 +- qt_ui/widgets/ato.py | 2 +- qt_ui/widgets/map/QLiberationMap.py | 5 +- qt_ui/windows/GameUpdateSignal.py | 4 +- qt_ui/windows/QLiberationWindow.py | 2 + qt_ui/windows/mission/QEditFlightDialog.py | 5 +- qt_ui/windows/mission/QPackageDialog.py | 9 +- .../windows/mission/flight/QFlightCreator.py | 12 +- .../windows/mission/flight/QFlightPlanner.py | 5 +- .../generator/QAbstractMissionGenerator.py | 7 +- .../flight/generator/QCAPMissionGenerator.py | 37 +++-- .../flight/generator/QCASMissionGenerator.py | 18 +- .../flight/generator/QSEADMissionGenerator.py | 18 +- .../generator/QSTRIKEMissionGenerator.py | 18 +- .../flight/waypoints/QFlightWaypointTab.py | 36 +++- 23 files changed, 282 insertions(+), 121 deletions(-) diff --git a/game/data/doctrine.py b/game/data/doctrine.py index e7333096..d81c5484 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -14,9 +14,13 @@ class Doctrine: strike_max_range: int sead_max_range: int + rendezvous_altitude: int + join_distance: int + split_distance: int ingress_egress_distance: int ingress_altitude: int egress_altitude: int + min_patrol_altitude: int max_patrol_altitude: int pattern_altitude: int @@ -35,6 +39,9 @@ MODERN_DOCTRINE = Doctrine( antiship=True, strike_max_range=1500000, sead_max_range=1500000, + rendezvous_altitude=feet_to_meter(25000), + join_distance=nm_to_meter(20), + split_distance=nm_to_meter(20), ingress_egress_distance=nm_to_meter(45), ingress_altitude=feet_to_meter(20000), egress_altitude=feet_to_meter(20000), @@ -55,6 +62,9 @@ COLDWAR_DOCTRINE = Doctrine( antiship=True, strike_max_range=1500000, sead_max_range=1500000, + rendezvous_altitude=feet_to_meter(22000), + join_distance=nm_to_meter(10), + split_distance=nm_to_meter(10), ingress_egress_distance=nm_to_meter(30), ingress_altitude=feet_to_meter(18000), egress_altitude=feet_to_meter(18000), @@ -75,6 +85,9 @@ WWII_DOCTRINE = Doctrine( antiship=True, strike_max_range=1500000, sead_max_range=1500000, + join_distance=nm_to_meter(5), + split_distance=nm_to_meter(5), + rendezvous_altitude=feet_to_meter(10000), ingress_egress_distance=nm_to_meter(7), ingress_altitude=feet_to_meter(8000), egress_altitude=feet_to_meter(8000), diff --git a/game/game.py b/game/game.py index b9aead14..fc3e2305 100644 --- a/game/game.py +++ b/game/game.py @@ -89,6 +89,7 @@ class Game: ) self.sanitize_sides() + self.on_load() def sanitize_sides(self): @@ -204,9 +205,10 @@ class Game: else: return event and event.name and event.name == self.player_name - def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): + def on_load(self) -> None: ObjectiveDistanceCache.set_theater(self.theater) + def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.turn = self.turn + 1 diff --git a/gen/ato.py b/gen/ato.py index 2ad8b6ec..a2e4a8a1 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -13,6 +13,7 @@ from dataclasses import dataclass, field import logging from typing import Dict, Iterator, List, Optional +from dcs.mapping import Point from .flights.flight import Flight, FlightType from theater.missiontarget import MissionTarget @@ -39,6 +40,11 @@ class Package: #: The set of flights in the package. flights: List[Flight] = field(default_factory=list) + join_point: Optional[Point] = field(default=None, init=False, hash=False) + split_point: Optional[Point] = field(default=None, init=False, hash=False) + ingress_point: Optional[Point] = field(default=None, init=False, hash=False) + egress_point: Optional[Point] = field(default=None, init=False, hash=False) + def add_flight(self, flight: Flight) -> None: """Adds a flight to the package.""" self.flights.append(flight) @@ -46,6 +52,9 @@ class Package: def remove_flight(self, flight: Flight) -> None: """Removes a flight from the package.""" self.flights.remove(flight) + if not self.flights: + self.ingress_point = None + self.egress_point = None @property def primary_task(self) -> Optional[FlightType]: diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index d2abd7fd..6b584e68 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -414,9 +414,9 @@ class CoalitionMissionPlanner: return package = builder.build() - builder = FlightPlanBuilder(self.game, self.is_player, package) + builder = FlightPlanBuilder(self.game, package, self.is_player) for flight in package.flights: - builder.populate_flight_plan(flight, package.target) + builder.populate_flight_plan(flight) self.ato.add_package(package) def message(self, title, text) -> None: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 0c5c7956..676b6bd8 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -47,6 +47,8 @@ class FlightWaypointType(Enum): TARGET_GROUP_LOC = 13 # A target group approximate location TARGET_SHIP = 14 # A target ship known location CUSTOM = 15 # User waypoint (no specific behaviour) + JOIN = 16 + SPLIT = 17 class PredefinedWaypointCategory(Enum): diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 5e3dea03..2e595b31 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -38,8 +38,7 @@ class InvalidObjectiveLocation(RuntimeError): class FlightPlanBuilder: """Generates flight plans for flights.""" - def __init__(self, game: Game, is_player: bool, - package: Optional[Package] = None) -> None: + def __init__(self, game: Game, package: Package, is_player: bool) -> None: self.game = game self.package = package self.is_player = is_player @@ -49,9 +48,15 @@ class FlightPlanBuilder: faction = self.game.enemy_faction self.doctrine: Doctrine = faction.get("doctrine", MODERN_DOCTRINE) - def populate_flight_plan(self, flight: Flight, - objective_location: MissionTarget) -> None: + def populate_flight_plan( + self, flight: Flight, + # TODO: Custom targets should be an attribute of the flight. + custom_targets: Optional[List[Unit]] = None) -> None: """Creates a default flight plan for the given mission.""" + if flight not in self.package.flights: + raise RuntimeError("Flight must be a part of the package") + self.generate_missing_package_waypoints() + # TODO: Flesh out mission types. try: task = flight.flight_type @@ -62,17 +67,17 @@ class FlightPlanBuilder: elif task == FlightType.BAI: logging.error("BAI flight plan generation not implemented") elif task == FlightType.BARCAP: - self.generate_barcap(flight, objective_location) + self.generate_barcap(flight) elif task == FlightType.CAP: - self.generate_barcap(flight, objective_location) + self.generate_barcap(flight) elif task == FlightType.CAS: - self.generate_cas(flight, objective_location) + self.generate_cas(flight) elif task == FlightType.DEAD: - self.generate_sead(flight, objective_location) + self.generate_sead(flight, custom_targets) elif task == FlightType.ELINT: logging.error("ELINT flight plan generation not implemented") elif task == FlightType.ESCORT: - self.generate_escort(flight, objective_location) + self.generate_escort(flight) elif task == FlightType.EVAC: logging.error("Evac flight plan generation not implemented") elif task == FlightType.EWAR: @@ -88,11 +93,11 @@ class FlightPlanBuilder: elif task == FlightType.RECON: logging.error("Recon flight plan generation not implemented") elif task == FlightType.SEAD: - self.generate_sead(flight, objective_location) + self.generate_sead(flight, custom_targets) elif task == FlightType.STRIKE: - self.generate_strike(flight, objective_location) + self.generate_strike(flight) elif task == FlightType.TARCAP: - self.generate_frontline_cap(flight, objective_location) + self.generate_frontline_cap(flight) elif task == FlightType.TROOP_TRANSPORT: logging.error( "Troop transport flight plan generation not implemented" @@ -100,23 +105,32 @@ class FlightPlanBuilder: except InvalidObjectiveLocation as ex: logging.error(f"Could not create flight plan: {ex}") - def generate_strike(self, flight: Flight, location: MissionTarget) -> None: + def generate_missing_package_waypoints(self) -> None: + if self.package.ingress_point is None: + self.package.ingress_point = self._ingress_point() + if self.package.egress_point is None: + self.package.egress_point = self._egress_point() + if self.package.join_point is None: + self.package.join_point = self._join_point() + if self.package.split_point is None: + self.package.split_point = self._split_point() + + def generate_strike(self, flight: Flight) -> None: """Generates a strike flight plan. Args: flight: The flight to generate the flight plan for. - location: The strike target location. """ + location = self.package.target + # TODO: Support airfield strikes. if not isinstance(location, TheaterGroundObject): raise InvalidObjectiveLocation(flight.flight_type, location) - # TODO: Stop clobbering flight type. - flight.flight_type = FlightType.STRIKE - builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.ingress_strike(self.ingress_point(flight, location), location) + builder.join(self.package.join_point) + builder.ingress_strike(self.package.ingress_point, location) if len(location.groups) > 0 and location.dcs_identifier == "AA": # TODO: Replace with DEAD? @@ -143,26 +157,23 @@ class FlightPlanBuilder: location ) - builder.egress(self.egress_point(flight, location), location) + builder.egress(self.package.egress_point, location) + builder.split(self.package.split_point) builder.rtb(flight.from_cp) flight.points = builder.build() - def generate_barcap(self, flight: Flight, location: MissionTarget) -> None: + def generate_barcap(self, flight: Flight) -> None: """Generate a BARCAP flight at a given location. Args: flight: The flight to generate the flight plan for. - location: The control point to protect. """ + location = self.package.target + if isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - if isinstance(location, ControlPoint) and location.is_carrier: - flight.flight_type = FlightType.BARCAP - else: - flight.flight_type = FlightType.CAP - patrol_alt = random.randint( self.doctrine.min_patrol_altitude, self.doctrine.max_patrol_altitude @@ -198,19 +209,18 @@ class FlightPlanBuilder: builder.rtb(flight.from_cp) flight.points = builder.build() - def generate_frontline_cap(self, flight: Flight, - location: MissionTarget) -> None: + def generate_frontline_cap(self, flight: Flight) -> None: """Generate a CAP flight plan for the given front line. Args: flight: The flight to generate the flight plan for. - location: Front line to protect. """ + location = self.package.target + if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) ally_cp, enemy_cp = location.control_points - flight.flight_type = FlightType.CAP patrol_alt = random.randint(self.doctrine.min_patrol_altitude, self.doctrine.max_patrol_altitude) @@ -240,26 +250,26 @@ class FlightPlanBuilder: builder.rtb(flight.from_cp) flight.points = builder.build() - def generate_sead(self, flight: Flight, location: MissionTarget, - custom_targets: Optional[List[Unit]] = None) -> None: + def generate_sead(self, flight: Flight, + custom_targets: Optional[List[Unit]]) -> None: """Generate a SEAD/DEAD flight at a given location. Args: flight: The flight to generate the flight plan for. - location: Location of the SAM site. custom_targets: Specific radar equipped units selected by the user. """ + location = self.package.target + if not isinstance(location, TheaterGroundObject): raise InvalidObjectiveLocation(flight.flight_type, location) if custom_targets is None: custom_targets = [] - flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) - builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.ingress_sead(self.ingress_point(flight, location), location) + builder.join(self.package.join_point) + builder.ingress_sead(self.package.ingress_point, location) # TODO: Unify these. # There doesn't seem to be any reason to treat the UI fragged missions @@ -283,14 +293,13 @@ class FlightPlanBuilder: else: builder.sead_area(location) - builder.egress(self.egress_point(flight, location), location) + builder.egress(self.package.egress_point, location) + builder.split(self.package.split_point) builder.rtb(flight.from_cp) flight.points = builder.build() - def generate_escort(self, flight: Flight, location: MissionTarget) -> None: - flight.flight_type = FlightType.ESCORT - + def generate_escort(self, flight: Flight) -> None: # TODO: Decide common waypoints for the package ahead of time. # Packages should determine some common points like push, ingress, # egress, and split points ahead of time so they can be shared by all @@ -303,25 +312,30 @@ class FlightPlanBuilder: builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.race_track(self.ingress_point(flight, location), - self.egress_point(flight, location), patrol_alt) + builder.join(self.package.join_point) + builder.race_track( + self.package.ingress_point, + self.package.egress_point, + patrol_alt + ) + builder.split(self.package.split_point) builder.rtb(flight.from_cp) flight.points = builder.build() - def generate_cas(self, flight: Flight, location: MissionTarget) -> None: + def generate_cas(self, flight: Flight) -> None: """Generate a CAS flight plan for the given target. Args: flight: The flight to generate the flight plan for. - location: Front line with CAS targets. """ + location = self.package.target + if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) is_helo = getattr(flight.unit_type, "helicopter", False) cap_alt = 500 if is_helo else 1000 - flight.flight_type = FlightType.CAS ingress, heading, distance = Conflict.frontline_vector( location.control_points[0], location.control_points[1], @@ -332,9 +346,11 @@ class FlightPlanBuilder: builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp, is_helo) + builder.join(self.package.join_point) builder.ingress_cas(ingress, location) builder.cas(center, cap_alt) builder.egress(egress, location) + builder.split(self.package.split_point) builder.rtb(flight.from_cp, is_helo) flight.points = builder.build() @@ -370,33 +386,51 @@ class FlightPlanBuilder: builder.land(arrival) return builder.build()[0] - def ingress_point(self, flight: Flight, target: MissionTarget) -> Point: - heading = self._heading_to_package_airfield(flight, target) - return target.position.point_from_heading( + def _join_point(self) -> Point: + ingress_point = self.package.ingress_point + heading = self._heading_to_package_airfield(ingress_point) + return ingress_point.point_from_heading(heading, + -self.doctrine.join_distance) + + def _split_point(self) -> Point: + egress_point = self.package.egress_point + heading = self._heading_to_package_airfield(egress_point) + return egress_point.point_from_heading(heading, + -self.doctrine.split_distance) + + def _ingress_point(self) -> Point: + heading = self._target_heading_to_package_airfield() + return self.package.target.position.point_from_heading( heading - 180 + 25, self.doctrine.ingress_egress_distance ) - def egress_point(self, flight: Flight, target: MissionTarget) -> Point: - heading = self._heading_to_package_airfield(flight, target) - return target.position.point_from_heading( + def _egress_point(self) -> Point: + heading = self._target_heading_to_package_airfield() + return self.package.target.position.point_from_heading( heading - 180 - 25, self.doctrine.ingress_egress_distance ) - def _heading_to_package_airfield(self, flight: Flight, - target: MissionTarget) -> int: - airfield = self.package_airfield(flight, target) - return airfield.position.heading_between_point(target.position) + def _target_heading_to_package_airfield(self) -> int: + return self._heading_to_package_airfield(self.package.target.position) + + def _heading_to_package_airfield(self, point: Point) -> int: + return self.package_airfield().position.heading_between_point(point) # TODO: Set ingress/egress/join/split points in the Package. - def package_airfield(self, flight: Flight, - target: MissionTarget) -> ControlPoint: + def package_airfield(self) -> ControlPoint: + # We'll always have a package, but if this is being planned via the UI + # it could be the first flight in the package. + if not self.package.flights: + raise RuntimeError( + "Cannot determine source airfield for package with no flights" + ) + # The package airfield is either the flight's airfield (when there is no # package) or the closest airfield to the objective that is the # departure airfield for some flight in the package. - if self.package is None: - return flight.from_cp - - cache = ObjectiveDistanceCache.get_closest_airfields(target) + cache = ObjectiveDistanceCache.get_closest_airfields( + self.package.target + ) for airfield in cache.closest_airfields: for flight in self.package.flights: if flight.from_cp == airfield: diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 7ef4a30e..8481b6ba 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -86,6 +86,30 @@ class WaypointBuilder: waypoint.pretty_name = "Land" self.waypoints.append(waypoint) + def join(self, position: Point) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.JOIN, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "Join" + waypoint.description = "Rendezvous with package" + waypoint.name = "JOIN" + self.waypoints.append(waypoint) + + def split(self, position: Point) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.SPLIT, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "Split" + waypoint.description = "Depart from package" + waypoint.name = "SPLIT" + self.waypoints.append(waypoint) + def ingress_cas(self, position: Point, objective: MissionTarget) -> None: self._ingress(FlightWaypointType.INGRESS_CAS, position, objective) diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py index 16920a15..e09dd92a 100644 --- a/qt_ui/dialogs.py +++ b/qt_ui/dialogs.py @@ -54,7 +54,12 @@ class Dialog: cls.edit_package_dialog.show() @classmethod - def open_edit_flight_dialog(cls, flight: Flight): + def open_edit_flight_dialog(cls, package_model: PackageModel, + flight: Flight) -> None: """Opens the dialog to edit the given flight.""" - cls.edit_flight_dialog = QEditFlightDialog(cls.game_model.game, flight) + cls.edit_flight_dialog = QEditFlightDialog( + cls.game_model.game, + package_model.package, + flight + ) cls.edit_flight_dialog.show() diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index f2f73b4f..fae3a11d 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -1,3 +1,5 @@ +from typing import Optional + from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton import qt_ui.uiconstants as CONST @@ -74,7 +76,7 @@ class QTopPanel(QFrame): self.layout.setContentsMargins(0,0,0,0) self.setLayout(self.layout) - def setGame(self, game:Game): + def setGame(self, game: Optional[Game]): self.game = game if game is not None: self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index ae9a5db3..e4c178c4 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -123,7 +123,7 @@ class QFlightPanel(QGroupBox): return from qt_ui.dialogs import Dialog Dialog.open_edit_flight_dialog( - self.package_model.flight_at_index(index) + self.package_model, self.package_model.flight_at_index(index) ) def on_delete(self) -> None: diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 3f5b026a..8654a364 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from PySide2.QtCore import Qt from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent @@ -43,6 +43,7 @@ class QLiberationMap(QGraphicsView): super(QLiberationMap, self).__init__() QLiberationMap.instance = self self.game_model = game_model + self.game: Optional[Game] = game_model.game self.flight_path_items: List[QGraphicsItem] = [] @@ -71,7 +72,7 @@ class QLiberationMap(QGraphicsView): def connectSignals(self): GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) - def setGame(self, game: Game): + def setGame(self, game: Optional[Game]): self.game = game print("Reloading Map Canvas") if self.game is not None: diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index 3e855149..8a52d555 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + from PySide2.QtCore import QObject, Signal from game import Game @@ -31,7 +33,7 @@ class GameUpdateSignal(QObject): # noinspection PyUnresolvedReferences self.flight_paths_changed.emit() - def updateGame(self, game: Game): + def updateGame(self, game: Optional[Game]): # noinspection PyUnresolvedReferences self.gameupdated.emit(game) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 9543ab66..50a4b120 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -232,6 +232,8 @@ class QLiberationWindow(QMainWindow): sys.exit(0) def setGame(self, game: Optional[Game]): + if game is not None: + game.on_load() self.game = game if self.info_panel: self.info_panel.setGame(game) diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py index 24fdfae2..9f795b79 100644 --- a/qt_ui/windows/mission/QEditFlightDialog.py +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -5,6 +5,7 @@ from PySide2.QtWidgets import ( ) from game import Game +from gen.ato import Package from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal @@ -14,7 +15,7 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner class QEditFlightDialog(QDialog): """Dialog window for editing flight plans and loadouts.""" - def __init__(self, game: Game, flight: Flight) -> None: + def __init__(self, game: Game, package: Package, flight: Flight) -> None: super().__init__() self.game = game @@ -24,7 +25,7 @@ class QEditFlightDialog(QDialog): layout = QVBoxLayout() - self.flight_planner = QFlightPlanner(flight, game) + self.flight_planner = QFlightPlanner(package, flight, game) layout.addWidget(self.flight_planner) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 37b73d04..21a44aa3 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -14,6 +14,7 @@ from PySide2.QtWidgets import ( from game.game import Game from gen.ato import Package from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder from qt_ui.models import AtoModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.ato import QFlightList @@ -100,15 +101,17 @@ class QPackageDialog(QDialog): def on_add_flight(self) -> None: """Opens the new flight dialog.""" - self.add_flight_dialog = QFlightCreator( - self.game, self.package_model.package - ) + self.add_flight_dialog = QFlightCreator(self.game, + self.package_model.package) self.add_flight_dialog.created.connect(self.add_flight) self.add_flight_dialog.show() def add_flight(self, flight: Flight) -> None: """Adds the new flight to the package.""" self.package_model.add_flight(flight) + planner = FlightPlanBuilder(self.game, self.package_model.package, + is_player=True) + planner.populate_flight_plan(flight) # noinspection PyUnresolvedReferences self.package_changed.emit() # noinspection PyUnresolvedReferences diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 24fb684f..fc3f9415 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -1,4 +1,3 @@ -import logging from typing import Optional from PySide2.QtCore import Qt, Signal @@ -11,15 +10,14 @@ from dcs.planes import PlaneType from game import Game from gen.ato import Package -from gen.flights.flightplan import FlightPlanBuilder -from gen.flights.flight import Flight, FlightType +from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner 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, FrontLine, TheaterGroundObject +from theater import ControlPoint class QFlightCreator(QDialog): @@ -29,9 +27,6 @@ class QFlightCreator(QDialog): super().__init__() self.game = game - self.package = package - - self.planner = FlightPlanBuilder(self.game, is_player=True) self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) @@ -39,7 +34,7 @@ class QFlightCreator(QDialog): layout = QVBoxLayout() self.task_selector = QFlightTypeComboBox( - self.game.theater, self.package.target + self.game.theater, package.target ) self.task_selector.setCurrentIndex(0) layout.addLayout(QLabeledWidget("Task:", self.task_selector)) @@ -95,7 +90,6 @@ class QFlightCreator(QDialog): size = self.flight_size_spinner.value() flight = Flight(aircraft, size, origin, task) - self.planner.populate_flight_plan(flight, self.package.target) # noinspection PyUnresolvedReferences self.created.emit(flight) diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index 4eed4754..af48219c 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -2,6 +2,7 @@ from PySide2.QtCore import Signal from PySide2.QtWidgets import QTabWidget from game import Game +from gen.ato import Package from gen.flights.flight import Flight from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ QFlightPayloadTab @@ -15,14 +16,14 @@ class QFlightPlanner(QTabWidget): on_planned_flight_changed = Signal() - def __init__(self, flight: Flight, game: Game): + def __init__(self, package: Package, flight: Flight, game: Game): super().__init__() self.general_settings_tab = QGeneralFlightSettingsTab(game, flight) self.general_settings_tab.on_flight_settings_changed.connect( lambda: self.on_planned_flight_changed.emit()) self.payload_tab = QFlightPayloadTab(flight, game) - self.waypoint_tab = QFlightWaypointTab(game, flight) + self.waypoint_tab = QFlightWaypointTab(game, package, flight) self.waypoint_tab.on_flight_changed.connect( lambda: self.on_planned_flight_changed.emit()) self.addTab(self.general_settings_tab, "General Flight settings") diff --git a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py index c18729c6..4811da1d 100644 --- a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py @@ -2,6 +2,7 @@ from PySide2.QtCore import Qt from PySide2.QtWidgets import QDialog, QPushButton from game import Game +from gen.ato import Package from gen.flights.flight import Flight from gen.flights.flightplan import FlightPlanBuilder from qt_ui.uiconstants import EVENT_ICONS @@ -10,9 +11,11 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFligh class QAbstractMissionGenerator(QDialog): - def __init__(self, game: Game, flight: Flight, flight_waypoint_list, title): + def __init__(self, game: Game, package: Package, flight: Flight, + flight_waypoint_list, title) -> None: super(QAbstractMissionGenerator, self).__init__() self.game = game + self.package = package self.flight = flight self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setMinimumSize(400, 250) @@ -20,7 +23,7 @@ class QAbstractMissionGenerator(QDialog): self.setWindowTitle(title) self.setWindowIcon(EVENT_ICONS["strike"]) self.flight_waypoint_list = flight_waypoint_list - self.planner = FlightPlanBuilder(self.game, is_player=True) + self.planner = FlightPlanBuilder(self.game, package, is_player=True) self.selected_waypoints = [] self.wpt_info = QFlightWaypointInfoBox() diff --git a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py index c1f5591e..b376851d 100644 --- a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py @@ -1,15 +1,26 @@ +import logging + from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout from game import Game -from gen.flights.flight import Flight, PredefinedWaypointCategory +from gen.ato import Package +from gen.flights.flight import Flight, FlightType from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator +from theater import ControlPoint, FrontLine class QCAPMissionGenerator(QAbstractMissionGenerator): - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCAPMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAP Generator") + def __init__(self, game: Game, package: Package, flight: Flight, + flight_waypoint_list) -> None: + super(QCAPMissionGenerator, self).__init__( + game, + package, + flight, + flight_waypoint_list, + "CAP Generator" + ) self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, True, True, False, False, True) self.wpt_selection_box.setMinimumWidth(200) @@ -34,16 +45,22 @@ class QCAPMissionGenerator(QAbstractMissionGenerator): self.setLayout(layout) def apply(self): - self.flight.points = [] - - wpt = self.selected_waypoints[0] - if wpt.category == PredefinedWaypointCategory.FRONTLINE: - self.planner.generate_frontline_cap(self.flight, wpt.data[0], wpt.data[1]) - elif wpt.category == PredefinedWaypointCategory.ALLY_CP: - self.planner.generate_barcap(self.flight, wpt.data) + location = self.package.target + if isinstance(location, FrontLine): + self.flight.flight_type = FlightType.TARCAP + self.planner.populate_flight_plan(self.flight) + elif isinstance(location, ControlPoint): + if location.is_fleet: + self.flight.flight_type = FlightType.BARCAP + else: + self.flight.flight_type = FlightType.CAP else: + name = location.__class__.__name__ + logging.error(f"Unexpected objective type for CAP: {name}") return + self.planner.generate_barcap(self.flight) + self.flight_waypoint_list.update_list() self.close() diff --git a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py index cfae4e52..76177772 100644 --- a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py @@ -4,15 +4,23 @@ from dcs import Point from game import Game from game.utils import meter_to_nm -from gen.flights.flight import Flight +from gen.ato import Package +from gen.flights.flight import Flight, FlightType from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator class QCASMissionGenerator(QAbstractMissionGenerator): - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QCASMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "CAS Generator") + def __init__(self, game: Game, package: Package, flight: Flight, + flight_waypoint_list) -> None: + super(QCASMissionGenerator, self).__init__( + game, + package, + flight, + flight_waypoint_list, + "CAS Generator" + ) self.wpt_selection_box = QPredefinedWaypointSelectionComboBox(self.game, self, False, False, True, False, False) self.wpt_selection_box.setMinimumWidth(200) @@ -55,8 +63,8 @@ class QCASMissionGenerator(QAbstractMissionGenerator): self.setLayout(layout) def apply(self): - self.flight.points = [] - self.planner.generate_cas(self.flight, self.selected_waypoints[0].data[0], self.selected_waypoints[0].data[1]) + self.flight.flight_type = FlightType.CAS + self.planner.populate_flight_plan(self.flight) self.flight_waypoint_list.update_list() self.close() diff --git a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py index 7221844c..dd3a31f8 100644 --- a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py @@ -3,7 +3,8 @@ from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox from game import Game from game.utils import meter_to_nm -from gen.flights.flight import Flight +from gen.ato import Package +from gen.flights.flight import Flight, FlightType from qt_ui.widgets.combos.QSEADTargetSelectionComboBox import QSEADTargetSelectionComboBox from qt_ui.widgets.views.QSeadTargetInfoView import QSeadTargetInfoView from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator @@ -11,8 +12,15 @@ from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAb class QSEADMissionGenerator(QAbstractMissionGenerator): - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSEADMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "SEAD/DEAD Generator") + def __init__(self, game: Game, package: Package, flight: Flight, + flight_waypoint_list) -> None: + super(QSEADMissionGenerator, self).__init__( + game, + package, + flight, + flight_waypoint_list, + "SEAD/DEAD Generator" + ) self.tgt_selection_box = QSEADTargetSelectionComboBox(self.game) self.tgt_selection_box.setMinimumWidth(200) @@ -73,9 +81,9 @@ class QSEADMissionGenerator(QAbstractMissionGenerator): self.setLayout(layout) def apply(self): - self.flight.points = [] target = self.tgt_selection_box.get_selected_target() - self.planner.generate_sead(self.flight, target.location, target.radars) + self.flight.flight_type = FlightType.SEAD + self.planner.populate_flight_plan(self.flight, target.radars) self.flight_waypoint_list.update_list() self.close() diff --git a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py index 6da88e0b..c210208c 100644 --- a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py @@ -3,7 +3,8 @@ from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox from game import Game from game.utils import meter_to_nm -from gen.flights.flight import Flight +from gen.ato import Package +from gen.flights.flight import Flight, FlightType from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import QStrikeTargetSelectionComboBox from qt_ui.widgets.views.QStrikeTargetInfoView import QStrikeTargetInfoView from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAbstractMissionGenerator @@ -11,8 +12,15 @@ from qt_ui.windows.mission.flight.generator.QAbstractMissionGenerator import QAb class QSTRIKEMissionGenerator(QAbstractMissionGenerator): - def __init__(self, game: Game, flight: Flight, flight_waypoint_list): - super(QSTRIKEMissionGenerator, self).__init__(game, flight, flight_waypoint_list, "Strike Generator") + def __init__(self, game: Game, package: Package, flight: Flight, + flight_waypoint_list) -> None: + super(QSTRIKEMissionGenerator, self).__init__( + game, + package, + flight, + flight_waypoint_list, + "Strike Generator" + ) self.tgt_selection_box = QStrikeTargetSelectionComboBox(self.game) self.tgt_selection_box.setMinimumWidth(200) @@ -53,9 +61,9 @@ class QSTRIKEMissionGenerator(QAbstractMissionGenerator): self.setLayout(layout) def apply(self): - self.flight.points = [] target = self.tgt_selection_box.get_selected_target() - self.planner.generate_strike(self.flight, target.location) + self.flight.flight_type = FlightType.STRIKE + self.planner.populate_flight_plan(self.flight, target.location) self.flight_waypoint_list.update_list() self.close() diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 9db48a09..7561d639 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -2,6 +2,7 @@ from PySide2.QtCore import Signal from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLayout from game import Game +from gen.ato import Package from gen.flights.flight import Flight from gen.flights.flightplan import FlightPlanBuilder from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator @@ -16,11 +17,12 @@ class QFlightWaypointTab(QFrame): on_flight_changed = Signal() - def __init__(self, game: Game, flight: Flight): + def __init__(self, game: Game, package: Package, flight: Flight): super(QFlightWaypointTab, self).__init__() - self.flight = flight self.game = game - self.planner = FlightPlanBuilder(self.game, is_player=True) + self.package = package + self.flight = flight + self.planner = FlightPlanBuilder(self.game, package, is_player=True) self.init_ui() def init_ui(self): @@ -104,22 +106,42 @@ class QFlightWaypointTab(QFrame): self.on_change() def on_cas_generator(self): - self.subwindow = QCASMissionGenerator(self.game, self.flight, self.flight_waypoint_list) + self.subwindow = QCASMissionGenerator( + self.game, + self.package, + self.flight, + self.flight_waypoint_list + ) self.subwindow.finished.connect(self.on_change) self.subwindow.show() def on_cap_generator(self): - self.subwindow = QCAPMissionGenerator(self.game, self.flight, self.flight_waypoint_list) + self.subwindow = QCAPMissionGenerator( + self.game, + self.package, + self.flight, + self.flight_waypoint_list + ) self.subwindow.finished.connect(self.on_change) self.subwindow.show() def on_sead_generator(self): - self.subwindow = QSEADMissionGenerator(self.game, self.flight, self.flight_waypoint_list) + self.subwindow = QSEADMissionGenerator( + self.game, + self.package, + self.flight, + self.flight_waypoint_list + ) self.subwindow.finished.connect(self.on_change) self.subwindow.show() def on_strike_generator(self): - self.subwindow = QSTRIKEMissionGenerator(self.game, self.flight, self.flight_waypoint_list) + self.subwindow = QSTRIKEMissionGenerator( + self.game, + self.package, + self.flight, + self.flight_waypoint_list + ) self.subwindow.finished.connect(self.on_change) self.subwindow.show()