From 80f2b7a1dbcea44b2fc5e10d3a9dfaa3f4f5183c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 13 Sep 2020 22:51:41 -0700 Subject: [PATCH 01/48] Cleanups in map object UI. * Fix the context menu * Remove unnecessary overrides * Clean up formatting/naming * Factor out base class for shared behavior --- qt_ui/widgets/map/QMapControlPoint.py | 101 ++++++++++---------------- qt_ui/widgets/map/QMapGroundObject.py | 84 ++++++++++----------- qt_ui/widgets/map/QMapObject.py | 28 +++++++ 3 files changed, 107 insertions(+), 106 deletions(-) create mode 100644 qt_ui/widgets/map/QMapObject.py diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index 09061e16..a006cb5a 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -1,29 +1,32 @@ -from PySide2.QtCore import QRect, Qt -from PySide2.QtGui import QColor, QPainter -from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsSceneHoverEvent, QGraphicsSceneContextMenuEvent, QMenu, \ - QAction, QGraphicsSceneMouseEvent +from typing import Optional -import qt_ui.uiconstants as CONST +from PySide2.QtGui import QColor, QPainter +from PySide2.QtWidgets import ( + QAction, + QGraphicsSceneContextMenuEvent, + QMenu, +) + +import qt_ui.uiconstants as const from game import Game from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 -from theater import ControlPoint, db +from theater import ControlPoint +from .QMapObject import QMapObject -class QMapControlPoint(QGraphicsRectItem): +class QMapControlPoint(QMapObject): - def __init__(self, parent, x: float, y: float, w: float, h: float, model: ControlPoint, game: Game): - super(QMapControlPoint, self).__init__(x, y, w, h) + def __init__(self, parent, x: float, y: float, w: float, h: float, + model: ControlPoint, game: Game) -> None: + super().__init__(x, y, w, h) self.model = model self.game = game self.parent = parent - self.setAcceptHoverEvents(True) self.setZValue(1) self.setToolTip(self.model.name) + self.base_details_dialog: Optional[QBaseMenu2] = None - - def paint(self, painter, option, widget=None): - #super(QMapControlPoint, self).paint(painter, option, widget) - + def paint(self, painter, option, widget=None) -> None: if self.parent.get_display_rule("cp"): painter.save() painter.setRenderHint(QPainter.Antialiasing) @@ -32,69 +35,43 @@ class QMapControlPoint(QGraphicsRectItem): if self.model.has_runway(): if self.isUnderMouse(): - painter.setBrush(CONST.COLORS["white"]) + painter.setBrush(const.COLORS["white"]) painter.setPen(self.pen_color) r = option.rect painter.drawEllipse(r.x(), r.y(), r.width(), r.height()) - - #gauge = QRect(r.x(), - # r.y()+CONST.CP_SIZE/2 + 2, - # r.width(), - # CONST.CP_SIZE / 4) - - #painter.setBrush(CONST.COLORS["bright_red"]) - #painter.setPen(CONST.COLORS["black"]) - #painter.drawRect(gauge) - - #gauge2 = QRect(r.x(), - # r.y() + CONST.CP_SIZE / 2 + 2, - # r.width()*self.model.base.strength, - # CONST.CP_SIZE / 4) - - #painter.setBrush(CONST.COLORS["green"]) - #painter.drawRect(gauge2) - else: - # TODO : not drawing sunk carriers. Can be improved to display sunk carrier. - pass + # TODO: Draw sunk carriers differently. + # Either don't draw them at all, or perhaps use a sunk ship icon. painter.restore() - def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent): - self.update() - - def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - - def mousePressEvent(self, event:QGraphicsSceneMouseEvent): - self.openBaseMenu() - #self.contextMenuEvent(event) - - def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent): - + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: if self.model.captured: - openBaseMenu = QAction("Open base menu") + text = "Open base menu" else: - openBaseMenu = QAction("Open intel menu") + text = "Open intel menu" - openBaseMenu.triggered.connect(self.openBaseMenu) + open_menu = QAction(text) + open_menu.triggered.connect(self.on_click) menu = QMenu("Menu", self.parent) - menu.addAction(openBaseMenu) + menu.addAction(open_menu) menu.exec_(event.screenPos()) - @property - def brush_color(self)->QColor: - return self.model.captured and CONST.COLORS["blue"] or CONST.COLORS["super_red"] + def brush_color(self) -> QColor: + if self.model.captured: + return const.COLORS["blue"] + else: + return const.COLORS["super_red"] @property def pen_color(self) -> QColor: - return self.model.captured and CONST.COLORS["white"] or CONST.COLORS["white"] + return const.COLORS["white"] - def openBaseMenu(self): - self.baseMenu = QBaseMenu2(self.window(), self.model, self.game) - self.baseMenu.show() \ No newline at end of file + def on_click(self) -> None: + self.base_details_dialog = QBaseMenu2( + self.window(), + self.model, + self.game + ) + self.base_details_dialog.show() diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index a79ce1ab..89d1bf07 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -1,33 +1,37 @@ -from PySide2.QtCore import QPoint, QRect, QPointF, Qt -from PySide2.QtGui import QPainter, QBrush -from PySide2.QtWidgets import QGraphicsRectItem, QGraphicsItem, QGraphicsSceneHoverEvent, QGraphicsSceneMouseEvent +from typing import List, Optional -import qt_ui.uiconstants as CONST -from game import db, Game +from PySide2.QtCore import QRect +from PySide2.QtGui import QBrush +from PySide2.QtWidgets import QGraphicsItem + +import qt_ui.uiconstants as const +from game import Game from game.data.building_data import FORTIFICATION_BUILDINGS from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from theater import TheaterGroundObject, ControlPoint +from .QMapObject import QMapObject -class QMapGroundObject(QGraphicsRectItem): - - def __init__(self, parent, x: float, y: float, w: float, h: float, cp: ControlPoint, model: TheaterGroundObject, game:Game, buildings=[]): - super(QMapGroundObject, self).__init__(x, y, w, h) +class QMapGroundObject(QMapObject): + def __init__(self, parent, x: float, y: float, w: float, h: float, + cp: ControlPoint, model: TheaterGroundObject, game: Game, + buildings: Optional[List[TheaterGroundObject]] = None) -> None: + super().__init__(x, y, w, h) self.model = model self.cp = cp self.parent = parent self.game = game - self.setAcceptHoverEvents(True) self.setZValue(2) - self.buildings = buildings + self.buildings = buildings if buildings is not None else [] self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) + self.ground_object_dialog: Optional[QGroundObjectMenu] = None if len(self.model.groups) > 0: units = {} for g in self.model.groups: print(g) for u in g.units: - if u.type in units.keys(): + if u.type in units: units[u.type] = units[u.type]+1 else: units[u.type] = 1 @@ -42,14 +46,9 @@ class QMapGroundObject(QGraphicsRectItem): tooltip = tooltip + str(building.dcs_identifier) + "\n" self.setToolTip(tooltip[:-1]) - def mousePressEvent(self, event:QGraphicsSceneMouseEvent): - self.openEditionMenu() - - def paint(self, painter, option, widget=None): - #super(QMapControlPoint, self).paint(painter, option, widget) - - playerIcons = "_blue" - enemyIcons = "" + def paint(self, painter, option, widget=None) -> None: + player_icons = "_blue" + enemy_icons = "" if self.parent.get_display_rule("go"): painter.save() @@ -58,7 +57,8 @@ class QMapGroundObject(QGraphicsRectItem): if cat == "aa" and self.model.sea_object: cat = "ship" - rect = QRect(option.rect.x()+2,option.rect.y(),option.rect.width()-2,option.rect.height()) + rect = QRect(option.rect.x() + 2, option.rect.y(), + option.rect.width() - 2, option.rect.height()) is_dead = self.model.is_dead for building in self.buildings: @@ -67,16 +67,16 @@ class QMapGroundObject(QGraphicsRectItem): break if not is_dead and not self.cp.captured: - painter.drawPixmap(rect, CONST.ICONS[cat + enemyIcons]) + painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) elif not is_dead: - painter.drawPixmap(rect, CONST.ICONS[cat + playerIcons]) + painter.drawPixmap(rect, const.ICONS[cat + player_icons]) else: - painter.drawPixmap(rect, CONST.ICONS["destroyed"]) + painter.drawPixmap(rect, const.ICONS["destroyed"]) - self.drawHealthGauge(painter, option) + self.draw_health_gauge(painter, option) painter.restore() - def drawHealthGauge(self, painter, option): + def draw_health_gauge(self, painter, option) -> None: units_alive = 0 units_dead = 0 @@ -97,22 +97,18 @@ class QMapGroundObject(QGraphicsRectItem): if units_dead + units_alive > 0: ratio = float(units_alive)/(float(units_dead) + float(units_alive)) bar_height = ratio * option.rect.height() - painter.fillRect(option.rect.x(), option.rect.y(), 2, option.rect.height(), QBrush(CONST.COLORS["dark_red"])) - painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, QBrush(CONST.COLORS["green"])) - - - def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent): - self.update() - self.setCursor(Qt.PointingHandCursor) - - def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent): - self.update() - - def openEditionMenu(self): - self.editionMenu = QGroundObjectMenu(self.window(), self.model, self.buildings, self.cp, self.game) - self.editionMenu.show() + painter.fillRect(option.rect.x(), option.rect.y(), 2, + option.rect.height(), + QBrush(const.COLORS["dark_red"])) + painter.fillRect(option.rect.x(), option.rect.y(), 2, bar_height, + QBrush(const.COLORS["green"])) + def on_click(self) -> None: + self.ground_object_dialog = QGroundObjectMenu( + self.window(), + self.model, + self.buildings, + self.cp, + self.game + ) + self.ground_object_dialog.show() diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py new file mode 100644 index 00000000..1e29c7dd --- /dev/null +++ b/qt_ui/widgets/map/QMapObject.py @@ -0,0 +1,28 @@ +"""Common base for objects drawn on the game map.""" +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QGraphicsRectItem, + QGraphicsSceneHoverEvent, + QGraphicsSceneMouseEvent, +) + + +class QMapObject(QGraphicsRectItem): + """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, x: float, y: float, w: float, h: float): + super().__init__(x, y, w, h) + self.setAcceptHoverEvents(True) + + def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): + self.setCursor(Qt.PointingHandCursor) + + def mousePressEvent(self, event: QGraphicsSceneMouseEvent): + if event.button() == Qt.LeftButton: + self.on_click() + + def on_click(self) -> None: + raise NotImplementedError From 0eee5747af5d0372e7de7d522a54593e51cccd51 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 22 Sep 2020 21:47:15 -0700 Subject: [PATCH 02/48] Clean up flight path drawing code. --- qt_ui/widgets/map/QLiberationMap.py | 110 ++++++++++++++++++---------- 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 41194ca0..40109381 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,18 +1,22 @@ import typing -from typing import Dict +from typing import Dict, Tuple -from PySide2 import QtCore -from PySide2.QtCore import Qt, QRect, QPointF -from PySide2.QtGui import QPixmap, QBrush, QColor, QWheelEvent, QPen, QFont -from PySide2.QtWidgets import QGraphicsView, QFrame, QGraphicsOpacityEffect +from PySide2.QtCore import Qt +from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent +from PySide2.QtWidgets import ( + QFrame, + QGraphicsOpacityEffect, + QGraphicsScene, + QGraphicsView, +) from dcs import Point from dcs.mapping import point_from_heading import qt_ui.uiconstants as CONST from game import Game, db from game.data.radar_db import UNITS_WITH_RADAR -from game.event import UnitsDeliveryEvent, Event, ControlPointType from gen import Conflict +from gen.flights.flight import Flight from qt_ui.widgets.map.QLiberationScene import QLiberationScene from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject @@ -21,6 +25,7 @@ from theater import ControlPoint class QLiberationMap(QGraphicsView): + WAYPOINT_SIZE = 4 instance = None display_rules: Dict[str, bool] = { @@ -168,38 +173,8 @@ class QLiberationMap(QGraphicsView): if self.get_display_rule("lines"): self.scene_create_lines_for_cp(cp, playerColor, enemyColor) - for cp in self.game.theater.controlpoints: - - if cp.captured: - pen = QPen(brush=CONST.COLORS[playerColor]) - brush = CONST.COLORS[playerColor+"_transparent"] - - flight_path_pen = QPen(brush=CONST.COLORS[playerColor]) - flight_path_pen.setColor(CONST.COLORS[playerColor]) - - else: - pen = QPen(brush=CONST.COLORS[enemyColor]) - brush = CONST.COLORS[enemyColor+"_transparent"] - - flight_path_pen = QPen(brush=CONST.COLORS[enemyColor]) - flight_path_pen.setColor(CONST.COLORS[enemyColor]) - - flight_path_pen.setWidth(1) - flight_path_pen.setStyle(Qt.DashDotLine) - - pos = self._transform_point(cp.position) - if self.get_display_rule("flight_paths"): - if cp.id in self.game.planners.keys(): - planner = self.game.planners[cp.id] - for flight in planner.flights: - scene.addEllipse(pos[0], pos[1], 4, 4) - prev_pos = list(pos) - for point in flight.points: - new_pos = self._transform_point(Point(point.x, point.y)) - scene.addLine(prev_pos[0]+2, prev_pos[1]+2, new_pos[0]+2, new_pos[1]+2, flight_path_pen) - scene.addEllipse(new_pos[0], new_pos[1], 4, 4, pen, brush) - prev_pos = list(new_pos) - scene.addLine(prev_pos[0] + 2, prev_pos[1] + 2, pos[0] + 2, pos[1] + 2, flight_path_pen) + if self.get_display_rule("flight_paths"): + self.draw_flight_plans(scene) for cp in self.game.theater.controlpoints: pos = self._transform_point(cp.position) @@ -209,6 +184,42 @@ class QLiberationMap(QGraphicsView): text.setDefaultTextColor(Qt.white) text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1) + def draw_flight_plans(self, scene) -> None: + for cp in self.game.theater.controlpoints: + if cp.id in self.game.planners: + planner = self.game.planners[cp.id] + for flight in planner.flights: + self.draw_flight_plan(scene, flight) + + def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None: + is_player = flight.from_cp.captured + pos = self._transform_point(flight.from_cp.position) + + self.draw_waypoint(scene, pos, is_player) + prev_pos = tuple(pos) + for point in flight.points: + new_pos = self._transform_point(Point(point.x, point.y)) + self.draw_flight_path(scene, prev_pos, new_pos, is_player) + self.draw_waypoint(scene, new_pos, is_player) + prev_pos = tuple(new_pos) + self.draw_flight_path(scene, prev_pos, pos, is_player) + + def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], + player: bool) -> None: + waypoint_pen = self.waypoint_pen(player) + waypoint_brush = self.waypoint_brush(player) + scene.addEllipse(position[0], position[1], self.WAYPOINT_SIZE, + self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush) + + def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], + pos1: Tuple[int, int], player: bool): + flight_path_pen = self.flight_path_pen(player) + # Draw the line to the *middle* of the waypoint. + offset = self.WAYPOINT_SIZE // 2 + scene.addLine(pos0[0] + offset, pos0[1] + offset, + pos1[0] + offset, pos1[1] + offset, + flight_path_pen) + def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor): scene = self.scene() pos = self._transform_point(cp.position) @@ -308,6 +319,29 @@ class QLiberationMap(QGraphicsView): return X > treshold and X or treshold, Y > treshold and Y or treshold + def base_faction_color_name(self, player: bool) -> str: + if player: + return self.game.get_player_color() + else: + return self.game.get_enemy_color() + + def waypoint_pen(self, player: bool) -> QPen: + name = self.base_faction_color_name(player) + return QPen(brush=CONST.COLORS[name]) + + def waypoint_brush(self, player: bool) -> QColor: + name = self.base_faction_color_name(player) + return CONST.COLORS[f"{name}_transparent"] + + def flight_path_pen(self, player: bool) -> QPen: + name = self.base_faction_color_name(player) + color = CONST.COLORS[name] + pen = QPen(brush=color) + pen.setColor(color) + pen.setWidth(1) + pen.setStyle(Qt.DashDotLine) + return pen + def addBackground(self): scene = self.scene() From ff083942e8ef94b65abcbbb6752bbe2c0b5588dd Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 13 Sep 2020 14:32:47 -0700 Subject: [PATCH 03/48] Replace mission planning UI. Mission planning has been completely redone. Missions are now planned by right clicking the target area and choosing "New package". A package can include multiple flights for the same objective. Right now the automatic flight planner is only fragging single-flight packages in the same manner that it used to, but that can be improved now. The air tasking order (ATO) is now the left bar of the main UI. This shows every fragged package, and the flights in the selected package. The info bar that was previously on the left is now a smaller bar at the bottom of the screen. The old "Mission Planning" button is now just the "Take Off" button. The flight plan display no longer shows enemy flight plans. That could be re-added if needed, probably with a difficulty/cheat option. Aircraft inventories have been disassociated from the Planner class. Aircraft inventories are now stored globally in the Game object. Save games made prior to this update will not be compatible do to the changes in how aircraft inventories and planned flights are stored. --- game/db.py | 2 +- game/game.py | 15 + game/inventory.py | 129 +++++++ gen/ato.py | 117 ++++++ gen/flights/ai_flight_planner.py | 350 +++++++----------- qt_ui/dialogs.py | 60 +++ qt_ui/models.py | 268 ++++++++++++++ qt_ui/widgets/QFlightSizeSpinner.py | 13 + qt_ui/widgets/QLabeledWidget.py | 17 + qt_ui/widgets/QTopPanel.py | 51 ++- qt_ui/widgets/ato.py | 249 +++++++++++++ qt_ui/widgets/combos/QAircraftTypeSelector.py | 16 + qt_ui/widgets/combos/QFlightTypeComboBox.py | 22 ++ .../widgets/combos/QOriginAirfieldSelector.py | 41 ++ qt_ui/widgets/map/QLiberationMap.py | 20 +- qt_ui/widgets/map/QMapControlPoint.py | 46 +-- qt_ui/widgets/map/QMapGroundObject.py | 34 +- qt_ui/widgets/map/QMapObject.py | 49 ++- qt_ui/windows/QLiberationWindow.py | 46 ++- qt_ui/windows/basemenu/QBaseMenu2.py | 17 +- qt_ui/windows/basemenu/QBaseMenuTabs.py | 18 +- qt_ui/windows/basemenu/QRecruitBehaviour.py | 40 +- .../airfield/QAircraftRecruitmentMenu.py | 35 +- .../basemenu/airfield/QAirfieldCommand.py | 19 +- .../ground_forces/QArmorRecruitmentMenu.py | 21 +- .../basemenu/ground_forces/QGroundForcesHQ.py | 17 +- qt_ui/windows/mission/QEditFlightDialog.py | 29 ++ qt_ui/windows/mission/QMissionPlanning.py | 159 -------- qt_ui/windows/mission/QPackageDialog.py | 198 ++++++++++ qt_ui/windows/mission/QPlannedFlightsView.py | 45 ++- .../windows/mission/flight/QFlightCreator.py | 239 +++++++----- .../windows/mission/flight/QFlightPlanner.py | 53 ++- .../flight/settings/QFlightSlotEditor.py | 8 +- .../settings/QGeneralFlightSettingsTab.py | 13 +- theater/conflicttheater.py | 2 +- theater/controlpoint.py | 13 +- theater/missiontarget.py | 18 + theater/theatergroundobject.py | 13 +- 38 files changed, 1807 insertions(+), 695 deletions(-) create mode 100644 game/inventory.py create mode 100644 gen/ato.py create mode 100644 qt_ui/dialogs.py create mode 100644 qt_ui/models.py create mode 100644 qt_ui/widgets/QFlightSizeSpinner.py create mode 100644 qt_ui/widgets/QLabeledWidget.py create mode 100644 qt_ui/widgets/ato.py create mode 100644 qt_ui/widgets/combos/QAircraftTypeSelector.py create mode 100644 qt_ui/widgets/combos/QFlightTypeComboBox.py create mode 100644 qt_ui/widgets/combos/QOriginAirfieldSelector.py create mode 100644 qt_ui/windows/mission/QEditFlightDialog.py delete mode 100644 qt_ui/windows/mission/QMissionPlanning.py create mode 100644 qt_ui/windows/mission/QPackageDialog.py create mode 100644 theater/missiontarget.py diff --git a/game/db.py b/game/db.py index 15024422..d1a1cd31 100644 --- a/game/db.py +++ b/game/db.py @@ -1132,7 +1132,7 @@ ShipDict = typing.Dict[ShipType, int] AirDefenseDict = typing.Dict[AirDefence, int] AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]] -TaskForceDict = typing.Dict[typing.Type[Task], AssignedUnitsDict] +TaskForceDict = typing.Dict[typing.Type[MainTask], AssignedUnitsDict] StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]] diff --git a/game/game.py b/game/game.py index 385261b8..affcea24 100644 --- a/game/game.py +++ b/game/game.py @@ -1,7 +1,9 @@ from datetime import datetime, timedelta from game.db import REWARDS, PLAYER_BUDGET_BASE, sys +from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats +from gen.ato import AirTaskingOrder from gen.flights.ai_flight_planner import FlightPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner from .event import * @@ -78,6 +80,13 @@ class Game: self.jtacs = [] self.savepath = "" + self.blue_ato = AirTaskingOrder() + self.red_ato = AirTaskingOrder() + + self.aircraft_inventory = GlobalAircraftInventory( + self.theater.controlpoints + ) + self.sanitize_sides() @@ -229,10 +238,16 @@ class Game: # Update statistics self.game_stats.update(self) + self.aircraft_inventory.reset() + for cp in self.theater.controlpoints: + self.aircraft_inventory.set_from_control_point(cp) + # Plan flights & combat for next turn self.__culling_points = self.compute_conflicts_position() self.planners = {} self.ground_planners = {} + self.blue_ato.clear() + self.red_ato.clear() for cp in self.theater.controlpoints: if cp.has_runway(): planner = FlightPlanner(cp, self) diff --git a/game/inventory.py b/game/inventory.py new file mode 100644 index 00000000..1d05a473 --- /dev/null +++ b/game/inventory.py @@ -0,0 +1,129 @@ +"""Inventory management APIs.""" +from collections import defaultdict +from typing import Dict, Iterable, Iterator, Set, Tuple + +from dcs.unittype import UnitType + +from gen.flights.flight import Flight + + +class ControlPointAircraftInventory: + """Aircraft inventory for a single control point.""" + + def __init__(self, control_point: "ControlPoint") -> None: + self.control_point = control_point + self.inventory: Dict[UnitType, int] = defaultdict(int) + + def add_aircraft(self, aircraft: UnitType, count: int) -> None: + """Adds aircraft to the inventory. + + Args: + aircraft: The type of aircraft to add. + count: The number of aircraft to add. + """ + self.inventory[aircraft] += count + + def remove_aircraft(self, aircraft: UnitType, count: int) -> None: + """Removes aircraft from the inventory. + + Args: + aircraft: The type of aircraft to remove. + count: The number of aircraft to remove. + + Raises: + ValueError: The control point cannot fulfill the requested number of + aircraft. + """ + available = self.inventory[aircraft] + if available < count: + raise ValueError( + f"Cannot remove {count} {aircraft.id} from " + f"{self.control_point.name}. Only have {available}." + ) + self.inventory[aircraft] -= count + + def available(self, aircraft: UnitType) -> int: + """Returns the number of available aircraft of the given type. + + Args: + aircraft: The type of aircraft to query. + """ + return self.inventory[aircraft] + + @property + def types_available(self) -> Iterator[UnitType]: + """Iterates over all available aircraft types.""" + for aircraft, count in self.inventory.items(): + if count > 0: + yield aircraft + + @property + def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]: + """Iterates over all available aircraft types, including amounts.""" + for aircraft, count in self.inventory.items(): + if count > 0: + yield aircraft, count + + @property + def total_available(self) -> int: + """Returns the total number of aircraft available.""" + # TODO: Remove? + # This probably isn't actually useful. It's used by the AI flight + # planner to determine how many flights of a given type it should + # allocate, but it should probably be making that decision based on the + # number of aircraft available to perform a particular role. + return sum(self.inventory.values()) + + def clear(self) -> None: + """Clears all aircraft from the inventory.""" + self.inventory.clear() + + +class GlobalAircraftInventory: + """Game-wide aircraft inventory.""" + def __init__(self, control_points: Iterable["ControlPoint"]) -> None: + self.inventories: Dict["ControlPoint", ControlPointAircraftInventory] = { + cp: ControlPointAircraftInventory(cp) for cp in control_points + } + + def reset(self) -> None: + """Clears all control points and their inventories.""" + for inventory in self.inventories.values(): + inventory.clear() + + def set_from_control_point(self, control_point: "ControlPoint") -> None: + """Set the control point's aircraft inventory. + + If the inventory for the given control point has already been set for + the turn, it will be overwritten. + """ + inventory = self.inventories[control_point] + for aircraft, count in control_point.base.aircraft.items(): + inventory.add_aircraft(aircraft, count) + + def for_control_point( + self, + control_point: "ControlPoint") -> ControlPointAircraftInventory: + """Returns the inventory specific to the given control point.""" + return self.inventories[control_point] + + @property + def available_types_for_player(self) -> Iterator[UnitType]: + """Iterates over all aircraft types available to the player.""" + seen: Set[UnitType] = set() + for control_point, inventory in self.inventories.items(): + if control_point.captured: + for aircraft in inventory.types_available: + if aircraft not in seen: + seen.add(aircraft) + yield aircraft + + def claim_for_flight(self, flight: Flight) -> None: + """Removes aircraft from the inventory for the given flight.""" + inventory = self.for_control_point(flight.from_cp) + inventory.remove_aircraft(flight.unit_type, flight.count) + + def return_from_flight(self, flight: Flight) -> None: + """Returns a flight's aircraft to the inventory.""" + inventory = self.for_control_point(flight.from_cp) + inventory.add_aircraft(flight.unit_type, flight.count) diff --git a/gen/ato.py b/gen/ato.py new file mode 100644 index 00000000..e82930fe --- /dev/null +++ b/gen/ato.py @@ -0,0 +1,117 @@ +"""Air Tasking Orders. + +The classes of the Air Tasking Order (ATO) define all of the missions that have +been planned, and which aircraft have been assigned to them. Each planned +mission, or "package" is composed of individual flights. The package may contain +dissimilar aircraft performing different roles, but all for the same goal. For +example, the package to strike an enemy airfield may contain an escort flight, +a SEAD flight, and the strike aircraft themselves. CAP packages may contain only +the single CAP flight. +""" +from collections import defaultdict +from dataclasses import dataclass, field +import logging +from typing import Dict, List + +from .flights.flight import Flight, FlightType +from theater.missiontarget import MissionTarget + + +@dataclass(frozen=True) +class Task: + """The main task of a flight or package.""" + + #: The type of task. + task_type: FlightType + + #: The location of the objective. + location: str + + +@dataclass +class Package: + """A mission package.""" + + #: The mission target. Currently can be either a ControlPoint or a + #: TheaterGroundObject (non-ControlPoint map objectives). + target: MissionTarget + + #: The set of flights in the package. + flights: List[Flight] = field(default_factory=list) + + def add_flight(self, flight: Flight) -> None: + """Adds a flight to the package.""" + self.flights.append(flight) + + def remove_flight(self, flight: Flight) -> None: + """Removes a flight from the package.""" + self.flights.remove(flight) + + @property + def package_description(self) -> str: + """Generates a package description based on flight composition.""" + if not self.flights: + return "No mission" + + flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0) + for flight in self.flights: + flight_counts[flight.flight_type] += 1 + + # The package will contain a mix of mission types, but in general we can + # determine the goal of the mission because some mission types are more + # likely to be the main task than others. For example, a package with + # only CAP flights is a CAP package, a flight with CAP and strike is a + # strike package, a flight with CAP and DEAD is a DEAD package, and a + # flight with strike and SEAD is an OCA/Strike package. The type of + # package is determined by the highest priority flight in the package. + task_priorities = [ + FlightType.CAS, + FlightType.STRIKE, + FlightType.ANTISHIP, + FlightType.BAI, + FlightType.EVAC, + FlightType.TROOP_TRANSPORT, + FlightType.RECON, + FlightType.ELINT, + FlightType.DEAD, + FlightType.SEAD, + FlightType.LOGISTICS, + FlightType.INTERCEPTION, + FlightType.TARCAP, + FlightType.CAP, + FlightType.BARCAP, + FlightType.EWAR, + ] + for task in task_priorities: + if flight_counts[task]: + return task.name + + # If we get here, our task_priorities list above is incomplete. Log the + # issue and return the type of *any* flight in the package. + some_mission = next(iter(self.flights)).flight_type + logging.warning(f"Unhandled mission type: {some_mission}") + return some_mission.name + + def __hash__(self) -> int: + # TODO: Far from perfect. Number packages? + return hash(self.target.name) + + +@dataclass +class AirTaskingOrder: + """The entire ATO for one coalition.""" + + #: The set of all planned packages in the ATO. + packages: List[Package] = field(default_factory=list) + + def add_package(self, package: Package) -> None: + """Adds a package to the ATO.""" + self.packages.append(package) + + def remove_package(self, package: Package) -> None: + """Removes a package from the ATO.""" + self.packages.remove(package) + + def clear(self) -> None: + """Removes all packages from the ATO.""" + self.packages.clear() diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 99cf8427..f6a665e4 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -1,28 +1,49 @@ import math import operator import random +from typing import Iterable, Iterator, List, Tuple + +from dcs.unittype import FlyingType from game import db from game.data.doctrine import MODERN_DOCTRINE from game.data.radar_db import UNITS_WITH_RADAR -from game.utils import meter_to_feet, nm_to_meter +from game.utils import nm_to_meter from gen import Conflict -from gen.flights.ai_flight_planner_db import INTERCEPT_CAPABLE, CAP_CAPABLE, CAS_CAPABLE, SEAD_CAPABLE, STRIKE_CAPABLE, \ - DRONES -from gen.flights.flight import Flight, FlightType, FlightWaypoint, FlightWaypointType - +from gen.ato import Package +from gen.flights.ai_flight_planner_db import ( + CAP_CAPABLE, + CAS_CAPABLE, + DRONES, + SEAD_CAPABLE, + STRIKE_CAPABLE, +) +from gen.flights.flight import ( + Flight, + FlightType, + FlightWaypoint, + FlightWaypointType, +) +from theater.controlpoint import ControlPoint +from theater.missiontarget import MissionTarget +from theater.theatergroundobject import TheaterGroundObject MISSION_DURATION = 80 +# TODO: Should not be per-control point. +# Packages can frag flights from individual airfields, so we should be planning +# coalition wide rather than per airfield. class FlightPlanner: - def __init__(self, from_cp, game): + def __init__(self, from_cp: ControlPoint, game: "Game") -> None: # TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine # TODO : the flight planner should plan package and operations self.from_cp = from_cp self.game = game - self.aircraft_inventory = {} # local copy of the airbase inventory + self.flights: List[Flight] = [] + self.potential_sead_targets: List[Tuple[TheaterGroundObject, int]] = [] + self.potential_strike_targets: List[Tuple[TheaterGroundObject, int]] = [] if from_cp.captured: self.faction = self.game.player_faction @@ -34,240 +55,146 @@ class FlightPlanner: else: self.doctrine = MODERN_DOCTRINE + @property + def aircraft_inventory(self) -> "GlobalAircraftInventory": + return self.game.aircraft_inventory - def reset(self): - """ - Reset the planned flights and available units - """ - self.aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()}) - self.interceptor_flights = [] - self.cap_flights = [] - self.cas_flights = [] - self.strike_flights = [] - self.sead_flights = [] - self.custom_flights = [] + def reset(self) -> None: + """Reset the planned flights and available units.""" self.flights = [] self.potential_sead_targets = [] self.potential_strike_targets = [] - def plan_flights(self): - + def plan_flights(self) -> None: self.reset() self.compute_sead_targets() self.compute_strike_targets() - # The priority is to assign air-superiority fighter or interceptor to interception roles, so they can scramble if there is an attacker - # self.commision_interceptors() + self.commission_cap() + self.commission_cas() + self.commission_sead() + self.commission_strike() + # TODO: Commission anti-ship and intercept. - # Then some CAP patrol for the next 2 hours - self.commision_cap() + def plan_legacy_mission(self, flight: Flight, + location: MissionTarget) -> None: + package = Package(location) + package.add_flight(flight) + if flight.from_cp.captured: + self.game.blue_ato.add_package(package) + else: + self.game.red_ato.add_package(package) + self.flights.append(flight) + self.aircraft_inventory.claim_for_flight(flight) - # Then setup cas - self.commision_cas() + def get_compatible_aircraft(self, candidates: Iterable[FlyingType], + minimum: int) -> List[FlyingType]: + inventory = self.aircraft_inventory.for_control_point(self.from_cp) + return [k for k, v in inventory.all_aircraft if + k in candidates and v >= minimum] - # Then prepare some sead flights if required - self.commision_sead() - - self.commision_strike() - - # TODO : commision ANTISHIP - - def remove_flight(self, index): - try: - flight = self.flights[index] - if flight in self.interceptor_flights: self.interceptor_flights.remove(flight) - if flight in self.cap_flights: self.cap_flights.remove(flight) - if flight in self.cas_flights: self.cas_flights.remove(flight) - if flight in self.strike_flights: self.strike_flights.remove(flight) - if flight in self.sead_flights: self.sead_flights.remove(flight) - if flight in self.custom_flights: self.custom_flights.remove(flight) - self.flights.remove(flight) - except IndexError: + def alloc_aircraft( + self, num_flights: int, flight_size: int, + allowed_types: Iterable[FlyingType]) -> Iterator[FlyingType]: + aircraft = self.get_compatible_aircraft(allowed_types, flight_size) + if not aircraft: return + for _ in range(num_flights): + yield random.choice(aircraft) + aircraft = self.get_compatible_aircraft(allowed_types, flight_size) + if not aircraft: + return - def commision_interceptors(self): - """ - Pick some aircraft to assign them to interception roles - """ + def commission_cap(self) -> None: + """Pick some aircraft to assign them to defensive CAP roles (BARCAP).""" + offset = random.randint(0, 5) + num_caps = MISSION_DURATION // self.doctrine["CAP_EVERY_X_MINUTES"] + for i, aircraft in enumerate(self.alloc_aircraft(num_caps, 2, CAP_CAPABLE)): + flight = Flight(aircraft, 2, self.from_cp, FlightType.CAP) - # At least try to generate one interceptor group - number_of_interceptor_groups = min(max(sum([v for k, v in self.aircraft_inventory.items()]) / 4, self.doctrine["MAX_NUMBER_OF_INTERCEPTION_GROUP"]), 1) - possible_interceptors = [k for k in self.aircraft_inventory.keys() if k in INTERCEPT_CAPABLE] - - if len(possible_interceptors) <= 0: - possible_interceptors = [k for k,v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] - - if number_of_interceptor_groups > 0: - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_interceptors}) - for i in range(number_of_interceptor_groups): - try: - unit = random.choice([k for k,v in inventory.items() if v >= 2]) - except IndexError: - break - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.INTERCEPTION) - flight.scheduled_in = 1 - flight.points = [] - - self.interceptor_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - def commision_cap(self): - """ - Pick some aircraft to assign them to defensive CAP roles (BARCAP) - """ - - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAP_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["CAP_EVERY_X_MINUTES"])): - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.CAP) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["CAP_EVERY_X_MINUTES"] - 5, self.doctrine["CAP_EVERY_X_MINUTES"] + 5) + flight.scheduled_in = offset + i * random.randint( + self.doctrine["CAP_EVERY_X_MINUTES"] - 5, + self.doctrine["CAP_EVERY_X_MINUTES"] + 5 + ) 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) else: + location = flight.from_cp self.generate_barcap(flight, flight.from_cp) - self.cap_flights.append(flight) - self.flights.append(flight) + self.plan_legacy_mission(flight, location) - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v + def commission_cas(self) -> None: + """Pick some aircraft to assign them to CAS.""" + cas_locations = self._get_cas_locations() + if not cas_locations: + return - def commision_cas(self): - """ - Pick some aircraft to assign them to CAS - """ + offset = random.randint(0,5) + num_cas = MISSION_DURATION // self.doctrine["CAS_EVERY_X_MINUTES"] + for i, aircraft in enumerate(self.alloc_aircraft(num_cas, 2, CAS_CAPABLE)): + flight = Flight(aircraft, 2, self.from_cp, FlightType.CAS) + flight.scheduled_in = offset + i * random.randint( + self.doctrine["CAS_EVERY_X_MINUTES"] - 5, + self.doctrine["CAS_EVERY_X_MINUTES"] + 5) + location = random.choice(cas_locations) - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in CAS_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - cas_location = self._get_cas_locations() + self.generate_cas(flight, flight.from_cp, location) + self.plan_legacy_mission(flight, location) - if len(cas_location) > 0: + def commission_sead(self) -> None: + """Pick some aircraft to assign them to SEAD tasks.""" - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["CAS_EVERY_X_MINUTES"])): + if not self.potential_sead_targets: + return - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break + offset = random.randint(0, 5) + num_sead = max( + MISSION_DURATION // self.doctrine["SEAD_EVERY_X_MINUTES"], + len(self.potential_sead_targets)) + for i, aircraft in enumerate(self.alloc_aircraft(num_sead, 2, SEAD_CAPABLE)): + flight = Flight(aircraft, 2, self.from_cp, + random.choice([FlightType.SEAD, FlightType.DEAD])) + flight.scheduled_in = offset + i * random.randint( + self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, + self.doctrine["SEAD_EVERY_X_MINUTES"] + 5) - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, FlightType.CAS) - flight.points = [] - flight.scheduled_in = offset + i * random.randint(self.doctrine["CAS_EVERY_X_MINUTES"] - 5, self.doctrine["CAS_EVERY_X_MINUTES"] + 5) - location = random.choice(cas_location) + location = self.potential_sead_targets[0][0] + self.potential_sead_targets.pop() - self.generate_cas(flight, flight.from_cp, location) + self.generate_sead(flight, location, []) + self.plan_legacy_mission(flight, location) - self.cas_flights.append(flight) - self.flights.append(flight) + def commission_strike(self) -> None: + """Pick some aircraft to assign them to STRIKE tasks.""" + if not self.potential_strike_targets: + return - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v + offset = random.randint(0,5) + num_strike = max( + MISSION_DURATION / self.doctrine["STRIKE_EVERY_X_MINUTES"], + len(self.potential_strike_targets) + ) + for i, aircraft in enumerate(self.alloc_aircraft(num_strike, 2, STRIKE_CAPABLE)): + if aircraft in DRONES: + count = 1 + else: + count = 2 - def commision_sead(self): - """ - Pick some aircraft to assign them to SEAD tasks - """ + flight = Flight(aircraft, count, self.from_cp, FlightType.STRIKE) + flight.scheduled_in = offset + i * random.randint( + self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, + self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5) - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in SEAD_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) + location = self.potential_strike_targets[0][0] + self.potential_strike_targets.pop(0) - if len(self.potential_sead_targets) > 0: - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["SEAD_EVERY_X_MINUTES"])): - - if len(self.potential_sead_targets) <= 0: - break - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - inventory[unit] = inventory[unit] - 2 - flight = Flight(unit, 2, self.from_cp, random.choice([FlightType.SEAD, FlightType.DEAD])) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, self.doctrine["SEAD_EVERY_X_MINUTES"] + 5) - - location = self.potential_sead_targets[0][0] - self.potential_sead_targets.pop(0) - - self.generate_sead(flight, location, []) - - self.sead_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v - - - def commision_strike(self): - """ - Pick some aircraft to assign them to STRIKE tasks - """ - possible_aircraft = [k for k, v in self.aircraft_inventory.items() if k in STRIKE_CAPABLE and v >= 2] - inventory = dict({k: v for k, v in self.aircraft_inventory.items() if k in possible_aircraft}) - - if len(self.potential_strike_targets) > 0: - - offset = random.randint(0,5) - for i in range(int(MISSION_DURATION/self.doctrine["STRIKE_EVERY_X_MINUTES"])): - - if len(self.potential_strike_targets) <= 0: - break - - try: - unit = random.choice([k for k, v in inventory.items() if v >= 2]) - except IndexError: - break - - if unit in DRONES: - count = 1 - else: - count = 2 - - inventory[unit] = inventory[unit] - count - flight = Flight(unit, count, self.from_cp, FlightType.STRIKE) - - flight.points = [] - flight.scheduled_in = offset + i*random.randint(self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5) - - location = self.potential_strike_targets[0][0] - self.potential_strike_targets.pop(0) - - self.generate_strike(flight, location) - - self.strike_flights.append(flight) - self.flights.append(flight) - - # Update inventory - for k, v in inventory.items(): - self.aircraft_inventory[k] = v + self.generate_strike(flight, location) + self.plan_legacy_mission(flight, location) def _get_cas_locations(self): return self._get_cas_locations_for_cp(self.from_cp) @@ -351,18 +278,7 @@ class FlightPlanner: return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\ + "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40 - def get_available_aircraft(self): - base_aircraft_inventory = dict({k: v for k, v in self.from_cp.base.aircraft.items()}) - for f in self.flights: - if f.unit_type in base_aircraft_inventory.keys(): - base_aircraft_inventory[f.unit_type] = base_aircraft_inventory[f.unit_type] - f.count - if base_aircraft_inventory[f.unit_type] <= 0: - del base_aircraft_inventory[f.unit_type] - return base_aircraft_inventory - - - def generate_strike(self, flight, location): - + def generate_strike(self, flight: Flight, location: TheaterGroundObject): flight.flight_type = FlightType.STRIKE ascend = self.generate_ascend_point(flight.from_cp) flight.points.append(ascend) diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py new file mode 100644 index 00000000..16920a15 --- /dev/null +++ b/qt_ui/dialogs.py @@ -0,0 +1,60 @@ +"""Application-wide dialog management.""" +from typing import Optional + +from gen.flights.flight import Flight +from theater.missiontarget import MissionTarget +from .models import GameModel, PackageModel +from .windows.mission.QEditFlightDialog import QEditFlightDialog +from .windows.mission.QPackageDialog import ( + QEditPackageDialog, + QNewPackageDialog, +) + + +class Dialog: + """Dialog management singleton. + + Opens dialogs and keeps references to dialog windows so that their creators + do not need to worry about the lifetime of the dialog object, and can open + dialogs without needing to have their own reference to common data like the + game model. + """ + + #: The game model. Is only None before initialization, as the game model + #: itself is responsible for handling the case where no game is loaded. + game_model: Optional[GameModel] = None + + new_package_dialog: Optional[QNewPackageDialog] = None + edit_package_dialog: Optional[QEditPackageDialog] = None + edit_flight_dialog: Optional[QEditFlightDialog] = None + + @classmethod + def set_game(cls, game_model: GameModel) -> None: + """Sets the game model.""" + cls.game_model = game_model + + @classmethod + def open_new_package_dialog(cls, mission_target: MissionTarget): + """Opens the dialog to create a new package with the given target.""" + cls.new_package_dialog = QNewPackageDialog( + cls.game_model.game, + cls.game_model.ato_model, + mission_target + ) + cls.new_package_dialog.show() + + @classmethod + def open_edit_package_dialog(cls, package_model: PackageModel): + """Opens the dialog to edit the given package.""" + cls.edit_package_dialog = QEditPackageDialog( + cls.game_model.game, + cls.game_model.ato_model, + package_model + ) + cls.edit_package_dialog.show() + + @classmethod + def open_edit_flight_dialog(cls, flight: Flight): + """Opens the dialog to edit the given flight.""" + cls.edit_flight_dialog = QEditFlightDialog(cls.game_model.game, flight) + cls.edit_flight_dialog.show() diff --git a/qt_ui/models.py b/qt_ui/models.py new file mode 100644 index 00000000..87d52538 --- /dev/null +++ b/qt_ui/models.py @@ -0,0 +1,268 @@ +"""Qt data models for game objects.""" +from typing import Any, Callable, Dict, Iterator, TypeVar, Optional + +from PySide2.QtCore import ( + QAbstractListModel, + QModelIndex, + Qt, + Signal, +) +from PySide2.QtGui import QIcon + +from game import db +from game.game import Game +from gen.ato import AirTaskingOrder, Package +from gen.flights.flight import Flight +from qt_ui.uiconstants import AIRCRAFT_ICONS +from theater.missiontarget import MissionTarget + + +class DeletableChildModelManager: + """Manages lifetimes for child models. + + Qt's data models don't have a good way of modeling related data aside from + lists, tables, or trees of similar objects. We could build one monolithic + GameModel that tracks all of the data in the game and use the parent/child + relationships of that model to index down into the ATO, packages, flights, + etc, but doing so is error prone because it requires us to manually manage + that relationship tree and keep our own mappings from row/column into + specific members. + + However, creating child models outside of the tree means that removing an + item from the parent will not signal the child's deletion to any views, so + we must track this explicitly. + + Any model which has child data types should use this class to track the + deletion of child models. All child model types must define a signal named + `deleted`. This signal will be emitted when the child model is being + deleted. Any views displaying such data should subscribe to those events and + update their display accordingly. + """ + + #: The type of data owned by models created by this class. + DataType = TypeVar("DataType") + + #: The type of model managed by this class. + ModelType = TypeVar("ModelType") + + ModelDict = Dict[DataType, ModelType] + + def __init__(self, create_model: Callable[[DataType], ModelType]) -> None: + self.create_model = create_model + self.models: DeletableChildModelManager.ModelDict = {} + + def acquire(self, data: DataType) -> ModelType: + """Returns a model for the given child data. + + If a model has already been created for the given data, it will be + returned. The data type must be hashable. + """ + if data in self.models: + return self.models[data] + model = self.create_model(data) + self.models[data] = model + return model + + def release(self, data: DataType) -> None: + """Releases the model matching the given data, if one exists. + + If the given data has had a model created for it, that model will be + deleted and its `deleted` signal will be emitted. + """ + if data in self.models: + model = self.models[data] + del self.models[data] + model.deleted.emit() + + def clear(self) -> None: + """Deletes all managed models.""" + for data in list(self.models.keys()): + self.release(data) + + +class NullListModel(QAbstractListModel): + """Generic empty list model.""" + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return 0 + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + return None + + +class PackageModel(QAbstractListModel): + """The model for an ATO package.""" + + #: Emitted when this package is being deleted from the ATO. + deleted = Signal() + + def __init__(self, package: Package) -> None: + super().__init__() + self.package = package + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.package.flights) + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + if not index.isValid(): + return None + flight = self.flight_at_index(index) + if role == Qt.DisplayRole: + return self.text_for_flight(flight) + if role == Qt.DecorationRole: + return self.icon_for_flight(flight) + return None + + @staticmethod + def text_for_flight(flight: Flight) -> str: + """Returns the text that should be displayed for the flight.""" + task = flight.flight_type.name + count = flight.count + name = db.unit_type_name(flight.unit_type) + delay = flight.scheduled_in + origin = flight.from_cp.name + return f"[{task}] {count} x {name} from {origin} in {delay} minutes" + + @staticmethod + def icon_for_flight(flight: Flight) -> Optional[QIcon]: + """Returns the icon that should be displayed for the flight.""" + name = db.unit_type_name(flight.unit_type) + if name in AIRCRAFT_ICONS: + return QIcon(AIRCRAFT_ICONS[name]) + return None + + def add_flight(self, flight: Flight) -> None: + """Adds the given flight to the package.""" + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.package.add_flight(flight) + self.endInsertRows() + + def delete_flight_at_index(self, index: QModelIndex) -> None: + """Removes the flight at the given index from the package.""" + self.delete_flight(self.flight_at_index(index)) + + def delete_flight(self, flight: Flight) -> None: + """Removes the given flight from the package. + + If the flight is using claimed inventory, the caller is responsible for + returning that inventory. + """ + index = self.package.flights.index(flight) + self.beginRemoveRows(QModelIndex(), index, index) + self.package.remove_flight(flight) + self.endRemoveRows() + + def flight_at_index(self, index: QModelIndex) -> Flight: + """Returns the flight located at the given index.""" + return self.package.flights[index.row()] + + @property + def mission_target(self) -> MissionTarget: + """Returns the mission target of the package.""" + package = self.package + target = package.target + return target + + @property + def description(self) -> str: + """Returns the description of the package.""" + return self.package.package_description + + @property + def flights(self) -> Iterator[Flight]: + """Iterates over the flights in the package.""" + for flight in self.package.flights: + yield flight + + +class AtoModel(QAbstractListModel): + """The model for an AirTaskingOrder.""" + + def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None: + super().__init__() + self.game = game + self.ato = ato + self.package_models = DeletableChildModelManager(PackageModel) + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + return len(self.ato.packages) + + def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: + if not index.isValid(): + return None + if role == Qt.DisplayRole: + package = self.ato.packages[index.row()] + return f"{package.package_description} {package.target.name}" + return None + + def add_package(self, package: Package) -> None: + """Adds a package to the ATO.""" + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.ato.add_package(package) + self.endInsertRows() + + def delete_package_at_index(self, index: QModelIndex) -> None: + """Removes the package at the given index from the ATO.""" + self.delete_package(self.package_at_index(index)) + + def delete_package(self, package: Package) -> None: + """Removes the given package from the ATO.""" + self.package_models.release(package) + index = self.ato.packages.index(package) + self.beginRemoveRows(QModelIndex(), index, index) + self.ato.remove_package(package) + for flight in package.flights: + self.game.aircraft_inventory.return_from_flight(flight) + self.endRemoveRows() + + def package_at_index(self, index: QModelIndex) -> Package: + """Returns the package at the given index.""" + return self.ato.packages[index.row()] + + def replace_from_game(self, game: Optional[Game]) -> None: + """Updates the ATO object to match the updated game object. + + If the game is None (as is the case when no game has been loaded), an + empty ATO will be used. + """ + self.beginResetModel() + self.game = game + self.package_models.clear() + if self.game is not None: + self.ato = game.blue_ato + else: + self.ato = AirTaskingOrder() + self.endResetModel() + + def get_package_model(self, index: QModelIndex) -> PackageModel: + """Returns a model for the package at the given index.""" + return self.package_models.acquire(self.package_at_index(index)) + + @property + def packages(self) -> Iterator[PackageModel]: + """Iterates over all the packages in the ATO.""" + for package in self.ato.packages: + yield self.package_models.acquire(package) + + +class GameModel: + """A model for the Game object. + + This isn't a real Qt data model, but simplifies management of the game and + its ATO objects. + """ + def __init__(self) -> None: + self.game: Optional[Game] = None + # TODO: Add red ATO model, add cheat option to show red flight plan. + self.ato_model = AtoModel(self.game, AirTaskingOrder()) + + def set(self, game: Optional[Game]) -> None: + """Updates the managed Game object. + + The argument will be None when no game has been loaded. In this state, + much of the UI is still visible and needs to handle that behavior. To + simplify that case, the AtoModel will model an empty ATO when no game is + loaded. + """ + self.game = game + self.ato_model.replace_from_game(self.game) diff --git a/qt_ui/widgets/QFlightSizeSpinner.py b/qt_ui/widgets/QFlightSizeSpinner.py new file mode 100644 index 00000000..a2619507 --- /dev/null +++ b/qt_ui/widgets/QFlightSizeSpinner.py @@ -0,0 +1,13 @@ +"""Spin box for selecting the number of aircraft in a flight.""" +from PySide2.QtWidgets import QSpinBox + + +class QFlightSizeSpinner(QSpinBox): + """Spin box for selecting the number of aircraft in a flight.""" + + def __init__(self, min_size: int = 1, max_size: int = 4, + default_size: int = 2) -> None: + super().__init__() + self.setMinimum(min_size) + self.setMaximum(max_size) + self.setValue(default_size) diff --git a/qt_ui/widgets/QLabeledWidget.py b/qt_ui/widgets/QLabeledWidget.py new file mode 100644 index 00000000..88459896 --- /dev/null +++ b/qt_ui/widgets/QLabeledWidget.py @@ -0,0 +1,17 @@ +"""A layout containing a widget with an associated label.""" +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget + + +class QLabeledWidget(QHBoxLayout): + """A layout containing a widget with an associated label. + + Best used for vertical forms, where the given widget is the input and the + label is used to name the input. + """ + + def __init__(self, text: str, widget: QWidget) -> None: + super().__init__() + self.addWidget(QLabel(text)) + self.addStretch() + self.addWidget(widget, alignment=Qt.AlignRight) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 30725095..f2f73b4f 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -1,16 +1,15 @@ -from PySide2.QtWidgets import QFrame, QHBoxLayout, QPushButton, QVBoxLayout, QGroupBox - -from game import Game -from qt_ui.widgets.QBudgetBox import QBudgetBox -from qt_ui.widgets.QFactionsInfos import QFactionsInfos -from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu -from qt_ui.windows.stats.QStatsWindow import QStatsWindow -from qt_ui.widgets.QTurnCounter import QTurnCounter +from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton import qt_ui.uiconstants as CONST +from game import Game +from game.event import CAP, CAS, FrontlineAttackEvent +from qt_ui.widgets.QBudgetBox import QBudgetBox +from qt_ui.widgets.QFactionsInfos import QFactionsInfos +from qt_ui.widgets.QTurnCounter import QTurnCounter from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from qt_ui.windows.mission.QMissionPlanning import QMissionPlanning from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow +from qt_ui.windows.stats.QStatsWindow import QStatsWindow +from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow class QTopPanel(QFrame): @@ -33,10 +32,10 @@ class QTopPanel(QFrame): self.passTurnButton.setProperty("style", "btn-primary") self.passTurnButton.clicked.connect(self.passTurn) - self.proceedButton = QPushButton("Mission Planning") + self.proceedButton = QPushButton("Take off") self.proceedButton.setIcon(CONST.ICONS["Proceed"]) - self.proceedButton.setProperty("style", "btn-success") - self.proceedButton.clicked.connect(self.proceed) + self.proceedButton.setProperty("style", "start-button") + self.proceedButton.clicked.connect(self.launch_mission) if self.game and self.game.turn == 0: self.proceedButton.setEnabled(False) @@ -100,9 +99,31 @@ class QTopPanel(QFrame): GameUpdateSignal.get_instance().updateGame(self.game) self.proceedButton.setEnabled(True) - def proceed(self): - self.subwindow = QMissionPlanning(self.game) - self.subwindow.show() + def launch_mission(self): + """Finishes planning and waits for mission completion.""" + # TODO: Refactor this nonsense. + game_event = None + for event in self.game.events: + if isinstance(event, + FrontlineAttackEvent) and event.is_player_attacking: + game_event = event + if game_event is None: + game_event = FrontlineAttackEvent( + self.game, + self.game.theater.controlpoints[0], + self.game.theater.controlpoints[0], + self.game.theater.controlpoints[0].position, + self.game.player_name, + self.game.enemy_name) + game_event.is_awacs_enabled = True + game_event.ca_slots = 1 + game_event.departure_cp = self.game.theater.controlpoints[0] + game_event.player_attacking({CAS: {}, CAP: {}}) + game_event.depart_from = self.game.theater.controlpoints[0] + + self.game.initiate_event(game_event) + waiting = QWaitingForMissionResultWindow(game_event, self.game) + waiting.show() def budget_update(self, game:Game): self.budgetBox.setGame(game) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py new file mode 100644 index 00000000..bee61a43 --- /dev/null +++ b/qt_ui/widgets/ato.py @@ -0,0 +1,249 @@ +"""Widgets for displaying air tasking orders.""" +import logging +from typing import Optional + +from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt +from PySide2.QtWidgets import ( + QAbstractItemView, + QGroupBox, + QHBoxLayout, + QListView, + QPushButton, + QSplitter, + QVBoxLayout, +) + +from gen.ato import Package +from gen.flights.flight import Flight +from ..models import AtoModel, GameModel, NullListModel, PackageModel + + +class QFlightList(QListView): + """List view for displaying the flights of a package.""" + + def __init__(self, model: Optional[PackageModel]) -> None: + super().__init__() + self.package_model = model + self.set_package(model) + self.setIconSize(QSize(91, 24)) + self.setSelectionBehavior(QAbstractItemView.SelectItems) + + def set_package(self, model: Optional[PackageModel]) -> None: + """Sets the package model to display.""" + if model is None: + self.disconnect_model() + else: + self.package_model = model + self.setModel(model) + # noinspection PyUnresolvedReferences + model.deleted.connect(self.disconnect_model) + self.selectionModel().setCurrentIndex( + model.index(0, 0, QModelIndex()), + QItemSelectionModel.Select + ) + + def disconnect_model(self) -> None: + """Clears the listview of any model attachments. + + Displays an empty list until set_package is called with a valid model. + """ + model = self.model() + if model is not None and isinstance(model, PackageModel): + model.deleted.disconnect(self.disconnect_model) + self.setModel(NullListModel()) + + @property + def selected_item(self) -> Optional[Flight]: + """Returns the selected flight, if any.""" + index = self.currentIndex() + if not index.isValid(): + return None + return self.package_model.flight_at_index(index) + + +class QFlightPanel(QGroupBox): + """The flight display portion of the ATO panel. + + Displays the flights assigned to the selected package, and includes edit and + delete buttons for flight management. + """ + + def __init__(self, game_model: GameModel, + package_model: Optional[PackageModel] = None) -> None: + super().__init__("Flights") + self.game_model = game_model + self.package_model = package_model + + self.vbox = QVBoxLayout() + self.setLayout(self.vbox) + + self.flight_list = QFlightList(package_model) + self.vbox.addWidget(self.flight_list) + + self.button_row = QHBoxLayout() + self.vbox.addLayout(self.button_row) + + self.edit_button = QPushButton("Edit") + self.edit_button.clicked.connect(self.on_edit) + self.button_row.addWidget(self.edit_button) + + self.delete_button = QPushButton("Delete") + # noinspection PyTypeChecker + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_row.addWidget(self.delete_button) + + self.selection_changed.connect(self.on_selection_changed) + self.on_selection_changed() + + def set_package(self, model: Optional[PackageModel]) -> None: + """Sets the package model to display.""" + self.package_model = model + self.flight_list.set_package(model) + self.on_selection_changed() + + @property + def selection_changed(self): + """Returns the signal emitted when the flight selection changes.""" + return self.flight_list.selectionModel().selectionChanged + + def on_selection_changed(self) -> None: + """Updates the status of the edit and delete buttons.""" + index = self.flight_list.currentIndex() + enabled = index.isValid() + self.edit_button.setEnabled(enabled) + self.delete_button.setEnabled(enabled) + + def on_edit(self) -> None: + """Opens the flight edit dialog.""" + index = self.flight_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot edit flight when no flight is selected.") + return + from qt_ui.dialogs import Dialog + Dialog.open_edit_flight_dialog( + self.package_model.flight_at_index(index) + ) + + def on_delete(self) -> None: + """Removes the selected flight from the package.""" + index = self.flight_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot delete flight when no flight is selected.") + return + self.game_model.game.aircraft_inventory.return_from_flight( + self.flight_list.selected_item) + self.package_model.delete_flight_at_index(index) + + +class QPackageList(QListView): + """List view for displaying the packages of an ATO.""" + + def __init__(self, model: AtoModel) -> None: + super().__init__() + self.ato_model = model + self.setModel(model) + self.setIconSize(QSize(91, 24)) + self.setSelectionBehavior(QAbstractItemView.SelectItems) + + @property + def selected_item(self) -> Optional[Package]: + """Returns the selected package, if any.""" + index = self.currentIndex() + if not index.isValid(): + return None + return self.ato_model.package_at_index(index) + + +class QPackagePanel(QGroupBox): + """The package display portion of the ATO panel. + + Displays the package assigned to the player's ATO, and includes edit and + delete buttons for package management. + """ + + def __init__(self, model: AtoModel) -> None: + super().__init__("Packages") + self.ato_model = model + self.ato_model.layoutChanged.connect(self.on_selection_changed) + + self.vbox = QVBoxLayout() + self.setLayout(self.vbox) + + self.package_list = QPackageList(self.ato_model) + self.vbox.addWidget(self.package_list) + + self.button_row = QHBoxLayout() + self.vbox.addLayout(self.button_row) + + self.edit_button = QPushButton("Edit") + self.edit_button.clicked.connect(self.on_edit) + self.button_row.addWidget(self.edit_button) + + self.delete_button = QPushButton("Delete") + # noinspection PyTypeChecker + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_row.addWidget(self.delete_button) + + self.selection_changed.connect(self.on_selection_changed) + self.on_selection_changed() + + @property + def selection_changed(self): + """Returns the signal emitted when the flight selection changes.""" + return self.package_list.selectionModel().selectionChanged + + def on_selection_changed(self) -> None: + """Updates the status of the edit and delete buttons.""" + index = self.package_list.currentIndex() + enabled = index.isValid() + self.edit_button.setEnabled(enabled) + self.delete_button.setEnabled(enabled) + + def on_edit(self) -> None: + """Opens the package edit dialog.""" + index = self.package_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot edit package when no package is selected.") + return + from qt_ui.dialogs import Dialog + Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index)) + + def on_delete(self) -> None: + """Removes the package from the ATO.""" + index = self.package_list.currentIndex() + if not index.isValid(): + logging.error(f"Cannot delete package when no package is selected.") + return + self.ato_model.delete_package_at_index(index) + + +class QAirTaskingOrderPanel(QSplitter): + """A split panel for displaying the packages and flights of an ATO. + + Used as the left-bar of the main UI. The top half of the panel displays the + packages of the player's ATO, and the bottom half displays the flights of + the selected package. + """ + def __init__(self, game_model: GameModel) -> None: + super().__init__(Qt.Vertical) + self.ato_model = game_model.ato_model + + self.package_panel = QPackagePanel(self.ato_model) + self.package_panel.selection_changed.connect(self.on_package_change) + self.ato_model.rowsInserted.connect(self.on_package_change) + self.addWidget(self.package_panel) + + self.flight_panel = QFlightPanel(game_model) + self.addWidget(self.flight_panel) + + def on_package_change(self) -> None: + """Sets the newly selected flight for display in the bottom panel.""" + index = self.package_panel.package_list.currentIndex() + if index.isValid(): + self.flight_panel.set_package( + self.ato_model.get_package_model(index) + ) + else: + self.flight_panel.set_package(None) diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py new file mode 100644 index 00000000..1f490e4d --- /dev/null +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -0,0 +1,16 @@ +"""Combo box for selecting aircraft types.""" +from typing import Iterable + +from PySide2.QtWidgets import QComboBox + +from dcs.planes import PlaneType + + +class QAircraftTypeSelector(QComboBox): + """Combo box for selecting among the given aircraft types.""" + + def __init__(self, aircraft_types: Iterable[PlaneType]) -> None: + super().__init__() + for aircraft in aircraft_types: + self.addItem(f"{aircraft.id}", userData=aircraft) + self.model().sort(0) diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py new file mode 100644 index 00000000..9a04df68 --- /dev/null +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -0,0 +1,22 @@ +"""Combo box for selecting a flight's task type.""" +from PySide2.QtWidgets import QComboBox + +from gen.flights.flight import FlightType + + +class QFlightTypeComboBox(QComboBox): + """Combo box for selecting a flight task type.""" + + def __init__(self) -> None: + super().__init__() + self.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP) + self.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP) + self.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP) + self.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION) + self.addItem("CAS [Close Air Support]", userData=FlightType.CAS) + self.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI) + self.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD) + self.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD) + self.addItem("STRIKE [Strike]", userData=FlightType.STRIKE) + self.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP) + self.model().sort(0) diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py new file mode 100644 index 00000000..b0995a6b --- /dev/null +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -0,0 +1,41 @@ +"""Combo box for selecting a departure airfield.""" +from typing import Iterable + +from PySide2.QtWidgets import QComboBox + +from dcs.planes import PlaneType +from game.inventory import GlobalAircraftInventory +from theater.controlpoint import ControlPoint + + +class QOriginAirfieldSelector(QComboBox): + """A combo box for selecting a flight's departure airfield. + + The combo box will automatically be populated with all departure airfields + that have unassigned inventory of the given aircraft type. + """ + + def __init__(self, global_inventory: GlobalAircraftInventory, + origins: Iterable[ControlPoint], + aircraft: PlaneType) -> None: + super().__init__() + self.global_inventory = global_inventory + self.origins = list(origins) + self.aircraft = aircraft + self.rebuild_selector() + + def change_aircraft(self, aircraft: PlaneType) -> None: + if self.aircraft == aircraft: + return + self.aircraft = aircraft + self.rebuild_selector() + + def rebuild_selector(self) -> None: + self.clear() + for origin in self.origins: + inventory = self.global_inventory.for_control_point(origin) + available = inventory.available(self.aircraft) + if available: + self.addItem(f"{origin.name} ({available} available)", origin) + self.model().sort(0) + self.update() diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 40109381..69723619 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -17,6 +17,7 @@ from game import Game, db from game.data.radar_db import UNITS_WITH_RADAR from gen import Conflict from gen.flights.flight import Flight +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 @@ -37,9 +38,10 @@ class QLiberationMap(QGraphicsView): "flight_paths": False } - def __init__(self, game: Game): + def __init__(self, game_model: GameModel): super(QLiberationMap, self).__init__() QLiberationMap.instance = self + self.game_model = game_model self.frontline_vector_cache = {} @@ -50,7 +52,7 @@ class QLiberationMap(QGraphicsView): self.factorized = 1 self.init_scene() self.connectSignals() - self.setGame(game) + self.setGame(game_model.game) def init_scene(self): scene = QLiberationScene(self) @@ -129,8 +131,10 @@ class QLiberationMap(QGraphicsView): pos = self._transform_point(cp.position) - scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE, - CONST.CP_SIZE, cp, self.game)) + scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, + pos[1] - CONST.CP_SIZE / 2, + CONST.CP_SIZE, + CONST.CP_SIZE, cp, self.game_model)) if cp.captured: pen = QPen(brush=CONST.COLORS[playerColor]) @@ -185,11 +189,9 @@ class QLiberationMap(QGraphicsView): text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1) def draw_flight_plans(self, scene) -> None: - for cp in self.game.theater.controlpoints: - if cp.id in self.game.planners: - planner = self.game.planners[cp.id] - for flight in planner.flights: - self.draw_flight_plan(scene, flight) + for package in self.game_model.ato_model.packages: + for flight in package.flights: + self.draw_flight_plan(scene, flight) def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None: is_player = flight.from_cp.captured diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index a006cb5a..f5b2e1c4 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -1,29 +1,23 @@ from typing import Optional from PySide2.QtGui import QColor, QPainter -from PySide2.QtWidgets import ( - QAction, - QGraphicsSceneContextMenuEvent, - QMenu, -) import qt_ui.uiconstants as const -from game import Game +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 from theater import ControlPoint from .QMapObject import QMapObject class QMapControlPoint(QMapObject): - def __init__(self, parent, x: float, y: float, w: float, h: float, - model: ControlPoint, game: Game) -> None: - super().__init__(x, y, w, h) - self.model = model - self.game = game + control_point: ControlPoint, game_model: GameModel) -> None: + super().__init__(x, y, w, h, mission_target=control_point) + self.game_model = game_model + self.control_point = control_point self.parent = parent self.setZValue(1) - self.setToolTip(self.model.name) + self.setToolTip(self.control_point.name) self.base_details_dialog: Optional[QBaseMenu2] = None def paint(self, painter, option, widget=None) -> None: @@ -33,7 +27,7 @@ class QMapControlPoint(QMapObject): painter.setBrush(self.brush_color) painter.setPen(self.pen_color) - if self.model.has_runway(): + if self.control_point.has_runway(): if self.isUnderMouse(): painter.setBrush(const.COLORS["white"]) painter.setPen(self.pen_color) @@ -44,22 +38,9 @@ class QMapControlPoint(QMapObject): # Either don't draw them at all, or perhaps use a sunk ship icon. painter.restore() - def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: - if self.model.captured: - text = "Open base menu" - else: - text = "Open intel menu" - - open_menu = QAction(text) - open_menu.triggered.connect(self.on_click) - - menu = QMenu("Menu", self.parent) - menu.addAction(open_menu) - menu.exec_(event.screenPos()) - @property def brush_color(self) -> QColor: - if self.model.captured: + if self.control_point.captured: return const.COLORS["blue"] else: return const.COLORS["super_red"] @@ -68,10 +49,17 @@ class QMapControlPoint(QMapObject): def pen_color(self) -> QColor: return const.COLORS["white"] + @property + def object_dialog_text(self) -> str: + if self.control_point.captured: + return "Open base menu" + else: + return "Open intel menu" + def on_click(self) -> None: self.base_details_dialog = QBaseMenu2( self.window(), - self.model, - self.game + self.control_point, + self.game_model ) self.base_details_dialog.show() diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py index 89d1bf07..1ed9f3d2 100644 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ b/qt_ui/widgets/map/QMapGroundObject.py @@ -14,11 +14,12 @@ from .QMapObject import QMapObject class QMapGroundObject(QMapObject): def __init__(self, parent, x: float, y: float, w: float, h: float, - cp: ControlPoint, model: TheaterGroundObject, game: Game, + control_point: ControlPoint, + ground_object: TheaterGroundObject, game: Game, buildings: Optional[List[TheaterGroundObject]] = None) -> None: - super().__init__(x, y, w, h) - self.model = model - self.cp = cp + super().__init__(x, y, w, h, mission_target=ground_object) + self.ground_object = ground_object + self.control_point = control_point self.parent = parent self.game = game self.setZValue(2) @@ -26,21 +27,20 @@ class QMapGroundObject(QMapObject): self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) self.ground_object_dialog: Optional[QGroundObjectMenu] = None - if len(self.model.groups) > 0: + if self.ground_object.groups: units = {} - for g in self.model.groups: - print(g) + for g in self.ground_object.groups: for u in g.units: if u.type in units: units[u.type] = units[u.type]+1 else: units[u.type] = 1 - tooltip = "[" + self.model.obj_name + "]" + "\n" + tooltip = "[" + self.ground_object.obj_name + "]" + "\n" for unit in units.keys(): tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n" self.setToolTip(tooltip[:-1]) else: - tooltip = "[" + self.model.obj_name + "]" + "\n" + tooltip = "[" + self.ground_object.obj_name + "]" + "\n" for building in buildings: if not building.is_dead: tooltip = tooltip + str(building.dcs_identifier) + "\n" @@ -53,20 +53,20 @@ class QMapGroundObject(QMapObject): if self.parent.get_display_rule("go"): painter.save() - cat = self.model.category - if cat == "aa" and self.model.sea_object: + cat = self.ground_object.category + if cat == "aa" and self.ground_object.sea_object: cat = "ship" rect = QRect(option.rect.x() + 2, option.rect.y(), option.rect.width() - 2, option.rect.height()) - is_dead = self.model.is_dead + is_dead = self.ground_object.is_dead for building in self.buildings: if not building.is_dead: is_dead = False break - if not is_dead and not self.cp.captured: + if not is_dead and not self.control_point.captured: painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) elif not is_dead: painter.drawPixmap(rect, const.ICONS[cat + player_icons]) @@ -80,7 +80,7 @@ class QMapGroundObject(QMapObject): units_alive = 0 units_dead = 0 - if len(self.model.groups) == 0: + if len(self.ground_object.groups) == 0: for building in self.buildings: if building.dcs_identifier in FORTIFICATION_BUILDINGS: continue @@ -89,7 +89,7 @@ class QMapGroundObject(QMapObject): else: units_alive += 1 - for g in self.model.groups: + for g in self.ground_object.groups: units_alive += len(g.units) if hasattr(g, "units_losts"): units_dead += len(g.units_losts) @@ -106,9 +106,9 @@ class QMapGroundObject(QMapObject): def on_click(self) -> None: self.ground_object_dialog = QGroundObjectMenu( self.window(), - self.model, + self.ground_object, self.buildings, - self.cp, + self.control_point, self.game ) self.ground_object_dialog.show() diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py index 1e29c7dd..c98cce5e 100644 --- a/qt_ui/widgets/map/QMapObject.py +++ b/qt_ui/widgets/map/QMapObject.py @@ -1,11 +1,20 @@ """Common base for objects drawn on the game map.""" +from typing import Optional + from PySide2.QtCore import Qt from PySide2.QtWidgets import ( + QAction, QGraphicsRectItem, + QGraphicsSceneContextMenuEvent, QGraphicsSceneHoverEvent, QGraphicsSceneMouseEvent, + QMenu, ) +from qt_ui.dialogs import Dialog +from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog +from theater.missiontarget import MissionTarget + class QMapObject(QGraphicsRectItem): """Base class for objects drawn on the game map. @@ -13,8 +22,12 @@ class QMapObject(QGraphicsRectItem): Game map objects have an on_click behavior that triggers on left click, and change the mouse cursor on hover. """ - def __init__(self, x: float, y: float, w: float, h: float): + + def __init__(self, x: float, y: float, w: float, h: float, + mission_target: MissionTarget) -> None: super().__init__(x, y, w, h) + self.mission_target = mission_target + self.new_package_dialog: Optional[QNewPackageDialog] = None self.setAcceptHoverEvents(True) def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): @@ -24,5 +37,39 @@ class QMapObject(QGraphicsRectItem): if event.button() == Qt.LeftButton: self.on_click() + def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: + menu = QMenu("Menu", self.parent) + + 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/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index da50e03f..f9daed62 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,22 +1,36 @@ import logging import sys import webbrowser +from typing import Optional from PySide2.QtCore import Qt from PySide2.QtGui import QIcon -from PySide2.QtWidgets import QWidget, QVBoxLayout, QMainWindow, QAction, QMessageBox, QDesktopWidget, \ - QSplitter, QFileDialog +from PySide2.QtWidgets import ( + QAction, + QDesktopWidget, + QFileDialog, + QMainWindow, + QMessageBox, + QSplitter, + QVBoxLayout, + QWidget, +) import qt_ui.uiconstants as CONST from game import Game +from game.inventory import GlobalAircraftInventory +from qt_ui.dialogs import Dialog +from qt_ui.models import GameModel from qt_ui.uiconstants import URLS from qt_ui.widgets.QTopPanel import QTopPanel +from qt_ui.widgets.ato import QAirTaskingOrderPanel from qt_ui.widgets.map.QLiberationMap import QLiberationMap -from qt_ui.windows.GameUpdateSignal import GameUpdateSignal, DebriefingSignal +from qt_ui.windows.GameUpdateSignal import DebriefingSignal, GameUpdateSignal from qt_ui.windows.QDebriefingWindow import QDebriefingWindow -from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard from qt_ui.windows.infos.QInfoPanel import QInfoPanel -from qt_ui.windows.preferences.QLiberationPreferencesWindow import QLiberationPreferencesWindow +from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard +from qt_ui.windows.preferences.QLiberationPreferencesWindow import \ + QLiberationPreferencesWindow from userdata import persistency @@ -25,6 +39,10 @@ class QLiberationWindow(QMainWindow): def __init__(self): super(QLiberationWindow, self).__init__() + self.game: Optional[Game] = None + self.game_model = GameModel() + Dialog.set_game(self.game_model) + self.ato_panel = None self.info_panel = None self.setGame(persistency.restore_game()) @@ -44,16 +62,19 @@ class QLiberationWindow(QMainWindow): self.setGeometry(0, 0, screen.width(), screen.height()) self.setWindowState(Qt.WindowMaximized) - def initUi(self): - - self.liberation_map = QLiberationMap(self.game) + self.ato_panel = QAirTaskingOrderPanel(self.game_model) + self.liberation_map = QLiberationMap(self.game_model) self.info_panel = QInfoPanel(self.game) hbox = QSplitter(Qt.Horizontal) - hbox.addWidget(self.info_panel) - hbox.addWidget(self.liberation_map) - hbox.setSizes([2, 8]) + vbox = QSplitter(Qt.Vertical) + hbox.addWidget(self.ato_panel) + hbox.addWidget(vbox) + vbox.addWidget(self.liberation_map) + vbox.addWidget(self.info_panel) + hbox.setSizes([100, 600]) + vbox.setSizes([600, 100]) vbox = QVBoxLayout() vbox.setMargin(0) @@ -210,10 +231,11 @@ class QLiberationWindow(QMainWindow): def exit(self): sys.exit(0) - def setGame(self, game: Game): + def setGame(self, game: Optional[Game]): self.game = game if self.info_panel: self.info_panel.setGame(game) + self.game_model.set(self.game) def showAboutDialog(self): text = "

DCS Liberation " + CONST.VERSION_STRING + "

" + \ diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 678f6098..597c2a32 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -1,9 +1,9 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QPixmap -from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget, QDialog, QGridLayout +from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget -from game import Game from game.event import ControlPointType +from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs @@ -13,19 +13,20 @@ from theater import ControlPoint class QBaseMenu2(QDialog): - def __init__(self, parent, cp: ControlPoint, game: Game): + def __init__(self, parent, cp: ControlPoint, game_model: GameModel): super(QBaseMenu2, self).__init__(parent) # Attrs self.cp = cp - self.game = game + self.game_model = game_model self.is_carrier = self.cp.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] self.objectName = "menuDialogue" # Widgets - self.qbase_menu_tab = QBaseMenuTabs(cp, game) + self.qbase_menu_tab = QBaseMenuTabs(cp, self.game_model) try: + game = self.game_model.game self.airport = game.theater.terrain.airport_by_id(self.cp.id) except: self.airport = None @@ -70,7 +71,9 @@ class QBaseMenu2(QDialog): self.mainLayout.addWidget(header, 0, 0) self.mainLayout.addWidget(self.topLayoutWidget, 1, 0) self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0) - totalBudget = QLabel(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + totalBudget = QLabel( + QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget) + ) totalBudget.setObjectName("budgetField") totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom) totalBudget.setProperty("style", "budget-label") @@ -78,7 +81,7 @@ class QBaseMenu2(QDialog): self.setLayout(self.mainLayout) def closeEvent(self, closeEvent:QCloseEvent): - GameUpdateSignal.get_instance().updateGame(self.game) + GameUpdateSignal.get_instance().updateGame(self.game_model.game) def get_base_image(self): if self.cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index dcb05ee6..11846bda 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -1,6 +1,6 @@ -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QTabWidget -from game import Game +from qt_ui.models import GameModel from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ @@ -10,29 +10,29 @@ from theater import ControlPoint class QBaseMenuTabs(QTabWidget): - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): super(QBaseMenuTabs, self).__init__() self.cp = cp if cp: if not cp.captured: - self.intel = QIntelInfo(cp, game) + self.intel = QIntelInfo(cp, game_model.game) self.addTab(self.intel, "Intel") if not cp.is_carrier: - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Base Defenses") else: if cp.has_runway(): - self.airfield_command = QAirfieldCommand(cp, game) + self.airfield_command = QAirfieldCommand(cp, game_model) self.addTab(self.airfield_command, "Airfield Command") if not cp.is_carrier: - self.ground_forces_hq = QGroundForcesHQ(cp, game) + self.ground_forces_hq = QGroundForcesHQ(cp, game_model) self.addTab(self.ground_forces_hq, "Ground Forces HQ") - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Base Defenses") else: - self.base_defenses_hq = QBaseDefensesHQ(cp, game) + self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) self.addTab(self.base_defenses_hq, "Fleet") else: diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index a42b00d5..a349e4bc 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,25 +1,34 @@ -from PySide2.QtWidgets import QLabel, QPushButton, \ - QSizePolicy, QSpacerItem, QGroupBox, QHBoxLayout +from PySide2.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QSpacerItem, +) from dcs.unittype import UnitType from theater import db -class QRecruitBehaviour: - game = None - cp = None - deliveryEvent = None - existing_units_labels = None - bought_amount_labels = None +class QRecruitBehaviour: BUDGET_FORMAT = "Available Budget: ${}M" - def __init__(self): + def __init__(self) -> None: + self.deliveryEvent = None self.bought_amount_labels = {} self.existing_units_labels = {} self.update_available_budget() - def add_purchase_row(self, unit_type, layout, row): + @property + def budget(self) -> int: + return self.game_model.game.budget + @budget.setter + def budget(self, value: int) -> None: + self.game_model.game.budget = value + + def add_purchase_row(self, unit_type, layout, row): exist = QGroupBox() exist.setProperty("style", "buy-box") exist.setMaximumHeight(36) @@ -98,27 +107,28 @@ class QRecruitBehaviour: parent = parent.parent() for child in parent.children(): if child.objectName() == "budgetField": - child.setText(QRecruitBehaviour.BUDGET_FORMAT.format(self.game.budget)) + child.setText( + QRecruitBehaviour.BUDGET_FORMAT.format(self.budget)) def buy(self, unit_type): price = db.PRICES[unit_type] - if self.game.budget >= price: + if self.budget >= price: self.deliveryEvent.deliver({unit_type: 1}) - self.game.budget -= price + self.budget -= price self._update_count_label(unit_type) self.update_available_budget() def sell(self, unit_type): if self.deliveryEvent.units.get(unit_type, 0) > 0: price = db.PRICES[unit_type] - self.game.budget += price + self.budget += price self.deliveryEvent.units[unit_type] = self.deliveryEvent.units[unit_type] - 1 if self.deliveryEvent.units[unit_type] == 0: del self.deliveryEvent.units[unit_type] elif self.cp.base.total_units_of_type(unit_type) > 0: price = db.PRICES[unit_type] - self.game.budget += price + self.budget += price self.cp.base.commit_losses({unit_type: 1}) self._update_count_label(unit_type) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 2d4621d6..73f23617 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -1,27 +1,35 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QScrollArea, QFrame, QWidget +from typing import Optional -from game.event import UnitsDeliveryEvent +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) + +from game.event.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint, CAP, CAS, db -from game import Game +from theater import CAP, CAS, ControlPoint, db class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): - - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model + self.deliveryEvent: Optional[UnitsDeliveryEvent] = None self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game.events: + for event in self.game_model.game.events: if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: self.deliveryEvent = event if not self.deliveryEvent: - self.deliveryEvent = self.game.units_delivery_event(self.cp) + self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) self.init_ui() @@ -29,8 +37,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): main_layout = QVBoxLayout() units = { - CAP: db.find_unittype(CAP, self.game.player_name), - CAS: db.find_unittype(CAS, self.game.player_name), + CAP: db.find_unittype(CAP, self.game_model.game.player_name), + CAS: db.find_unittype(CAS, self.game_model.game.player_name), } scroll_content = QWidget() @@ -39,7 +47,8 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): for task_type in units.keys(): units_column = list(set(units[task_type])) - if len(units_column) == 0: continue + if len(units_column) == 0: + continue units_column.sort(key=lambda x: db.PRICES[x]) for unit_type in units_column: if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE: diff --git a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py index 74b3c973..5274640d 100644 --- a/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py +++ b/qt_ui/windows/basemenu/airfield/QAirfieldCommand.py @@ -1,27 +1,30 @@ -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QHBoxLayout, QGroupBox, QVBoxLayout -from game import Game -from qt_ui.widgets.base.QAirportInformation import QAirportInformation -from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import QAircraftRecruitmentMenu +from PySide2.QtWidgets import QFrame, QGridLayout, QGroupBox, QVBoxLayout + +from qt_ui.models import GameModel +from qt_ui.windows.basemenu.airfield.QAircraftRecruitmentMenu import \ + QAircraftRecruitmentMenu from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView from theater import ControlPoint class QAirfieldCommand(QFrame): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp:ControlPoint, game_model: GameModel): super(QAirfieldCommand, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game), 0, 0) + layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0) try: planned = QGroupBox("Planned Flights") planned_layout = QVBoxLayout() - planned_layout.addWidget(QPlannedFlightsView(self.game.planners[self.cp.id])) + planned_layout.addWidget( + QPlannedFlightsView(self.game_model, self.cp) + ) planned.setLayout(planned_layout) layout.addWidget(planned, 0, 1) except: diff --git a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py index e260d64c..ec1cabf6 100644 --- a/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/ground_forces/QArmorRecruitmentMenu.py @@ -1,27 +1,33 @@ from PySide2.QtCore import Qt -from PySide2.QtWidgets import QVBoxLayout, QGridLayout, QGroupBox, QFrame, QWidget, QScrollArea +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QScrollArea, + QVBoxLayout, + QWidget, +) -from game import Game from game.event import UnitsDeliveryEvent +from qt_ui.models import GameModel from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from theater import ControlPoint, PinpointStrike, db class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel): QFrame.__init__(self) self.cp = cp - self.game = game + self.game_model = game_model self.bought_amount_labels = {} self.existing_units_labels = {} - for event in self.game.events: + for event in self.game_model.game.events: if event.__class__ == UnitsDeliveryEvent and event.from_cp == self.cp: self.deliveryEvent = event if not self.deliveryEvent: - self.deliveryEvent = self.game.units_delivery_event(self.cp) + self.deliveryEvent = self.game_model.game.units_delivery_event(self.cp) self.init_ui() @@ -29,7 +35,8 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour): main_layout = QVBoxLayout() units = { - PinpointStrike: db.find_unittype(PinpointStrike, self.game.player_name), + PinpointStrike: db.find_unittype(PinpointStrike, + self.game_model.game.player_name), } scroll_content = QWidget() diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py index 1ea116e3..bb18594f 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesHQ.py @@ -1,21 +1,24 @@ from PySide2.QtWidgets import QFrame, QGridLayout -from game import Game -from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import QArmorRecruitmentMenu -from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import QGroundForcesStrategy +from qt_ui.models import GameModel +from qt_ui.windows.basemenu.ground_forces.QArmorRecruitmentMenu import \ + QArmorRecruitmentMenu +from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategy import \ + QGroundForcesStrategy from theater import ControlPoint class QGroundForcesHQ(QFrame): - def __init__(self, cp:ControlPoint, game:Game): + def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: super(QGroundForcesHQ, self).__init__() self.cp = cp - self.game = game + self.game_model = game_model self.init_ui() def init_ui(self): layout = QGridLayout() - layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game), 0, 0) - layout.addWidget(QGroundForcesStrategy(self.cp, self.game), 0, 1) + layout.addWidget(QArmorRecruitmentMenu(self.cp, self.game_model), 0, 0) + layout.addWidget(QGroundForcesStrategy(self.cp, self.game_model.game), + 0, 1) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py new file mode 100644 index 00000000..29e4eafb --- /dev/null +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -0,0 +1,29 @@ +"""Dialog window for editing flights.""" +from PySide2.QtWidgets import ( + QDialog, + QVBoxLayout, +) + +from game import Game +from gen.flights.flight import Flight +from qt_ui.uiconstants import EVENT_ICONS +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: + super().__init__() + + self.game = game + + self.setWindowTitle("Create flight") + self.setWindowIcon(EVENT_ICONS["strike"]) + + layout = QVBoxLayout() + + self.flight_planner = QFlightPlanner(flight, game) + layout.addWidget(self.flight_planner) + + self.setLayout(layout) diff --git a/qt_ui/windows/mission/QMissionPlanning.py b/qt_ui/windows/mission/QMissionPlanning.py deleted file mode 100644 index 04ffdeb3..00000000 --- a/qt_ui/windows/mission/QMissionPlanning.py +++ /dev/null @@ -1,159 +0,0 @@ -from PySide2.QtCore import Qt, Slot, QItemSelectionModel, QPoint -from PySide2.QtWidgets import QDialog, QGridLayout, QScrollArea, QVBoxLayout, QPushButton, QHBoxLayout, QMessageBox -from game import Game -from game.event import CAP, CAS, FrontlineAttackEvent -from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow -from qt_ui.windows.mission.QPlannedFlightsView import QPlannedFlightsView -from qt_ui.windows.mission.QChooseAirbase import QChooseAirbase -from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner - - -class QMissionPlanning(QDialog): - - def __init__(self, game: Game): - super(QMissionPlanning, self).__init__() - self.game = game - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setMinimumSize(1000, 440) - self.setWindowTitle("Mission Preparation") - self.setWindowIcon(EVENT_ICONS["strike"]) - self.init_ui() - print("DONE") - - def init_ui(self): - - self.captured_cp = [cp for cp in self.game.theater.controlpoints if cp.captured] - - self.layout = QGridLayout() - self.left_bar_layout = QVBoxLayout() - - self.select_airbase = QChooseAirbase(self.game) - self.select_airbase.selected_airbase_changed.connect(self.on_departure_cp_changed) - self.planned_flight_view = QPlannedFlightsView(None) - self.available_aircraft_at_selected_location = {} - if self.captured_cp[0].id in self.game.planners.keys(): - self.planner = self.game.planners[self.captured_cp[0].id] - self.planned_flight_view.set_flight_planner(self.planner) - self.selected_cp = self.captured_cp[0] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - - self.planned_flight_view.selectionModel().setCurrentIndex(self.planned_flight_view.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows) - self.planned_flight_view.selectionModel().selectionChanged.connect(self.on_flight_selection_change) - - if len(self.planned_flight_view.flight_planner.flights) > 0: - self.flight_planner = QFlightPlanner(self.planned_flight_view.flight_planner.flights[0], self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - else: - self.flight_planner = QFlightPlanner(None, self.game, self.planned_flight_view.flight_planner, 0) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - - self.add_flight_button = QPushButton("Add Flight") - self.add_flight_button.clicked.connect(self.on_add_flight) - self.delete_flight_button = QPushButton("Delete Selected") - self.delete_flight_button.setProperty("style", "btn-danger") - self.delete_flight_button.clicked.connect(self.on_delete_flight) - - self.button_layout = QHBoxLayout() - self.button_layout.addStretch() - self.button_layout.addWidget(self.delete_flight_button) - self.button_layout.addWidget(self.add_flight_button) - - self.mission_start_button = QPushButton("Take Off") - self.mission_start_button.setProperty("style", "start-button") - self.mission_start_button.clicked.connect(self.on_start) - - self.left_bar_layout.addWidget(self.select_airbase) - self.left_bar_layout.addWidget(self.planned_flight_view) - self.left_bar_layout.addLayout(self.button_layout) - - self.layout.addLayout(self.left_bar_layout, 0, 0) - self.layout.addWidget(self.flight_planner, 0, 1) - self.layout.addWidget(self.mission_start_button, 1, 1, alignment=Qt.AlignRight) - - self.setLayout(self.layout) - - @Slot(str) - def on_departure_cp_changed(self, cp_name): - cps = [cp for cp in self.game.theater.controlpoints if cp.name == cp_name] - - print(cps) - - if len(cps) == 1: - self.selected_cp = cps[0] - self.planner = self.game.planners[cps[0].id] - self.available_aircraft_at_selected_location = self.planner.get_available_aircraft() - self.planned_flight_view.set_flight_planner(self.planner) - else: - self.available_aircraft_at_selected_location = {} - self.planned_flight_view.set_flight_planner(None) - - def on_flight_selection_change(self): - - print("On flight selection change") - - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planned_flight_view.repaint() - - if self.flight_planner is not None: - self.flight_planner.on_planned_flight_changed.disconnect() - self.flight_planner.clearTabs() - - try: - flight = self.planner.flights[index] - except IndexError: - flight = None - self.flight_planner = QFlightPlanner(flight, self.game, self.planner, self.flight_planner.currentIndex()) - self.flight_planner.on_planned_flight_changed.connect(self.update_planned_flight_view) - self.layout.addWidget(self.flight_planner, 0, 1) - - def update_planned_flight_view(self): - self.planned_flight_view.update_content() - - def on_add_flight(self): - possible_aircraft_type = list(self.selected_cp.base.aircraft.keys()) - - if len(possible_aircraft_type) == 0: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("No more aircraft are available on " + self.selected_cp.name + " airbase.") - msg.setWindowTitle("No more aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() - else: - self.subwindow = QFlightCreator(self.game, self.selected_cp, possible_aircraft_type, self.planned_flight_view) - self.subwindow.show() - - def on_delete_flight(self): - index = self.planned_flight_view.selectionModel().currentIndex().row() - self.planner.remove_flight(index) - self.planned_flight_view.set_flight_planner(self.planner, index) - - - def on_start(self): - - # TODO : refactor this nonsense - self.gameEvent = None - for event in self.game.events: - if isinstance(event, FrontlineAttackEvent) and event.is_player_attacking: - self.gameEvent = event - if self.gameEvent is None: - self.gameEvent = FrontlineAttackEvent(self.game, self.game.theater.controlpoints[0], self.game.theater.controlpoints[0], - self.game.theater.controlpoints[0].position, self.game.player_name, self.game.enemy_name) - #if self.awacs_checkbox.isChecked() == 1: - # self.gameEvent.is_awacs_enabled = True - # self.game.awacs_expense_commit() - #else: - # self.gameEvent.is_awacs_enabled = False - self.gameEvent.is_awacs_enabled = True - self.gameEvent.ca_slots = 1 - self.gameEvent.departure_cp = self.game.theater.controlpoints[0] - self.gameEvent.player_attacking({CAS:{}, CAP:{}}) - self.gameEvent.depart_from = self.game.theater.controlpoints[0] - - self.game.initiate_event(self.gameEvent) - waiting = QWaitingForMissionResultWindow(self.gameEvent, self.game) - waiting.show() - self.close() diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py new file mode 100644 index 00000000..2b27a035 --- /dev/null +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -0,0 +1,198 @@ +"""Dialogs for creating and editing ATO packages.""" +import logging +from typing import Optional + +from PySide2.QtCore import QItemSelection, Signal +from PySide2.QtWidgets import ( + QDialog, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, +) + +from game.game import Game +from gen.ato import Package +from gen.flights.flight import Flight +from qt_ui.models import AtoModel, PackageModel +from qt_ui.uiconstants import EVENT_ICONS +from qt_ui.widgets.ato import QFlightList +from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator +from theater.missiontarget import MissionTarget + + +class QPackageDialog(QDialog): + """Base package management dialog. + + The dialogs for creating a new package and editing an existing dialog are + very similar, and this implements the shared behavior. + """ + + #: Emitted when a change is made to the package. + package_changed = Signal() + + #: Emitted when a flight is added to the package. + flight_added = Signal(Flight) + + #: Emitted when a flight is removed from the package. + flight_removed = Signal(Flight) + + def __init__(self, game: Game, model: PackageModel) -> None: + super().__init__() + self.game = game + self.package_model = model + self.add_flight_dialog: Optional[QFlightCreator] = None + + self.setMinimumSize(1000, 440) + self.setWindowTitle( + f"Mission Package: {self.package_model.mission_target.name}" + ) + self.setWindowIcon(EVENT_ICONS["strike"]) + + self.layout = QVBoxLayout() + + self.summary_row = QHBoxLayout() + self.layout.addLayout(self.summary_row) + + self.package_type_label = QLabel("Package Type:") + self.package_type_text = QLabel(self.package_model.description) + # noinspection PyUnresolvedReferences + self.package_changed.connect(lambda: self.package_type_text.setText( + self.package_model.description + )) + self.summary_row.addWidget(self.package_type_label) + self.summary_row.addWidget(self.package_type_text) + + self.package_view = QFlightList(self.package_model) + self.package_view.selectionModel().selectionChanged.connect( + self.on_selection_changed + ) + self.layout.addWidget(self.package_view) + + self.button_layout = QHBoxLayout() + self.layout.addLayout(self.button_layout) + + self.add_flight_button = QPushButton("Add Flight") + self.add_flight_button.clicked.connect(self.on_add_flight) + self.button_layout.addWidget(self.add_flight_button) + + self.delete_flight_button = QPushButton("Delete Selected") + self.delete_flight_button.setProperty("style", "btn-danger") + self.delete_flight_button.clicked.connect(self.on_delete_flight) + self.delete_flight_button.setEnabled(False) + self.button_layout.addWidget(self.delete_flight_button) + + self.button_layout.addStretch() + + self.setLayout(self.layout) + + def on_selection_changed(self, selected: QItemSelection, + _deselected: QItemSelection) -> None: + """Updates the state of the delete button.""" + self.delete_flight_button.setEnabled(not selected.empty()) + + 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.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) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + # noinspection PyUnresolvedReferences + self.flight_added.emit(flight) + + def on_delete_flight(self) -> None: + """Removes the selected flight from the package.""" + flight = self.package_view.selected_item + if flight is None: + logging.error(f"Cannot delete flight when no flight is selected.") + return + self.package_model.delete_flight(flight) + # noinspection PyUnresolvedReferences + self.package_changed.emit() + # noinspection PyUnresolvedReferences + self.flight_removed.emit(flight) + + +class QNewPackageDialog(QPackageDialog): + """Dialog window for creating a new package. + + New packages do not affect the ATO model until they are saved. + """ + + def __init__(self, game: Game, model: AtoModel, + target: MissionTarget) -> None: + super().__init__(game, PackageModel(Package(target))) + self.ato_model = model + + self.save_button = QPushButton("Save") + self.save_button.setProperty("style", "start-button") + self.save_button.clicked.connect(self.on_save) + self.button_layout.addWidget(self.save_button) + + self.delete_flight_button.clicked.connect(self.on_delete_flight) + + def on_save(self) -> None: + """Saves the created package. + + Empty packages may be created. They can be modified later, and will have + no effect if empty when the mission is generated. + """ + self.ato_model.add_package(self.package_model.package) + for flight in self.package_model.package.flights: + self.game.aircraft_inventory.claim_for_flight(flight) + self.close() + + +class QEditPackageDialog(QPackageDialog): + """Dialog window for editing an existing package. + + Changes to existing packages occur immediately. + """ + + def __init__(self, game: Game, model: AtoModel, + package: PackageModel) -> None: + super().__init__(game, package) + self.ato_model = model + + self.delete_button = QPushButton("Delete package") + self.delete_button.setProperty("style", "btn-danger") + self.delete_button.clicked.connect(self.on_delete) + self.button_layout.addWidget(self.delete_button) + + self.done_button = QPushButton("Done") + self.done_button.setProperty("style", "start-button") + self.done_button.clicked.connect(self.on_done) + self.button_layout.addWidget(self.done_button) + + # noinspection PyUnresolvedReferences + self.flight_added.connect(self.on_flight_added) + # noinspection PyUnresolvedReferences + self.flight_removed.connect(self.on_flight_removed) + + # TODO: Make the new package dialog do this too, return on cancel. + # Not claiming the aircraft when they are added to the planner means that + # inventory counts are not updated until after the new package is updated, + # so you can add an infinite number of aircraft to a new package in the UI, + # which will crash when the flight package is saved. + def on_flight_added(self, flight: Flight) -> None: + self.game.aircraft_inventory.claim_for_flight(flight) + + def on_flight_removed(self, flight: Flight) -> None: + self.game.aircraft_inventory.return_from_flight(flight) + + def on_done(self) -> None: + """Closes the window.""" + self.close() + + def on_delete(self) -> None: + """Removes the viewed package from the ATO.""" + # The ATO model returns inventory for us when deleting a package. + self.ato_model.delete_package(self.package_model.package) + self.close() diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py index 0dcc8a81..a7c45e51 100644 --- a/qt_ui/windows/mission/QPlannedFlightsView.py +++ b/qt_ui/windows/mission/QPlannedFlightsView.py @@ -1,37 +1,36 @@ -from PySide2.QtCore import QSize, QItemSelectionModel, QPoint +from PySide2.QtCore import QItemSelectionModel, QSize from PySide2.QtGui import QStandardItemModel -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtWidgets import QAbstractItemView, QListView -from gen.flights.ai_flight_planner import FlightPlanner +from qt_ui.models import GameModel from qt_ui.windows.mission.QFlightItem import QFlightItem +from theater.controlpoint import ControlPoint class QPlannedFlightsView(QListView): - def __init__(self, flight_planner: FlightPlanner): + def __init__(self, game_model: GameModel, cp: ControlPoint) -> None: super(QPlannedFlightsView, self).__init__() + self.game_model = game_model + self.cp = cp self.model = QStandardItemModel(self) self.setModel(self.model) - self.flightitems = [] + self.flight_items = [] self.setIconSize(QSize(91, 24)) self.setSelectionBehavior(QAbstractItemView.SelectItems) - if flight_planner: - self.set_flight_planner(flight_planner) + self.set_flight_planner() - def update_content(self): - for i, f in enumerate(self.flight_planner.flights): - self.flightitems[i].update(f) + def setup_content(self): + self.flight_items = [] + for package in self.game_model.ato_model.packages: + for flight in package.flights: + if flight.from_cp == self.cp: + item = QFlightItem(flight) + self.model.appendRow(item) + self.flight_items.append(item) + self.set_selected_flight(0) - def setup_content(self, row=0): - self.flightitems = [] - for i, f in enumerate(self.flight_planner.flights): - item = QFlightItem(f) - self.model.appendRow(item) - self.flightitems.append(item) - self.setSelectedFlight(row) - self.repaint() - - def setSelectedFlight(self, row): + def set_selected_flight(self, row): self.selectionModel().clearSelection() index = self.model.index(row, 0) if not index.isValid(): @@ -42,8 +41,6 @@ class QPlannedFlightsView(QListView): def clear_layout(self): self.model.removeRows(0, self.model.rowCount()) - def set_flight_planner(self, flight_planner: FlightPlanner, row=0): + def set_flight_planner(self) -> None: self.clear_layout() - self.flight_planner = flight_planner - if self.flight_planner: - self.setup_content(row) + self.setup_content() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 293ba75f..00df380d 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -1,122 +1,169 @@ -from typing import List +import logging +from typing import Optional -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QDialog, QGridLayout, QLabel, QComboBox, QHBoxLayout, QVBoxLayout, QPushButton, QSpinBox, \ - QMessageBox -from dcs import Point -from dcs.unittype import UnitType +from PySide2.QtCore import Qt, Signal +from PySide2.QtWidgets import ( + QDialog, + QMessageBox, + QPushButton, + QVBoxLayout, +) +from dcs.planes import PlaneType from game import Game +from gen.ato import Package from gen.flights.ai_flight_planner import FlightPlanner -from gen.flights.flight import Flight, FlightWaypoint, FlightType +from gen.flights.flight import Flight, FlightType from qt_ui.uiconstants import EVENT_ICONS -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox -from theater import ControlPoint - -PREDEFINED_WAYPOINT_CATEGORIES = [ - "Frontline (CAS AREA)", - "Building", - "Units", - "Airbase" -] +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, TheaterGroundObject class QFlightCreator(QDialog): + created = Signal(Flight) + + def __init__(self, game: Game, package: Package) -> None: + super().__init__() - def __init__(self, game: Game, from_cp:ControlPoint, possible_aircraft_type:List[UnitType], flight_view=None): - super(QFlightCreator, self).__init__() self.game = game - self.from_cp = from_cp - self.flight_view = flight_view - self.planner = self.game.planners[from_cp.id] - self.available = self.planner.get_available_aircraft() + self.package = package - self.setWindowFlags(Qt.WindowStaysOnTopHint) - self.setModal(True) self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) - self.select_type_aircraft = QComboBox() - for aircraft_type in self.planner.get_available_aircraft().keys(): - print(aircraft_type) - print(aircraft_type.name) - if self.available[aircraft_type] > 0: - self.select_type_aircraft.addItem(aircraft_type.id, userData=aircraft_type) - self.select_type_aircraft.setCurrentIndex(0) - - self.select_flight_type = QComboBox() - self.select_flight_type.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP) - self.select_flight_type.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP) - self.select_flight_type.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP) - self.select_flight_type.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION) - self.select_flight_type.addItem("CAS [Close Air Support]", userData=FlightType.CAS) - self.select_flight_type.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI) - self.select_flight_type.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD) - self.select_flight_type.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD) - self.select_flight_type.addItem("STRIKE [Strike]", userData=FlightType.STRIKE) - self.select_flight_type.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP) - self.select_flight_type.setCurrentIndex(0) - - self.select_count_of_aircraft = QSpinBox() - self.select_count_of_aircraft.setMinimum(1) - self.select_count_of_aircraft.setMaximum(4) - self.select_count_of_aircraft.setValue(2) - - aircraft_type = self.select_type_aircraft.currentData() - if aircraft_type is not None: - self.select_count_of_aircraft.setValue(min(self.available[aircraft_type], 2)) - self.select_count_of_aircraft.setMaximum(min(self.available[aircraft_type], 4)) - - self.add_button = QPushButton("Add") - self.add_button.clicked.connect(self.create_flight) - - self.init_ui() - - - def init_ui(self): layout = QVBoxLayout() - type_layout = QHBoxLayout() - type_layout.addWidget(QLabel("Type of Aircraft : ")) - type_layout.addStretch() - type_layout.addWidget(self.select_type_aircraft, alignment=Qt.AlignRight) + # TODO: Limit task selection to those valid for the target type. + self.task_selector = QFlightTypeComboBox() + self.task_selector.setCurrentIndex(0) + layout.addLayout(QLabeledWidget("Task:", self.task_selector)) - count_layout = QHBoxLayout() - count_layout.addWidget(QLabel("Count : ")) - count_layout.addStretch() - count_layout.addWidget(self.select_count_of_aircraft, alignment=Qt.AlignRight) + self.aircraft_selector = QAircraftTypeSelector( + self.game.aircraft_inventory.available_types_for_player + ) + self.aircraft_selector.setCurrentIndex(0) + self.aircraft_selector.currentIndexChanged.connect( + self.on_aircraft_changed) + layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector)) - flight_type_layout = QHBoxLayout() - flight_type_layout.addWidget(QLabel("Task : ")) - flight_type_layout.addStretch() - flight_type_layout.addWidget(self.select_flight_type, alignment=Qt.AlignRight) + self.airfield_selector = QOriginAirfieldSelector( + self.game.aircraft_inventory, + [cp for cp in game.theater.controlpoints if cp.captured], + self.aircraft_selector.currentData() + ) + layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector)) + + self.flight_size_spinner = QFlightSizeSpinner() + layout.addLayout(QLabeledWidget("Count:", self.flight_size_spinner)) - layout.addLayout(type_layout) - layout.addLayout(count_layout) - layout.addLayout(flight_type_layout) layout.addStretch() - layout.addWidget(self.add_button, alignment=Qt.AlignRight) + + self.create_button = QPushButton("Create") + self.create_button.clicked.connect(self.create_flight) + layout.addWidget(self.create_button, alignment=Qt.AlignRight) self.setLayout(layout) - def create_flight(self): - aircraft_type = self.select_type_aircraft.currentData() - count = self.select_count_of_aircraft.value() + def verify_form(self) -> Optional[str]: + aircraft: PlaneType = self.aircraft_selector.currentData() + origin: ControlPoint = self.airfield_selector.currentData() + size: int = self.flight_size_spinner.value() + if not origin.captured: + return f"{origin.name} is not owned by your coalition." + available = origin.base.aircraft.get(aircraft, 0) + if not available: + return f"{origin.name} has no {aircraft.id} available." + if size > available: + return f"{origin.name} has only {available} {aircraft.id} available." + return None - if self.available[aircraft_type] < count: - msg = QMessageBox() - msg.setIcon(QMessageBox.Information) - msg.setText("Not enough aircraft of this type are available. Only " + str(self.available[aircraft_type]) + " available.") - msg.setWindowTitle("Not enough aircraft") - msg.setStandardButtons(QMessageBox.Ok) - msg.setWindowFlags(Qt.WindowStaysOnTopHint) - msg.exec_() + def create_flight(self) -> None: + error = self.verify_form() + if error is not None: + self.error_box("Could not create flight", error) return - else: - flight = Flight(aircraft_type, count, self.from_cp, self.select_flight_type.currentData()) - self.planner.flights.append(flight) - self.planner.custom_flights.append(flight) - if self.flight_view is not None: - self.flight_view.set_flight_planner(self.planner, len(self.planner.flights)-1) - self.close() + task = self.task_selector.currentData() + aircraft = self.aircraft_selector.currentData() + origin = self.airfield_selector.currentData() + size = self.flight_size_spinner.value() + + flight = Flight(aircraft, size, origin, task) + self.populate_flight_plan(flight, task) + + # noinspection PyUnresolvedReferences + self.created.emit(flight) + self.close() + + def on_aircraft_changed(self, index: int) -> None: + new_aircraft = self.aircraft_selector.itemData(index) + self.airfield_selector.change_aircraft(new_aircraft) + + @property + def planner(self) -> FlightPlanner: + return self.game.planners[self.airfield_selector.currentData().id] + + 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: + logging.error("BAI flight plan generation not implemented") + elif task == FlightType.BARCAP: + self.generate_cap(flight) + elif task == FlightType.CAP: + self.generate_cap(flight) + elif task == FlightType.CAS: + logging.error("CAS flight plan generation not implemented") + elif task == FlightType.DEAD: + self.generate_sead(flight) + elif task == FlightType.ELINT: + logging.error("ELINT flight plan generation not implemented") + elif task == FlightType.EVAC: + logging.error("Evac flight plan generation not implemented") + elif task == FlightType.EWAR: + logging.error("EWar flight plan generation not implemented") + elif task == FlightType.INTERCEPTION: + logging.error("Intercept flight plan generation not implemented") + elif task == FlightType.LOGISTICS: + logging.error("Logistics flight plan generation not implemented") + elif task == FlightType.RECON: + logging.error("Recon flight plan generation not implemented") + elif task == FlightType.SEAD: + self.generate_sead(flight) + elif task == FlightType.STRIKE: + self.generate_strike(flight) + elif task == FlightType.TARCAP: + self.generate_cap(flight) + elif task == FlightType.TROOP_TRANSPORT: + logging.error( + "Troop transport flight plan generation not implemented" + ) + + def generate_cap(self, flight: Flight) -> None: + if not isinstance(self.package.target, ControlPoint): + logging.error( + "Could not create flight plan: CAP missions for strike targets " + "not implemented" + ) + return + self.planner.generate_barcap(flight, self.package.target) + + def generate_sead(self, flight: Flight) -> None: + self.planner.generate_sead(flight, self.package.target) + + def generate_strike(self, flight: Flight) -> None: + if not isinstance(self.package.target, TheaterGroundObject): + logging.error( + "Could not create flight plan: strike missions for capture " + "points not implemented" + ) + return + self.planner.generate_strike(flight, self.package.target) diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index 6e422b93..4eed4754 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -1,42 +1,31 @@ from PySide2.QtCore import Signal -from PySide2.QtWidgets import QTabWidget, QFrame, QGridLayout, QLabel +from PySide2.QtWidgets import QTabWidget -from gen.flights.flight import Flight from game import Game -from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import QFlightPayloadTab -from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import QGeneralFlightSettingsTab -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import QFlightWaypointTab +from gen.flights.flight import Flight +from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ + QFlightPayloadTab +from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \ + QGeneralFlightSettingsTab +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \ + QFlightWaypointTab class QFlightPlanner(QTabWidget): on_planned_flight_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner, selected_tab): - super(QFlightPlanner, self).__init__() + def __init__(self, flight: Flight, game: Game): + super().__init__() - print(selected_tab) - - self.tabCount = 0 - if flight: - self.general_settings_tab = QGeneralFlightSettingsTab(flight, game, planner) - 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.on_flight_changed.connect(lambda: self.on_planned_flight_changed.emit()) - self.addTab(self.general_settings_tab, "General Flight settings") - self.addTab(self.payload_tab, "Payload") - self.addTab(self.waypoint_tab, "Waypoints") - self.tabCount = 3 - self.setCurrentIndex(selected_tab) - else: - tabError = QFrame() - l = QGridLayout() - l.addWidget(QLabel("No flight selected")) - tabError.setLayout(l) - self.addTab(tabError, "No flight") - self.tabCount = 1 - - def clearTabs(self): - for i in range(self.tabCount): - self.removeTab(i) + 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.on_flight_changed.connect( + lambda: self.on_planned_flight_changed.emit()) + self.addTab(self.general_settings_tab, "General Flight settings") + self.addTab(self.payload_tab, "Payload") + self.addTab(self.waypoint_tab, "Waypoints") + self.setCurrentIndex(0) diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index 36a72dc4..4c8f3bac 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -6,12 +6,14 @@ class QFlightSlotEditor(QGroupBox): changed = Signal() - def __init__(self, flight, game, planner): + def __init__(self, flight, game): super(QFlightSlotEditor, self).__init__("Slots") self.flight = flight self.game = game - self.planner = planner - self.available = self.planner.get_available_aircraft() + inventory = self.game.aircraft_inventory.for_control_point( + flight.from_cp + ) + self.available = inventory.all_aircraft if self.flight.unit_type not in self.available: max = self.flight.count else: diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index cabd99cf..99f2b63f 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -12,18 +12,15 @@ from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTyp class QGeneralFlightSettingsTab(QFrame): on_flight_settings_changed = Signal() - def __init__(self, flight: Flight, game: Game, planner): + def __init__(self, game: Game, flight: Flight): super(QGeneralFlightSettingsTab, self).__init__() self.flight = flight self.game = game - self.planner = planner - self.init_ui() - def init_ui(self): layout = QGridLayout() flight_info = QFlightTypeTaskInfo(self.flight) flight_departure = QFlightDepartureEditor(self.flight) - flight_slots = QFlightSlotEditor(self.flight, self.game, self.planner) + flight_slots = QFlightSlotEditor(self.flight, self.game) flight_start_type = QFlightStartType(self.flight) layout.addWidget(flight_info, 0, 0) layout.addWidget(flight_departure, 1, 0) @@ -35,5 +32,7 @@ class QGeneralFlightSettingsTab(QFrame): self.setLayout(layout) flight_start_type.setEnabled(self.flight.client_count > 0) - flight_slots.changed.connect(lambda: flight_start_type.setEnabled(self.flight.client_count > 0)) - flight_departure.changed.connect(lambda: self.on_flight_settings_changed.emit()) + flight_slots.changed.connect( + lambda: flight_start_type.setEnabled(self.flight.client_count > 0)) + flight_departure.changed.connect( + lambda: self.on_flight_settings_changed.emit()) diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 621f106a..b796ed3e 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -46,7 +46,7 @@ COAST_DR_W = [135, 180, 225, 315] class ConflictTheater: terrain = None # type: dcs.terrain.Terrain - controlpoints = None # type: typing.Collection[ControlPoint] + controlpoints = None # type: typing.List[ControlPoint] reference_points = None # type: typing.Dict overview_image = None # type: str diff --git a/theater/controlpoint.py b/theater/controlpoint.py index d7a726e7..8f0d59c9 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -3,11 +3,17 @@ import typing from enum import Enum from dcs.mapping import * -from dcs.terrain import Airport -from dcs.ships import CVN_74_John_C__Stennis, LHA_1_Tarawa, CV_1143_5_Admiral_Kuznetsov, Type_071_Amphibious_Transport_Dock +from dcs.ships import ( + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + LHA_1_Tarawa, + Type_071_Amphibious_Transport_Dock, +) +from dcs.terrain.terrain import Airport from game import db from gen.ground_forces.combat_stance import CombatStance +from .missiontarget import MissionTarget from .theatergroundobject import TheaterGroundObject @@ -19,7 +25,7 @@ class ControlPointType(Enum): FOB = 5 # A FOB (ground units only) -class ControlPoint: +class ControlPoint(MissionTarget): id = 0 position = None # type: Point @@ -183,4 +189,3 @@ class ControlPoint: if g.obj_name == obj_name: found.append(g) return found - diff --git a/theater/missiontarget.py b/theater/missiontarget.py new file mode 100644 index 00000000..fdb37eec --- /dev/null +++ b/theater/missiontarget.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod + +from dcs.mapping import Point + + +class MissionTarget(ABC): + # TODO: These should just be required objects to the constructor + # The TheatherGroundObject class is difficult to modify because it's + # generated data that's pickled ahead of time. + @property + @abstractmethod + def name(self) -> str: + """The name of the mission target.""" + + @property + @abstractmethod + def position(self) -> Point: + """The position of the mission target.""" diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 42fc69f1..a01ad85f 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,6 +1,11 @@ -from dcs.mapping import Point +from typing import List import uuid +from dcs.mapping import Point + +from .missiontarget import MissionTarget + + NAME_BY_CATEGORY = { "power": "Power plant", "ammo": "Ammo depot", @@ -59,7 +64,7 @@ CATEGORY_MAP = { } -class TheaterGroundObject: +class TheaterGroundObject(MissionTarget): cp_id = 0 group_id = 0 object_id = 0 @@ -93,3 +98,7 @@ class TheaterGroundObject: def matches_string_identifier(self, id): return self.string_identifier == id + + @property + def name(self) -> str: + return self.obj_name From 8a4a81a008292b63f24c4a3ee35ac4501e2ad661 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 24 Sep 2020 18:10:16 -0700 Subject: [PATCH 04/48] Remove unused frontline code. --- qt_ui/widgets/map/QLiberationMap.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 69723619..19b3303a 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -43,8 +43,6 @@ class QLiberationMap(QGraphicsView): QLiberationMap.instance = self self.game_model = game_model - self.frontline_vector_cache = {} - self.setMinimumSize(800,600) self.setMaximumHeight(2160) self._zoom = 0 @@ -255,23 +253,6 @@ class QLiberationMap(QGraphicsView): else: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) - def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint): - # Cache mechanism to avoid performing frontline vector computation on every frame - key = str(from_cp.id) + "_" + str(to_cp.id) - if key in self.frontline_vector_cache: - return self.frontline_vector_cache[key] - else: - frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater) - self.frontline_vector_cache[key] = frontline - return frontline - - def _frontline_center(self, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[Point]: - frontline_vector = self._frontline_vector(from_cp, to_cp) - if frontline_vector: - return frontline_vector[0].point_from_heading(frontline_vector[1], frontline_vector[2]/2) - else: - return None - def wheelEvent(self, event: QWheelEvent): if event.angleDelta().y() > 0: From 0e1dfb8ccbe81819ce8411c6cf11400240598f42 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 24 Sep 2020 19:32:10 -0700 Subject: [PATCH 05/48] 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.""" From a3c06ce6e0d81435e4acd41a2a704a8b9867702e Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 25 Sep 2020 00:25:25 -0700 Subject: [PATCH 06/48] Limit task type combo box to valid mission types. --- qt_ui/widgets/combos/QFlightTypeComboBox.py | 107 ++++++++++++++++-- .../windows/mission/flight/QFlightCreator.py | 5 +- theater/theatergroundobject.py | 10 +- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index 9a04df68..8da41217 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -1,22 +1,105 @@ """Combo box for selecting a flight's task type.""" +import logging +from typing import Iterator + from PySide2.QtWidgets import QComboBox from gen.flights.flight import FlightType +from theater import ( + ConflictTheater, + ControlPoint, + FrontLine, + MissionTarget, + TheaterGroundObject, +) class QFlightTypeComboBox(QComboBox): """Combo box for selecting a flight task type.""" - def __init__(self) -> None: + COMMON_ENEMY_MISSIONS = [ + FlightType.TARCAP, + FlightType.SEAD, + FlightType.DEAD, + # TODO: FlightType.ELINT, + # TODO: FlightType.ESCORT, + # TODO: FlightType.EWAR, + # TODO: FlightType.RECON, + ] + + FRIENDLY_AIRBASE_MISSIONS = [ + FlightType.CAP, + # TODO: FlightType.INTERCEPTION + # TODO: FlightType.LOGISTICS + ] + + FRIENDLY_CARRIER_MISSIONS = [ + FlightType.BARCAP, + # TODO: FlightType.INTERCEPTION + # TODO: Buddy tanking for the A-4? + # TODO: Rescue chopper? + # TODO: Inter-ship logistics? + ] + + ENEMY_CARRIER_MISSIONS = [ + FlightType.TARCAP, + # TODO: FlightType.ANTISHIP + # TODO: FlightType.ESCORT, + ] + + ENEMY_AIRBASE_MISSIONS = [ + # TODO: FlightType.STRIKE + ] + COMMON_ENEMY_MISSIONS + + FRIENDLY_GROUND_OBJECT_MISSIONS = [ + FlightType.CAP, + # TODO: FlightType.LOGISTICS + # TODO: FlightType.TROOP_TRANSPORT + ] + + ENEMY_GROUND_OBJECT_MISSIONS = [ + FlightType.STRIKE, + ] + COMMON_ENEMY_MISSIONS + + FRONT_LINE_MISSIONS = [ + FlightType.CAS, + # TODO: FlightType.TROOP_TRANSPORT + # TODO: FlightType.EVAC + ] + COMMON_ENEMY_MISSIONS + + # TODO: Add BAI missions after we have useful BAI targets. + + def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None: super().__init__() - self.addItem("CAP [Combat Air Patrol]", userData=FlightType.CAP) - self.addItem("BARCAP [Barrier Combat Air Patrol]", userData=FlightType.BARCAP) - self.addItem("TARCAP [Target Combat Air Patrol]", userData=FlightType.TARCAP) - self.addItem("INTERCEPT [Interception]", userData=FlightType.INTERCEPTION) - self.addItem("CAS [Close Air Support]", userData=FlightType.CAS) - self.addItem("BAI [Battlefield Interdiction]", userData=FlightType.BAI) - self.addItem("SEAD [Suppression of Enemy Air Defenses]", userData=FlightType.SEAD) - self.addItem("DEAD [Destruction of Enemy Air Defenses]", userData=FlightType.DEAD) - self.addItem("STRIKE [Strike]", userData=FlightType.STRIKE) - self.addItem("ANTISHIP [Antiship Attack]", userData=FlightType.ANTISHIP) - self.model().sort(0) + self.theater = theater + self.target = target + for mission_type in self.mission_types_for_target(): + self.addItem(mission_type.name, userData=mission_type) + + def mission_types_for_target(self) -> Iterator[FlightType]: + if isinstance(self.target, ControlPoint): + friendly = self.target.captured + fleet = self.target.is_fleet + if friendly: + if fleet: + yield from self.FRIENDLY_CARRIER_MISSIONS + else: + yield from self.FRIENDLY_AIRBASE_MISSIONS + else: + if fleet: + yield from self.ENEMY_CARRIER_MISSIONS + else: + yield from self.ENEMY_AIRBASE_MISSIONS + elif isinstance(self.target, TheaterGroundObject): + # TODO: Filter more based on the category. + friendly = self.target.parent_control_point(self.theater).captured + if friendly: + yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS + else: + yield from self.ENEMY_GROUND_OBJECT_MISSIONS + elif isinstance(self.target, FrontLine): + yield from self.FRONT_LINE_MISSIONS + else: + logging.error( + f"Unhandled target type: {self.target.__class__.__name__}" + ) diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 87693dfc..f1041071 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -36,8 +36,9 @@ class QFlightCreator(QDialog): layout = QVBoxLayout() - # TODO: Limit task selection to those valid for the target type. - self.task_selector = QFlightTypeComboBox() + self.task_selector = QFlightTypeComboBox( + self.game.theater, self.package.target + ) self.task_selector.setCurrentIndex(0) layout.addLayout(QLabeledWidget("Task:", self.task_selector)) diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index a01ad85f..3ffa86f2 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,11 +1,9 @@ -from typing import List import uuid from dcs.mapping import Point from .missiontarget import MissionTarget - NAME_BY_CATEGORY = { "power": "Power plant", "ammo": "Ammo depot", @@ -102,3 +100,11 @@ class TheaterGroundObject(MissionTarget): @property def name(self) -> str: return self.obj_name + + def parent_control_point( + self, theater: "ConflictTheater") -> "ControlPoint": + """Searches the theater for the parent control point.""" + for cp in theater.controlpoints: + if cp.id == self.cp_id: + return cp + raise RuntimeError("Could not find matching control point in theater") From f8ef5db5a31e8c218643c4e3a20cc8119b867059 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 2 Oct 2020 14:29:40 +0200 Subject: [PATCH 07/48] bug when continuing an old campaign save --- game/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/game/game.py b/game/game.py index 385261b8..0d8b9682 100644 --- a/game/game.py +++ b/game/game.py @@ -190,9 +190,9 @@ class Game: def is_player_attack(self, event): if isinstance(event, Event): - return event.attacker_name == self.player_name + return event and event.attacker_name and event.attacker_name == self.player_name else: - return event.name == self.player_name + return event and event.name and event.name == self.player_name def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): From 98b2d8b3b9c28725a3a91676860945ad2c00ec26 Mon Sep 17 00:00:00 2001 From: David Pierron Date: Fri, 2 Oct 2020 14:30:12 +0200 Subject: [PATCH 08/48] typo in the `generate_initial_units` function name --- qt_ui/windows/newgame/QNewGameWizard.py | 2 +- theater/start_generator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 3daace57..cba58371 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -83,7 +83,7 @@ class NewGameWizard(QtWidgets.QWizard): print("Enemy name : " + enemy_name) print("Player name : " + player_name) print("Midgame : " + str(midgame)) - start_generator.generate_inital_units(conflictTheater, enemy_name, True, multiplier) + start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier) print("-- Initial units generated") game = Game(player_name=player_name, diff --git a/theater/start_generator.py b/theater/start_generator.py index b029ae92..d459cbb1 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -27,7 +27,7 @@ COUNT_BY_TASK = { } -def generate_inital_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float): +def generate_initial_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float): for cp in theater.enemy_points(): if cp.captured: continue From 07d4b126f5d59c7b521b69ed1d66222c44103bb3 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 16:18:12 +0200 Subject: [PATCH 09/48] Enable EPLRS for ground units that can use it. --- gen/armor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gen/armor.py b/gen/armor.py index 5042149f..10a54b4b 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -183,6 +183,11 @@ class GroundConflictGenerator: return for dcs_group, group in ally_groups: + + if hasattr(group.unit_type, 'eplrs'): + if group.unit_type.eplrs: + group.points[0].tasks.append(EPLRS(group.id)) + if group.role == CombatGroupRole.ARTILLERY: # Fire on any ennemy in range if self.game.settings.perf_artillery: From 5ba2e8a7a1a7ac81bf8df30089cca3ca1fdee33a Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 16:18:32 +0200 Subject: [PATCH 10/48] Fixed error after merge --- qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 9ce843d7..b679bf7b 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -6,11 +6,14 @@ from PySide2.QtWidgets import ( QGridLayout, QScrollArea, QVBoxLayout, + QHBoxLayout, + QLabel, QWidget, ) from game.event.event import UnitsDeliveryEvent from qt_ui.models import GameModel +from qt_ui.uiconstants import ICONS from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour from theater import CAP, CAS, ControlPoint, db From e72c82521a290b831e3063398ab3f58fcbaa2a79 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 16:45:21 +0200 Subject: [PATCH 11/48] Forgot the changelog for 2.1.4 --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 0f60b72f..59a20d76 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# 2.1.4 + +## Fixes : +* **[UI]** Fixed an issue that prevent generating the mission (take off button no working) on old savegames. + # 2.1.3 ## Features/Improvements : From 7dd3367203b042fdaef639016dc249ac38c42123 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 16:50:56 +0200 Subject: [PATCH 12/48] Version number for release 2.1.4 --- qt_ui/uiconstants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index ada6c94c..30f61ef1 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP from userdata.liberation_theme import get_theme_icons -VERSION_STRING = "2.1.3" +VERSION_STRING = "2.1.4" URLS : Dict[str, str] = { "Manual": "https://github.com/khopa/dcs_liberation/wiki", From db36a76c2cf492db3390f5c425ce850f2d516167 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 17:35:06 +0200 Subject: [PATCH 13/48] Fixed eplrs for frontline ground units --- gen/armor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gen/armor.py b/gen/armor.py index 10a54b4b..68c2aa55 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -184,9 +184,9 @@ class GroundConflictGenerator: for dcs_group, group in ally_groups: - if hasattr(group.unit_type, 'eplrs'): - if group.unit_type.eplrs: - group.points[0].tasks.append(EPLRS(group.id)) + if hasattr(group.units[0], 'eprls'): + if group.units[0].eprls: + dcs_group.points[0].tasks.append(EPLRS(dcs_group.id)) if group.role == CombatGroupRole.ARTILLERY: # Fire on any ennemy in range From 5ecf9aeed8e8650147e72b49389b477372c6cfca Mon Sep 17 00:00:00 2001 From: Khopa Date: Sat, 3 Oct 2020 18:32:11 +0200 Subject: [PATCH 14/48] EPLRS implemented for base defense unit. --- changelog.md | 4 +--- gen/armor.py | 1 + gen/groundobjectsgen.py | 5 +++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 59a20d76..c0ecb509 100644 --- a/changelog.md +++ b/changelog.md @@ -1,9 +1,7 @@ # 2.1.4 ## Fixes : -* **[UI]** Fixed an issue that prevent generating the mission (take off button no working) on old savegames. - -# 2.1.3 +* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames. ## Features/Improvements : * **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions diff --git a/gen/armor.py b/gen/armor.py index 68c2aa55..abd6ccdb 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -184,6 +184,7 @@ class GroundConflictGenerator: for dcs_group, group in ally_groups: + # TODO : REPLACE EPRLS BY EPLRS once fix implemented in pydcs if hasattr(group.units[0], 'eprls'): if group.units[0].eprls: dcs_group.points[0].tasks.append(EPLRS(dcs_group.id)) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index d1cd5378..4db56b80 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -80,6 +80,11 @@ class GroundObjectsGenerator: vehicle.heading = u.heading vehicle.player_can_drive = True vg.add_unit(vehicle) + + #TODO : REPLACE EPRLS BY EPLRS once fix implemented in pydcs + if hasattr(utype, 'eprls'): + if utype.eprls: + vg.points[0].tasks.append(EPLRS(vg.id)) else: vg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading) From 6317f376b796534003dfa60b46ff6c1ac0afc1fa Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 4 Oct 2020 14:11:28 +0200 Subject: [PATCH 15/48] EPLRS typo fixed --- gen/armor.py | 5 ++- gen/groundobjectsgen.py | 5 ++- pydcs | 2 +- pydcs_extensions/frenchpack/frenchpack.py | 42 +++++++++++------------ 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/gen/armor.py b/gen/armor.py index abd6ccdb..463d7571 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -184,9 +184,8 @@ class GroundConflictGenerator: for dcs_group, group in ally_groups: - # TODO : REPLACE EPRLS BY EPLRS once fix implemented in pydcs - if hasattr(group.units[0], 'eprls'): - if group.units[0].eprls: + if hasattr(group.units[0], 'eplrs'): + if group.units[0].eplrs: dcs_group.points[0].tasks.append(EPLRS(dcs_group.id)) if group.role == CombatGroupRole.ARTILLERY: diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 4db56b80..ddf2706e 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -81,9 +81,8 @@ class GroundObjectsGenerator: vehicle.player_can_drive = True vg.add_unit(vehicle) - #TODO : REPLACE EPRLS BY EPLRS once fix implemented in pydcs - if hasattr(utype, 'eprls'): - if utype.eprls: + if hasattr(utype, 'eplrs'): + if utype.eplrs: vg.points[0].tasks.append(EPLRS(vg.id)) else: vg = self.m.ship_group(side, g.name, utype, position=g.position, diff --git a/pydcs b/pydcs index ceea62a8..c203e5a1 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit ceea62a8e0731c21b3e1a3e90682aa0affc168f1 +Subproject commit c203e5a1b8d5eb42d559dab074e668bf37fa5158 diff --git a/pydcs_extensions/frenchpack/frenchpack.py b/pydcs_extensions/frenchpack/frenchpack.py index 12d51827..2d5f5433 100644 --- a/pydcs_extensions/frenchpack/frenchpack.py +++ b/pydcs_extensions/frenchpack/frenchpack.py @@ -25,7 +25,7 @@ class ERC_90(unittype.VehicleType): detection_range = 0 threat_range = 4000 air_weapon_dist = 4000 - eprls = True + eplrs = True class VAB__50(unittype.VehicleType): @@ -34,7 +34,7 @@ class VAB__50(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class VAB_T20_13(unittype.VehicleType): @@ -43,7 +43,7 @@ class VAB_T20_13(unittype.VehicleType): detection_range = 0 threat_range = 2000 air_weapon_dist = 2000 - eprls = True + eplrs = True class VAB_MEPHISTO(unittype.VehicleType): @@ -52,7 +52,7 @@ class VAB_MEPHISTO(unittype.VehicleType): detection_range = 0 threat_range = 4000 air_weapon_dist = 4000 - eprls = True + eplrs = True class VBL__50(unittype.VehicleType): @@ -61,7 +61,7 @@ class VBL__50(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class VBL_AANF1(unittype.VehicleType): @@ -70,7 +70,7 @@ class VBL_AANF1(unittype.VehicleType): detection_range = 0 threat_range = 1000 air_weapon_dist = 1000 - eprls = True + eplrs = True class VBAE_CRAB(unittype.VehicleType): @@ -79,7 +79,7 @@ class VBAE_CRAB(unittype.VehicleType): detection_range = 0 threat_range = 3500 air_weapon_dist = 3500 - eprls = True + eplrs = True class VBAE_CRAB_MMP(unittype.VehicleType): @@ -88,7 +88,7 @@ class VBAE_CRAB_MMP(unittype.VehicleType): detection_range = 0 threat_range = 3500 air_weapon_dist = 3500 - eprls = True + eplrs = True class AMX_30B2(unittype.VehicleType): @@ -121,7 +121,7 @@ class DIM__TOYOTA_BLUE(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__TOYOTA_GREEN(unittype.VehicleType): @@ -130,7 +130,7 @@ class DIM__TOYOTA_GREEN(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__TOYOTA_DESERT(unittype.VehicleType): @@ -139,7 +139,7 @@ class DIM__TOYOTA_DESERT(unittype.VehicleType): detection_range = 0 threat_range = 1200 air_weapon_dist = 1200 - eprls = True + eplrs = True class DIM__KAMIKAZE(unittype.VehicleType): @@ -148,7 +148,7 @@ class DIM__KAMIKAZE(unittype.VehicleType): detection_range = 0 threat_range = 50 air_weapon_dist = 50 - eprls = True + eplrs = True ## FORTIFICATION @@ -187,7 +187,7 @@ class TRM_2000(unittype.VehicleType): detection_range = 3500 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class TRM_2000_Fuel(unittype.VehicleType): id = "TRM2000_Citerne" @@ -195,7 +195,7 @@ class TRM_2000_Fuel(unittype.VehicleType): detection_range = 3500 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VAB_MEDICAL(unittype.VehicleType): id = "VABH" @@ -203,7 +203,7 @@ class VAB_MEDICAL(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VAB(unittype.VehicleType): id = "VAB_RADIO" @@ -211,7 +211,7 @@ class VAB(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class VBL(unittype.VehicleType): id = "VBL-Radio" @@ -219,7 +219,7 @@ class VBL(unittype.VehicleType): detection_range = 0 threat_range = 0 air_weapon_dist = 0 - eprls = True + eplrs = True class Tracma_TD_1500(unittype.VehicleType): id = "Tracma" @@ -236,7 +236,7 @@ class SMOKE_SAM_IR(unittype.VehicleType): detection_range = 20000 threat_range = 20000 air_weapon_dist = 20000 - eprls = True + eplrs = True class _53T2(unittype.VehicleType): id = "AA20" @@ -251,7 +251,7 @@ class TRM_2000_53T2(unittype.VehicleType): detection_range = 6000 threat_range = 2000 air_weapon_dist = 2000 - eprls = True + eplrs = True class TRM_2000_PAMELA(unittype.VehicleType): id = "TRMMISTRAL" @@ -259,7 +259,7 @@ class TRM_2000_PAMELA(unittype.VehicleType): detection_range = 8000 threat_range = 10000 air_weapon_dist = 10000 - eprls = True + eplrs = True ## INFANTRY @@ -285,4 +285,4 @@ class VAB_MORTIER(unittype.VehicleType): detection_range = 0 threat_range = 15000 air_weapon_dist = 15000 - eprls = True \ No newline at end of file + eplrs = True \ No newline at end of file From 1f240b02f4d62f427d06d210f009f89da0801f0e Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 4 Oct 2020 14:27:13 +0200 Subject: [PATCH 16/48] Fix aircrafts landing point --- gen/aircraft.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gen/aircraft.py b/gen/aircraft.py index ec5dec26..fb485e0a 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -952,6 +952,7 @@ class AircraftConflictGenerator: # pt.tasks.append(engagetgt) elif point.waypoint_type == FlightWaypointType.LANDING_POINT: pt.type = "Land" + pt.action = PointAction.Landing elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE: if group.units[0].unit_type == B_17G: From 1e041b6249aed8fc7d02013c2469acf43137c9ea Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 26 Sep 2020 13:17:34 -0700 Subject: [PATCH 17/48] Perform coalition-wide mission planning. Mission planning on a per-control point basis lacked the context it needed to make good decisions, and the ability to make larger missions that pulled aircraft from multiple airfields. The per-CP planners have been replaced in favor of a global planner per coalition. The planner generates a list of potential missions in order of priority and then allocates aircraft to the proposed flights until no missions remain. Mission planning behavior has changed: * CAP flights will now only be generated for airfields within a predefined threat range of an enemy airfield. * CAS, SEAD, and strike missions get escorts. Strike missions get a SEAD flight. * CAS, SEAD, and strike missions will not be planned unless they have an escort available. * Missions may originate from multiple airfields. There's more to do: * The range limitations imposed on the mission planner should take aircraft range limitations into account. * Air superiority aircraft like the F-15 should be preferred for CAP over multi-role aircraft like the F/A-18 since otherwise we run the risk of running out of ground attack capable aircraft even though there are still unused aircraft. * Mission priorities may need tuning. * Target areas could be analyzed for potential threats, allowing escort flights to be optional or omitted if there is no threat to defend against. For example, late game a SEAD flight for a strike mission probably is not necessary. * SAM threat should be judged by how close the extent of the SAM's range is to friendly locations, not the distance to the site itself. An SA-10 30 nm away is more threatening than an SA-6 25 nm away. * Much of the planning behavior should be factored out into the coalition's doctrine. But as-is this is an improvement over the existing behavior, so those things can be follow ups. The potential regression in behavior here is that we're no longer planning multiple cycles of missions. Each objective will get one CAP. I think this fits better with the turn cycle of the game, as a CAP flight should be able to remain on station for the duration of the turn (especially with refueling). Note that this does break save compatibility as the old planner was a part of the game object, and since that class is now gone it can't be unpickled. --- game/db.py | 2 +- game/game.py | 16 +- game/operation/operation.py | 15 +- gen/aircraft.py | 42 +- gen/flights/ai_flight_planner.py | 1074 +++++++---------- gen/flights/flightplan.py | 582 +++++++++ qt_ui/windows/mission/QChooseAirbase.py | 32 - .../windows/mission/flight/QFlightCreator.py | 80 +- .../generator/QAbstractMissionGenerator.py | 3 +- .../flight/waypoints/QFlightWaypointTab.py | 3 +- theater/controlpoint.py | 5 +- theater/frontline.py | 18 + theater/missiontarget.py | 12 + 13 files changed, 1070 insertions(+), 814 deletions(-) create mode 100644 gen/flights/flightplan.py delete mode 100644 qt_ui/windows/mission/QChooseAirbase.py diff --git a/game/db.py b/game/db.py index 003bbcc4..45ddfdf7 100644 --- a/game/db.py +++ b/game/db.py @@ -807,7 +807,7 @@ CARRIER_TAKEOFF_BAN = [ Units separated by country. country : DCS Country name """ -FACTIONS = { +FACTIONS: typing.Dict[str, typing.Dict[str, typing.Any]] = { "Bluefor Modern": BLUEFOR_MODERN, "Bluefor Cold War 1970s": BLUEFOR_COLDWAR, diff --git a/game/game.py b/game/game.py index 5fa8c050..0226de89 100644 --- a/game/game.py +++ b/game/game.py @@ -4,11 +4,12 @@ from game.db import REWARDS, PLAYER_BUDGET_BASE, sys from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats from gen.ato import AirTaskingOrder -from gen.flights.ai_flight_planner import FlightPlanner +from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner from .event import * from .settings import Settings + COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 COMMISION_LIMITS_FACTORS = { @@ -70,7 +71,6 @@ class Game: self.date = datetime(start_date.year, start_date.month, start_date.day) self.game_stats = GameStats() self.game_stats.update(self) - self.planners = {} self.ground_planners = {} self.informations = [] self.informations.append(Information("Game Start", "-" * 40, 0)) @@ -104,11 +104,11 @@ class Game: self.enemy_country = "Russia" @property - def player_faction(self): + def player_faction(self) -> Dict[str, Any]: return db.FACTIONS[self.player_name] @property - def enemy_faction(self): + def enemy_faction(self) -> Dict[str, Any]: return db.FACTIONS[self.enemy_name] def _roll(self, prob, mult): @@ -244,16 +244,12 @@ class Game: # Plan flights & combat for next turn self.__culling_points = self.compute_conflicts_position() - self.planners = {} self.ground_planners = {} self.blue_ato.clear() self.red_ato.clear() + CoalitionMissionPlanner(self, is_player=True).plan_missions() + CoalitionMissionPlanner(self, is_player=False).plan_missions() for cp in self.theater.controlpoints: - if cp.has_runway(): - planner = FlightPlanner(cp, self) - planner.plan_flights() - self.planners[cp.id] = planner - if cp.has_frontline: gplanner = GroundPlanner(cp, self) gplanner.plan_groundwar() diff --git a/game/operation/operation.py b/game/operation/operation.py index c0ce5e67..c86efacb 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -189,15 +189,16 @@ class Operation: side = cp.captured if side: country = self.current_mission.country(self.game.player_country) + ato = self.game.blue_ato else: country = self.current_mission.country(self.game.enemy_country) - if cp.id in self.game.planners.keys(): - self.airgen.generate_flights( - cp, - country, - self.game.planners[cp.id], - self.groundobjectgen.runways - ) + ato = self.game.red_ato + self.airgen.generate_flights( + cp, + country, + ato, + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] diff --git a/gen/aircraft.py b/gen/aircraft.py index fb485e0a..04301de7 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -14,8 +14,8 @@ from game.settings import Settings from game.utils import nm_to_meter from gen.airfields import RunwayData from gen.airsupportgen import AirSupport +from gen.ato import AirTaskingOrder from gen.callsigns import create_group_callsign_from_unit -from gen.flights.ai_flight_planner import FlightPlanner from gen.flights.flight import ( Flight, FlightType, @@ -751,31 +751,27 @@ class AircraftConflictGenerator: else: logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type)) - - def generate_flights(self, cp, country, flight_planner: FlightPlanner, - dynamic_runways: Dict[str, RunwayData]): - # Clear pydcs parking slots - if cp.airport is not None: - logging.info("CLEARING SLOTS @ " + cp.airport.name) - logging.info("===============") + def clear_parking_slots(self) -> None: + for cp in self.game.theater.controlpoints: if cp.airport is not None: - for ps in cp.airport.parking_slots: - logging.info("SLOT : " + str(ps.unit_id)) - ps.unit_id = None - logging.info("----------------") - logging.info("===============") + for parking_slot in cp.airport.parking_slots: + parking_slot.unit_id = None - for flight in flight_planner.flights: - - if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position): - logging.info("Flight not generated : culled") - continue - logging.info("Generating flight : " + str(flight.unit_type)) - group = self.generate_planned_flight(cp, country, flight) - self.setup_flight_group(group, flight, flight.flight_type, - dynamic_runways) - self.setup_group_activation_trigger(flight, group) + def generate_flights(self, cp, country, ato: AirTaskingOrder, + dynamic_runways: Dict[str, RunwayData]) -> None: + self.clear_parking_slots() + for package in ato.packages: + for flight in package.flights: + culled = self.game.position_culled(flight.from_cp.position) + if flight.client_count == 0 and culled: + logging.info("Flight not generated: culled") + continue + logging.info(f"Generating flight: {flight.unit_type}") + group = self.generate_planned_flight(cp, country, flight) + self.setup_flight_group(group, flight, flight.flight_type, + dynamic_runways) + self.setup_group_activation_trigger(flight, group) def setup_group_activation_trigger(self, flight, group): if flight.scheduled_in > 0 and flight.client_count == 0: diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 29deb1f4..6e97e91d 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -1,705 +1,455 @@ -import math -import operator -import random -from typing import Iterable, Iterator, List, Tuple +from __future__ import annotations -from dcs.unittype import FlyingType +import logging +import operator +from dataclasses import dataclass +from typing import Dict, Iterator, List, Optional, Set, TYPE_CHECKING, Tuple + +from dcs.unittype import UnitType from game import db -from game.data.doctrine import MODERN_DOCTRINE from game.data.radar_db import UNITS_WITH_RADAR +from game.infos.information import Information from game.utils import nm_to_meter from gen import Conflict from gen.ato import Package from gen.flights.ai_flight_planner_db import ( CAP_CAPABLE, CAS_CAPABLE, - DRONES, SEAD_CAPABLE, STRIKE_CAPABLE, ) from gen.flights.flight import ( Flight, FlightType, - FlightWaypoint, - FlightWaypointType, ) -from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from gen.flights.flightplan import FlightPlanBuilder +from theater import ( + ControlPoint, + FrontLine, + MissionTarget, + TheaterGroundObject, +) -MISSION_DURATION = 80 +# Avoid importing some types that cause circular imports unless type checking. +if TYPE_CHECKING: + from game import Game + from game.inventory import GlobalAircraftInventory -# TODO: Should not be per-control point. -# Packages can frag flights from individual airfields, so we should be planning -# coalition wide rather than per airfield. -class FlightPlanner: +class ClosestAirfields: + """Precalculates which control points are closes to the given target.""" - def __init__(self, from_cp: ControlPoint, game: "Game") -> None: - # TODO : have the flight planner depend on a 'stance' setting : [Defensive, Aggresive... etc] and faction doctrine - # TODO : the flight planner should plan package and operations - self.from_cp = from_cp - self.game = game - self.flights: List[Flight] = [] - self.potential_sead_targets: List[Tuple[TheaterGroundObject, int]] = [] - self.potential_strike_targets: List[Tuple[TheaterGroundObject, int]] = [] - - if from_cp.captured: - self.faction = self.game.player_faction - else: - self.faction = self.game.enemy_faction - - if "doctrine" in self.faction.keys(): - self.doctrine = self.faction["doctrine"] - else: - self.doctrine = MODERN_DOCTRINE - - @property - def aircraft_inventory(self) -> "GlobalAircraftInventory": - return self.game.aircraft_inventory - - def reset(self) -> None: - """Reset the planned flights and available units.""" - self.flights = [] - self.potential_sead_targets = [] - self.potential_strike_targets = [] - - def plan_flights(self) -> None: - self.reset() - self.compute_sead_targets() - self.compute_strike_targets() - - self.commission_cap() - self.commission_cas() - self.commission_sead() - self.commission_strike() - # TODO: Commission anti-ship and intercept. - - def plan_legacy_mission(self, flight: Flight, - location: MissionTarget) -> None: - package = Package(location) - package.add_flight(flight) - if flight.from_cp.captured: - self.game.blue_ato.add_package(package) - else: - self.game.red_ato.add_package(package) - self.flights.append(flight) - self.aircraft_inventory.claim_for_flight(flight) - - def get_compatible_aircraft(self, candidates: Iterable[FlyingType], - minimum: int) -> List[FlyingType]: - inventory = self.aircraft_inventory.for_control_point(self.from_cp) - return [k for k, v in inventory.all_aircraft if - k in candidates and v >= minimum] - - def alloc_aircraft( - self, num_flights: int, flight_size: int, - allowed_types: Iterable[FlyingType]) -> Iterator[FlyingType]: - aircraft = self.get_compatible_aircraft(allowed_types, flight_size) - if not aircraft: - return - - for _ in range(num_flights): - yield random.choice(aircraft) - aircraft = self.get_compatible_aircraft(allowed_types, flight_size) - if not aircraft: - return - - def commission_cap(self) -> None: - """Pick some aircraft to assign them to defensive CAP roles (BARCAP).""" - offset = random.randint(0, 5) - num_caps = MISSION_DURATION // self.doctrine["CAP_EVERY_X_MINUTES"] - for i, aircraft in enumerate(self.alloc_aircraft(num_caps, 2, CAP_CAPABLE)): - flight = Flight(aircraft, 2, self.from_cp, FlightType.CAP) - - flight.scheduled_in = offset + i * random.randint( - self.doctrine["CAP_EVERY_X_MINUTES"] - 5, - self.doctrine["CAP_EVERY_X_MINUTES"] + 5 - ) - - if len(self._get_cas_locations()) > 0: - 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) - - self.plan_legacy_mission(flight, location) - - def commission_cas(self) -> None: - """Pick some aircraft to assign them to CAS.""" - cas_locations = self._get_cas_locations() - if not cas_locations: - return - - offset = random.randint(0,5) - num_cas = MISSION_DURATION // self.doctrine["CAS_EVERY_X_MINUTES"] - for i, aircraft in enumerate(self.alloc_aircraft(num_cas, 2, CAS_CAPABLE)): - flight = Flight(aircraft, 2, self.from_cp, FlightType.CAS) - flight.scheduled_in = offset + i * random.randint( - self.doctrine["CAS_EVERY_X_MINUTES"] - 5, - self.doctrine["CAS_EVERY_X_MINUTES"] + 5) - location = random.choice(cas_locations) - - self.generate_cas(flight, location) - self.plan_legacy_mission(flight, location) - - def commission_sead(self) -> None: - """Pick some aircraft to assign them to SEAD tasks.""" - - if not self.potential_sead_targets: - return - - offset = random.randint(0, 5) - num_sead = max( - MISSION_DURATION // self.doctrine["SEAD_EVERY_X_MINUTES"], - len(self.potential_sead_targets)) - for i, aircraft in enumerate(self.alloc_aircraft(num_sead, 2, SEAD_CAPABLE)): - flight = Flight(aircraft, 2, self.from_cp, - random.choice([FlightType.SEAD, FlightType.DEAD])) - flight.scheduled_in = offset + i * random.randint( - self.doctrine["SEAD_EVERY_X_MINUTES"] - 5, - self.doctrine["SEAD_EVERY_X_MINUTES"] + 5) - - location = self.potential_sead_targets[0][0] - self.potential_sead_targets.pop() - - self.generate_sead(flight, location, []) - self.plan_legacy_mission(flight, location) - - def commission_strike(self) -> None: - """Pick some aircraft to assign them to STRIKE tasks.""" - if not self.potential_strike_targets: - return - - offset = random.randint(0,5) - num_strike = max( - MISSION_DURATION / self.doctrine["STRIKE_EVERY_X_MINUTES"], - len(self.potential_strike_targets) + def __init__(self, target: MissionTarget, + all_control_points: List[ControlPoint]) -> None: + self.target = target + self.closest_airfields: List[ControlPoint] = sorted( + all_control_points, key=lambda c: self.target.distance_to(c) ) - for i, aircraft in enumerate(self.alloc_aircraft(num_strike, 2, STRIKE_CAPABLE)): - if aircraft in DRONES: - count = 1 + + def airfields_within(self, meters: int) -> Iterator[ControlPoint]: + """Iterates over all airfields within the given range of the target. + + Note that this iterates over *all* airfields, not just friendly + airfields. + """ + for cp in self.closest_airfields: + if cp.distance_to(self.target) < meters: + yield cp else: - count = 2 + break - flight = Flight(aircraft, count, self.from_cp, FlightType.STRIKE) - flight.scheduled_in = offset + i * random.randint( - self.doctrine["STRIKE_EVERY_X_MINUTES"] - 5, - self.doctrine["STRIKE_EVERY_X_MINUTES"] + 5) - location = self.potential_strike_targets[0][0] - self.potential_strike_targets.pop(0) +@dataclass(frozen=True) +class ProposedFlight: + """A flight outline proposed by the mission planner. - self.generate_strike(flight, location) - self.plan_legacy_mission(flight, location) + Proposed flights haven't been assigned specific aircraft yet. They have only + a task, a required number of aircraft, and a maximum distance allowed + between the objective and the departure airfield. + """ - def _get_cas_locations(self) -> List[FrontLine]: - return self._get_cas_locations_for_cp(self.from_cp) + #: The flight's role. + task: FlightType + + #: The number of aircraft required. + num_aircraft: int + + #: The maximum distance between the objective and the departure airfield. + max_distance: int + + def __str__(self) -> str: + return f"{self.task.name} {self.num_aircraft} ship" + + +@dataclass(frozen=True) +class ProposedMission: + """A mission outline proposed by the mission planner. + + Proposed missions haven't been assigned aircraft yet. They have only an + objective location and a list of proposed flights that are required for the + mission. + """ + + #: The mission objective. + location: MissionTarget + + #: The proposed flights that are required for the mission. + flights: List[ProposedFlight] + + def __str__(self) -> str: + flights = ', '.join([str(f) for f in self.flights]) + return f"{self.location.name}: {flights}" + + +class AircraftAllocator: + """Finds suitable aircraft for proposed missions.""" + + def __init__(self, closest_airfields: ClosestAirfields, + global_inventory: GlobalAircraftInventory, + is_player: bool) -> None: + self.closest_airfields = closest_airfields + self.global_inventory = global_inventory + self.is_player = is_player + + def find_aircraft_for_flight( + self, flight: ProposedFlight + ) -> Optional[Tuple[ControlPoint, UnitType]]: + """Finds aircraft suitable for the given mission. + + Searches for aircraft capable of performing the given mission within the + maximum allowed range. If insufficient aircraft are available for the + mission, None is returned. + + Note that aircraft *will* be removed from the global inventory on + success. This is to ensure that the same aircraft are not matched twice + on subsequent calls. If the found aircraft are not used, the caller is + responsible for returning them to the inventory. + """ + cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) + if flight.task in cap_missions: + types = CAP_CAPABLE + elif flight.task == FlightType.CAS: + types = CAS_CAPABLE + elif flight.task in (FlightType.DEAD, FlightType.SEAD): + types = SEAD_CAPABLE + elif flight.task == FlightType.STRIKE: + types = STRIKE_CAPABLE + else: + logging.error(f"Unplannable flight type: {flight.task}") + return None + + # TODO: Implement mission type weighting for aircraft. + # We should avoid assigning F/A-18s to CAP missions when there are F-15s + # available, since the F/A-18 is capable of performing other tasks that + # the F-15 is not capable of. + airfields_in_range = self.closest_airfields.airfields_within( + flight.max_distance + ) + for airfield in airfields_in_range: + if not airfield.is_friendly(self.is_player): + continue + inventory = self.global_inventory.for_control_point(airfield) + for aircraft, available in inventory.all_aircraft: + if aircraft in types and available >= flight.num_aircraft: + inventory.remove_aircraft(aircraft, flight.num_aircraft) + return airfield, aircraft + + return None + + +class PackageBuilder: + """Builds a Package for the flights it receives.""" + + def __init__(self, location: MissionTarget, + closest_airfields: ClosestAirfields, + global_inventory: GlobalAircraftInventory, + is_player: bool) -> None: + self.package = Package(location) + self.allocator = AircraftAllocator(closest_airfields, global_inventory, + is_player) + self.global_inventory = global_inventory + + def plan_flight(self, plan: ProposedFlight) -> bool: + """Allocates aircraft for the given flight and adds them to the package. + + If no suitable aircraft are available, False is returned. If the failed + flight was critical and the rest of the mission will be scrubbed, the + caller should return any previously planned flights to the inventory + using release_planned_aircraft. + """ + assignment = self.allocator.find_aircraft_for_flight(plan) + if assignment is None: + return False + airfield, aircraft = assignment + flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) + self.package.add_flight(flight) + return True + + def build(self) -> Package: + """Returns the built package.""" + return self.package + + def release_planned_aircraft(self) -> None: + """Returns any planned flights to the inventory.""" + flights = list(self.package.flights) + for flight in flights: + self.global_inventory.return_from_flight(flight) + self.package.remove_flight(flight) + + +class ObjectiveFinder: + """Identifies potential objectives for the mission planner.""" + + # TODO: Merge into doctrine. + AIRFIELD_THREAT_RANGE = nm_to_meter(150) + SAM_THREAT_RANGE = nm_to_meter(100) + + def __init__(self, game: Game, is_player: bool) -> None: + self.game = game + self.is_player = is_player + # TODO: Cache globally at startup to avoid generating twice per turn? + self.closest_airfields: Dict[str, ClosestAirfields] = { + t.name: ClosestAirfields(t, self.game.theater.controlpoints) + for t in self.all_possible_targets() + } + + def enemy_sams(self) -> Iterator[TheaterGroundObject]: + """Iterates over all enemy SAM sites.""" + # Control points might have the same ground object several times, for + # some reason. + found_targets: Set[str] = set() + for cp in self.enemy_control_points(): + for ground_object in cp.ground_objects: + if ground_object.name in found_targets: + continue + + if ground_object.dcs_identifier != "AA": + continue + + if not self.object_has_radar(ground_object): + continue + + # TODO: Yield in order of most threatening. + # Need to sort in order of how close their defensive range comes + # to friendly assets. To do that we need to add effective range + # information to the database. + yield ground_object + found_targets.add(ground_object.name) + + def threatening_sams(self) -> Iterator[TheaterGroundObject]: + """Iterates over enemy SAMs in threat range of friendly control points. + + SAM sites are sorted by their closest proximity to any friendly control + point (airfield or fleet). + """ + sams: List[Tuple[TheaterGroundObject, int]] = [] + for sam in self.enemy_sams(): + ranges: List[int] = [] + for cp in self.friendly_control_points(): + ranges.append(sam.distance_to(cp)) + sams.append((sam, min(ranges))) + + sams = sorted(sams, key=operator.itemgetter(1)) + for sam, _range in sams: + yield sam + + def strike_targets(self) -> Iterator[TheaterGroundObject]: + """Iterates over enemy strike targets. + + Targets are sorted by their closest proximity to any friendly control + point (airfield or fleet). + """ + targets: List[Tuple[TheaterGroundObject, int]] = [] + # Control points might have the same ground object several times, for + # some reason. + found_targets: Set[str] = set() + for enemy_cp in self.enemy_control_points(): + for ground_object in enemy_cp.ground_objects: + if ground_object.name in found_targets: + continue + ranges: List[int] = [] + for friendly_cp in self.friendly_control_points(): + ranges.append(ground_object.distance_to(friendly_cp)) + targets.append((ground_object, min(ranges))) + found_targets.add(ground_object.name) + targets = sorted(targets, key=operator.itemgetter(1)) + for target, _range in targets: + yield target @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(FrontLine(for_cp, cp)) - return cas_locations + def object_has_radar(ground_object: TheaterGroundObject) -> bool: + """Returns True if the ground object contains a unit with radar.""" + for group in ground_object.groups: + for unit in group.units: + if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR: + return True + return False - def compute_strike_targets(self): + def front_lines(self) -> Iterator[FrontLine]: + """Iterates over all active front lines in the theater.""" + for cp in self.friendly_control_points(): + for connected in cp.connected_points: + if connected.is_friendly(self.is_player): + continue + + if Conflict.has_frontline_between(cp, connected): + yield FrontLine(cp, connected) + + def vulnerable_control_points(self) -> Iterator[ControlPoint]: + """Iterates over friendly CPs that are vulnerable to enemy CPs. + + Vulnerability is defined as any enemy CP within threat range of of the + CP. """ - @return a list of potential strike targets in range - """ - - # target, distance - self.potential_strike_targets = [] - - for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance > 2*self.doctrine["STRIKE_MAX_RANGE"]: - # Then it's unlikely any child ground object is in range - return - - added_group = [] - for g in cp.ground_objects: - if g.group_id in added_group or g.is_dead: continue - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance < self.doctrine["SEAD_MAX_RANGE"]: - self.potential_strike_targets.append((g, distance)) - added_group.append(g) - - self.potential_strike_targets.sort(key=operator.itemgetter(1)) - - def compute_sead_targets(self): - """ - @return a list of potential sead targets in range - """ - - # target, distance - self.potential_sead_targets = [] - - for cp in [c for c in self.game.theater.controlpoints if c.captured != self.from_cp.captured]: - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - # Then it's unlikely any ground object is range - if distance > 2*self.doctrine["SEAD_MAX_RANGE"]: - return - - for g in cp.ground_objects: - - if g.dcs_identifier == "AA": - - # Check that there is at least one unit with a radar in the ground objects unit groups - number_of_units = sum([len([r for r in group.units if db.unit_type_from_name(r.type) in UNITS_WITH_RADAR]) for group in g.groups]) - if number_of_units <= 0: - continue - - # Compute distance to current cp - distance = math.hypot(cp.position.x - self.from_cp.position.x, - cp.position.y - self.from_cp.position.y) - - if distance < self.doctrine["SEAD_MAX_RANGE"]: - self.potential_sead_targets.append((g, distance)) - - self.potential_sead_targets.sort(key=operator.itemgetter(1)) - - def __repr__(self): - return "-"*40 + "\n" + self.from_cp.name + " planned flights :\n"\ - + "-"*40 + "\n" + "\n".join([repr(f) for f in self.flights]) + "\n" + "-"*40 - - def generate_strike(self, flight: Flight, location: TheaterGroundObject): - flight.flight_type = FlightType.STRIKE - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - heading = flight.from_cp.position.heading_between_point(location.position) - ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 - - ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_STRIKE, - ingress_pos.x, - ingress_pos.y, - self.doctrine["INGRESS_ALT"] - ) - ingress_point.pretty_name = "INGRESS on " + location.obj_name - ingress_point.description = "INGRESS on " + location.obj_name - ingress_point.name = "INGRESS" - flight.points.append(ingress_point) - - if len(location.groups) > 0 and location.dcs_identifier == "AA": - for g in location.groups: - for j, u in enumerate(g.units): - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - u.position.x, - u.position.y, - 0 - ) - point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) - point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j) - point.name = location.obj_name + "#" + str(j) - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) - else: - if hasattr(location, "obj_name"): - buildings = self.game.theater.find_ground_objects_by_obj_name(location.obj_name) - print(buildings) - for building in buildings: - print("BUILDING " + str(building.is_dead) + " " + str(building.dcs_identifier)) - if building.is_dead: - continue - - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - building.position.x, - building.position.y, - 0 - ) - point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" - point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]" - point.name = building.obj_name - point.only_for_player = True - ingress_point.targets.append(building) - flight.points.append(point) - else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 - ) - point.description = "STRIKE on " + location.obj_name - point.pretty_name = "STRIKE on " + location.obj_name - point.name = location.obj_name - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) - - egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine["EGRESS_ALT"] - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.obj_name - egress_point.description = "EGRESS from " + location.obj_name - flight.points.append(egress_point) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - def generate_barcap(self, flight, for_cp): - """ - Generate a barcap flight at a given location - :param flight: Flight to setup - :param for_cp: CP to protect - """ - flight.flight_type = FlightType.BARCAP if for_cp.is_carrier else FlightType.CAP - patrol_alt = random.randint(self.doctrine["PATROL_ALT_RANGE"][0], self.doctrine["PATROL_ALT_RANGE"][1]) - - if len(for_cp.ground_objects) > 0: - loc = random.choice(for_cp.ground_objects) - hdg = for_cp.position.heading_between_point(loc.position) - radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1]) - orbit0p = loc.position.point_from_heading(hdg - 90, radius) - orbit1p = loc.position.point_from_heading(hdg + 90, radius) - else: - loc = for_cp.position.point_from_heading(random.randint(0, 360), random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0], self.doctrine["CAP_DISTANCE_FROM_CP"][1])) - hdg = for_cp.position.heading_between_point(loc) - radius = random.randint(self.doctrine["CAP_PATTERN_LENGTH"][0], self.doctrine["CAP_PATTERN_LENGTH"][1]) - orbit0p = loc.point_from_heading(hdg - 90, radius) - orbit1p = loc.point_from_heading(hdg + 90, radius) - - # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - orbit0.targets.append(for_cp) - obj_added = [] - for ground_object in for_cp.ground_objects: - if ground_object.obj_name not in obj_added and not ground_object.airbase_group: - orbit0.targets.append(ground_object) - obj_added.append(ground_object.obj_name) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - 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. - - :param flight: Flight to setup - :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]) - - # Find targets waypoints - ingress, heading, distance = Conflict.frontline_vector(ally_cp, enemy_cp, self.game.theater) - center = ingress.point_from_heading(heading, distance / 2) - orbit_center = center.point_from_heading(heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))) - - combat_width = distance / 2 - if combat_width > 500000: - combat_width = 500000 - if combat_width < 35000: - combat_width = 35000 - - radius = combat_width*1.25 - orbit0p = orbit_center.point_from_heading(heading, radius) - orbit1p = orbit_center.point_from_heading(heading + 180, radius) - - # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - # Note : Targets of a PATROL TRACK waypoints are the points to be defended - orbit0.targets.append(flight.from_cp) - orbit0.targets.append(center) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - - def generate_sead(self, flight, location, custom_targets = []): - """ - Generate a sead flight at a given location - :param flight: Flight to setup - :param location: Location of the SEAD target - :param custom_targets: Custom targets if any - """ - flight.points = [] - flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) - - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - heading = flight.from_cp.position.heading_between_point(location.position) - ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 - - ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_SEAD, - ingress_pos.x, - ingress_pos.y, - self.doctrine["INGRESS_ALT"] - ) - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS on " + location.obj_name - ingress_point.description = "INGRESS on " + location.obj_name - flight.points.append(ingress_point) - - if len(custom_targets) > 0: - for target in custom_targets: - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - target.position.x, - target.position.y, - 0 - ) - point.alt_type = "RADIO" - if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + target.type - point.pretty_name = "DEAD on " + location.obj_name - point.only_for_player = True - else: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - flight.points.append(point) - ingress_point.targets.append(location) - ingress_point.targetGroup = location - else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 + for cp in self.friendly_control_points(): + airfields_in_proximity = self.closest_airfields[cp.name] + airfields_in_threat_range = airfields_in_proximity.airfields_within( + self.AIRFIELD_THREAT_RANGE ) - point.alt_type = "RADIO" - if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + location.obj_name - point.pretty_name = "DEAD on " + location.obj_name - point.only_for_player = True - else: - point.description = "SEAD on " + location.obj_name - point.pretty_name = "SEAD on " + location.obj_name - point.only_for_player = True - ingress_point.targets.append(location) - ingress_point.targetGroup = location - flight.points.append(point) + for airfield in airfields_in_threat_range: + if not airfield.is_friendly(self.is_player): + yield cp + break - egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"]) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine["EGRESS_ALT"] - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.obj_name - egress_point.description = "EGRESS from " + location.obj_name - flight.points.append(egress_point) + def friendly_control_points(self) -> Iterator[ControlPoint]: + """Iterates over all friendly control points.""" + return (c for c in self.game.theater.controlpoints if + c.is_friendly(self.is_player)) - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) + def enemy_control_points(self) -> Iterator[ControlPoint]: + """Iterates over all enemy control points.""" + return (c for c in self.game.theater.controlpoints if + not c.is_friendly(self.is_player)) - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + def all_possible_targets(self) -> Iterator[MissionTarget]: + """Iterates over all possible mission targets in the theater. - def generate_cas(self, flight: Flight, front_line: FrontLine) -> None: - """Generate a CAS flight plan for the given target. - - :param flight: Flight to setup - :param front_line: Front line containing CAS targets. + Valid mission targets are control points (airfields and carriers), front + lines, and ground objects (SAM sites, factories, resource extraction + sites, etc). """ - 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 + for cp in self.game.theater.controlpoints: + yield cp + yield from cp.ground_objects + yield from self.front_lines() - ingress, heading, distance = Conflict.frontline_vector( - from_cp, location, self.game.theater + def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields: + """Returns the closest airfields to the given location.""" + return self.closest_airfields[location.name] + + +class CoalitionMissionPlanner: + """Coalition flight planning AI. + + This class is responsible for automatically planning missions for the + coalition at the start of the turn. + + The primary goal of the mission planner is to protect existing friendly + assets. Missions will be planned with the following priorities: + + 1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy + losses of friendly aircraft. + 2. CAP for front line areas to protect ground and CAS units. + 3. DEAD to reduce necessity of SEAD for future missions. + 4. CAS to protect friendly ground units. + 5. Strike missions to reduce the enemy's resources. + + TODO: Anti-ship and airfield strikes to reduce enemy sortie rates. + TODO: BAI to prevent enemy forces from reaching the front line. + TODO: Should fleets always have a CAP? + + TODO: Stance and doctrine-specific planning behavior. + """ + + # TODO: Merge into doctrine, also limit by aircraft. + MAX_CAP_RANGE = nm_to_meter(100) + MAX_CAS_RANGE = nm_to_meter(50) + MAX_SEAD_RANGE = nm_to_meter(150) + MAX_STRIKE_RANGE = nm_to_meter(150) + + def __init__(self, game: Game, is_player: bool) -> None: + self.game = game + self.is_player = is_player + self.objective_finder = ObjectiveFinder(self.game, self.is_player) + self.ato = self.game.blue_ato if is_player else self.game.red_ato + + def propose_missions(self) -> Iterator[ProposedMission]: + """Identifies and iterates over potential mission in priority order.""" + # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. + for cp in self.objective_finder.vulnerable_control_points(): + yield ProposedMission(cp, [ + ProposedFlight(FlightType.CAP, 2, self.MAX_CAP_RANGE), + ]) + + # Find front lines, plan CAP. + for front_line in self.objective_finder.front_lines(): + yield ProposedMission(front_line, [ + ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE), + ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), + ]) + + # Find enemy SAM sites with ranges that cover friendly CPs, front lines, + # or objects, plan DEAD. + # Find enemy SAM sites with ranges that extend to within 50 nmi of + # friendly CPs, front, lines, or objects, plan DEAD. + for sam in self.objective_finder.threatening_sams(): + yield ProposedMission(sam, [ + ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), + # TODO: Max escort range. + ProposedFlight(FlightType.CAP, 2, self.MAX_SEAD_RANGE), + ]) + + # Plan strike missions. + for target in self.objective_finder.strike_targets(): + yield ProposedMission(target, [ + ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), + # TODO: Max escort range. + ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE), + ProposedFlight(FlightType.CAP, 2, self.MAX_STRIKE_RANGE), + ]) + + def plan_missions(self) -> None: + """Identifies and plans mission for the turn.""" + for proposed_mission in self.propose_missions(): + self.plan_mission(proposed_mission) + + for cp in self.objective_finder.friendly_control_points(): + inventory = self.game.aircraft_inventory.for_control_point(cp) + for aircraft, available in inventory.all_aircraft: + self.message("Unused aircraft", + f"{available} {aircraft.id} from {cp}") + + def plan_mission(self, mission: ProposedMission) -> None: + """Allocates aircraft for a proposed mission and adds it to the ATO.""" + builder = PackageBuilder( + mission.location, + self.objective_finder.closest_airfields_to(mission.location), + self.game.aircraft_inventory, + self.is_player ) - center = ingress.point_from_heading(heading, distance / 2) - egress = ingress.point_from_heading(heading, distance) + for flight in mission.flights: + if not builder.plan_flight(flight): + builder.release_planned_aircraft() + self.message("Insufficient aircraft", + f"Not enough aircraft in range for {mission}") + return - ascend = self.generate_ascend_point(flight.from_cp) - if is_helo: - cap_alt = 500 - ascend.alt = 500 - flight.points.append(ascend) + package = builder.build() + for flight in package.flights: + builder = FlightPlanBuilder(self.game, self.is_player) + builder.populate_flight_plan(flight, package.target) + self.ato.add_package(package) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_CAS, - ingress.x, - ingress.y, - cap_alt - ) - ingress_point.alt_type = "RADIO" - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS" - ingress_point.description = "Ingress into CAS area" - flight.points.append(ingress_point) + def message(self, title, text) -> None: + """Emits a planning message to the player. - center_point = FlightWaypoint( - FlightWaypointType.CAS, - center.x, - center.y, - cap_alt - ) - center_point.alt_type = "RADIO" - center_point.description = "Provide CAS" - center_point.name = "CAS" - center_point.pretty_name = "CAS" - flight.points.append(center_point) - - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress.x, - egress.y, - cap_alt - ) - egress_point.alt_type = "RADIO" - egress_point.description = "Egress from CAS area" - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS" - flight.points.append(egress_point) - - descend = self.generate_descend_point(flight.from_cp) - if is_helo: - descend.alt = 300 - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) - - def generate_ascend_point(self, from_cp): + If the mission planner belongs to the players coalition, this emits a + message to the info panel. """ - Generate ascend point - :param from_cp: Airport you're taking off from - :return: - """ - ascend_heading = from_cp.heading - pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000) - ascend = FlightWaypoint( - FlightWaypointType.ASCEND_POINT, - pos_ascend.x, - pos_ascend.y, - self.doctrine["PATTERN_ALTITUDE"] - ) - ascend.name = "ASCEND" - ascend.alt_type = "RADIO" - ascend.description = "Ascend" - ascend.pretty_name = "Ascend" - return ascend - - def generate_descend_point(self, from_cp): - """ - Generate approach/descend point - :param from_cp: Airport you're landing at - :return: - """ - ascend_heading = from_cp.heading - descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000) - descend = FlightWaypoint( - FlightWaypointType.DESCENT_POINT, - descend.x, - descend.y, - self.doctrine["PATTERN_ALTITUDE"] - ) - descend.name = "DESCEND" - descend.alt_type = "RADIO" - descend.description = "Descend to pattern alt" - descend.pretty_name = "Descend to pattern alt" - return descend - - def generate_rtb_waypoint(self, from_cp): - """ - Generate RTB landing point - :param from_cp: Airport you're landing at - :return: - """ - rtb = from_cp.position - rtb = FlightWaypoint( - FlightWaypointType.LANDING_POINT, - rtb.x, - rtb.y, - 0 - ) - rtb.name = "LANDING" - rtb.alt_type = "RADIO" - rtb.description = "RTB" - rtb.pretty_name = "RTB" - return rtb + if self.is_player: + self.game.informations.append( + Information(title, text, self.game.turn) + ) + else: + logging.info(f"{title}: {text}") diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py new file mode 100644 index 00000000..c8bd988a --- /dev/null +++ b/gen/flights/flightplan.py @@ -0,0 +1,582 @@ +"""Flight plan generation. + +Flights are first planned generically by either the player or by the +MissionPlanner. Those only plan basic information like the objective, aircraft +type, and the size of the flight. The FlightPlanBuilder is responsible for +generating the waypoints for the mission. +""" +from __future__ import annotations + +import logging +import random +from typing import List, Optional, TYPE_CHECKING + +from game.data.doctrine import MODERN_DOCTRINE +from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint +from ..conflictgen import Conflict +from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from game.utils import nm_to_meter +from dcs.unit import Unit + +if TYPE_CHECKING: + from game import Game + + +class InvalidObjectiveLocation(RuntimeError): + """Raised when the objective location is invalid for the mission type.""" + def __init__(self, task: FlightType, location: MissionTarget) -> None: + super().__init__( + f"{location.name} is not valid for {task.name} missions." + ) + + +class FlightPlanBuilder: + """Generates flight plans for flights.""" + + def __init__(self, game: Game, is_player: bool) -> None: + self.game = game + if is_player: + faction = self.game.player_faction + else: + faction = self.game.enemy_faction + self.doctrine = faction.get("doctrine", MODERN_DOCTRINE) + + def populate_flight_plan(self, flight: Flight, + objective_location: MissionTarget) -> None: + """Creates a default flight plan for the given mission.""" + # TODO: Flesh out mission types. + try: + task = flight.flight_type + if task == FlightType.ANTISHIP: + logging.error( + "Anti-ship flight plan generation not implemented" + ) + elif task == FlightType.BAI: + logging.error("BAI flight plan generation not implemented") + elif task == FlightType.BARCAP: + self.generate_barcap(flight, objective_location) + elif task == FlightType.CAP: + self.generate_barcap(flight, objective_location) + elif task == FlightType.CAS: + self.generate_cas(flight, objective_location) + elif task == FlightType.DEAD: + self.generate_sead(flight, objective_location) + elif task == FlightType.ELINT: + logging.error("ELINT flight plan generation not implemented") + elif task == FlightType.EVAC: + logging.error("Evac flight plan generation not implemented") + elif task == FlightType.EWAR: + logging.error("EWar flight plan generation not implemented") + elif task == FlightType.INTERCEPTION: + logging.error( + "Intercept flight plan generation not implemented" + ) + elif task == FlightType.LOGISTICS: + logging.error( + "Logistics flight plan generation not implemented" + ) + elif task == FlightType.RECON: + logging.error("Recon flight plan generation not implemented") + elif task == FlightType.SEAD: + self.generate_sead(flight, objective_location) + elif task == FlightType.STRIKE: + self.generate_strike(flight, objective_location) + elif task == FlightType.TARCAP: + self.generate_frontline_cap(flight, objective_location) + elif task == FlightType.TROOP_TRANSPORT: + logging.error( + "Troop transport flight plan generation not implemented" + ) + except InvalidObjectiveLocation as ex: + logging.error(f"Could not create flight plan: {ex}") + + def generate_strike(self, flight: Flight, location: MissionTarget) -> None: + """Generates a strike flight plan. + + Args: + flight: The flight to generate the flight plan for. + location: The strike target location. + """ + # 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 + ascend = self.generate_ascend_point(flight.from_cp) + flight.points.append(ascend) + + heading = flight.from_cp.position.heading_between_point( + location.position + ) + ingress_heading = heading - 180 + 25 + egress_heading = heading - 180 - 25 + + ingress_pos = location.position.point_from_heading( + ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_STRIKE, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) + ingress_point.pretty_name = "INGRESS on " + location.name + ingress_point.description = "INGRESS on " + location.name + ingress_point.name = "INGRESS" + flight.points.append(ingress_point) + + if len(location.groups) > 0 and location.dcs_identifier == "AA": + for g in location.groups: + for j, u in enumerate(g.units): + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + u.position.x, + u.position.y, + 0 + ) + point.description = ( + f"STRIKE [{location.name}] : {u.type} #{j}" + ) + point.pretty_name = ( + f"STRIKE [{location.name}] : {u.type} #{j}" + ) + point.name = f"{location.name} #{j}" + point.only_for_player = True + ingress_point.targets.append(location) + flight.points.append(point) + else: + if hasattr(location, "obj_name"): + buildings = self.game.theater.find_ground_objects_by_obj_name( + location.obj_name + ) + for building in buildings: + if building.is_dead: + continue + + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + building.position.x, + building.position.y, + 0 + ) + point.description = ( + f"STRIKE on {building.obj_name} {building.category} " + f"[{building.dcs_identifier}]" + ) + point.pretty_name = ( + f"STRIKE on {building.obj_name} {building.category} " + f"[{building.dcs_identifier}]" + ) + point.name = building.obj_name + point.only_for_player = True + ingress_point.targets.append(building) + flight.points.append(point) + else: + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) + point.description = "STRIKE on " + location.name + point.pretty_name = "STRIKE on " + location.name + point.name = location.name + point.only_for_player = True + ingress_point.targets.append(location) + flight.points.append(point) + + egress_pos = location.position.point_from_heading( + egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) + egress_point.name = "EGRESS" + egress_point.pretty_name = "EGRESS from " + location.name + egress_point.description = "EGRESS from " + location.name + flight.points.append(egress_point) + + descend = self.generate_descend_point(flight.from_cp) + flight.points.append(descend) + + rtb = self.generate_rtb_waypoint(flight.from_cp) + flight.points.append(rtb) + + def generate_barcap(self, flight: Flight, location: MissionTarget) -> 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. + """ + 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["PATROL_ALT_RANGE"][0], + self.doctrine["PATROL_ALT_RANGE"][1] + ) + + loc = location.position.point_from_heading( + random.randint(0, 360), + random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0], + self.doctrine["CAP_DISTANCE_FROM_CP"][1]) + ) + hdg = location.position.heading_between_point(loc) + radius = random.randint( + self.doctrine["CAP_PATTERN_LENGTH"][0], + self.doctrine["CAP_PATTERN_LENGTH"][1] + ) + orbit0p = loc.point_from_heading(hdg - 90, radius) + orbit1p = loc.point_from_heading(hdg + 90, radius) + + # Create points + ascend = self.generate_ascend_point(flight.from_cp) + flight.points.append(ascend) + + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) + orbit0.name = "ORBIT 0" + orbit0.description = "Standby between this point and the next one" + orbit0.pretty_name = "Race-track start" + flight.points.append(orbit0) + + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) + orbit1.name = "ORBIT 1" + orbit1.description = "Standby between this point and the previous one" + orbit1.pretty_name = "Race-track end" + flight.points.append(orbit1) + + orbit0.targets.append(location) + + descend = self.generate_descend_point(flight.from_cp) + flight.points.append(descend) + + rtb = self.generate_rtb_waypoint(flight.from_cp) + flight.points.append(rtb) + + def generate_frontline_cap(self, flight: Flight, + location: MissionTarget) -> 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. + """ + 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["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 + ) + center = ingress.point_from_heading(heading, distance / 2) + orbit_center = center.point_from_heading( + heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15)) + ) + + combat_width = distance / 2 + if combat_width > 500000: + combat_width = 500000 + if combat_width < 35000: + combat_width = 35000 + + radius = combat_width*1.25 + orbit0p = orbit_center.point_from_heading(heading, radius) + orbit1p = orbit_center.point_from_heading(heading + 180, radius) + + # Create points + ascend = self.generate_ascend_point(flight.from_cp) + flight.points.append(ascend) + + orbit0 = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + orbit0p.x, + orbit0p.y, + patrol_alt + ) + orbit0.name = "ORBIT 0" + orbit0.description = "Standby between this point and the next one" + orbit0.pretty_name = "Race-track start" + flight.points.append(orbit0) + + orbit1 = FlightWaypoint( + FlightWaypointType.PATROL, + orbit1p.x, + orbit1p.y, + patrol_alt + ) + orbit1.name = "ORBIT 1" + orbit1.description = "Standby between this point and the previous one" + orbit1.pretty_name = "Race-track end" + flight.points.append(orbit1) + + # Note: Targets of PATROL TRACK waypoints are the points to be defended. + orbit0.targets.append(flight.from_cp) + orbit0.targets.append(center) + + descend = self.generate_descend_point(flight.from_cp) + flight.points.append(descend) + + rtb = self.generate_rtb_waypoint(flight.from_cp) + flight.points.append(rtb) + + def generate_sead(self, flight: Flight, location: MissionTarget, + custom_targets: Optional[List[Unit]] = None) -> 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. + """ + if not isinstance(location, TheaterGroundObject): + raise InvalidObjectiveLocation(flight.flight_type, location) + + if custom_targets is None: + custom_targets = [] + + flight.points = [] + flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) + + ascend = self.generate_ascend_point(flight.from_cp) + flight.points.append(ascend) + + heading = flight.from_cp.position.heading_between_point( + location.position + ) + ingress_heading = heading - 180 + 25 + egress_heading = heading - 180 - 25 + + ingress_pos = location.position.point_from_heading( + ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ) + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_SEAD, + ingress_pos.x, + ingress_pos.y, + self.doctrine["INGRESS_ALT"] + ) + ingress_point.name = "INGRESS" + ingress_point.pretty_name = "INGRESS on " + location.name + ingress_point.description = "INGRESS on " + location.name + flight.points.append(ingress_point) + + if len(custom_targets) > 0: + for target in custom_targets: + point = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) + point.alt_type = "RADIO" + if flight.flight_type == FlightType.DEAD: + point.description = "DEAD on " + target.type + point.pretty_name = "DEAD on " + location.name + point.only_for_player = True + else: + point.description = "SEAD on " + location.name + point.pretty_name = "SEAD on " + location.name + point.only_for_player = True + flight.points.append(point) + ingress_point.targets.append(location) + ingress_point.targetGroup = location + else: + point = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) + point.alt_type = "RADIO" + if flight.flight_type == FlightType.DEAD: + point.description = "DEAD on " + location.name + point.pretty_name = "DEAD on " + location.name + point.only_for_player = True + else: + point.description = "SEAD on " + location.name + point.pretty_name = "SEAD on " + location.name + point.only_for_player = True + ingress_point.targets.append(location) + ingress_point.targetGroup = location + flight.points.append(point) + + egress_pos = location.position.point_from_heading( + egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ) + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress_pos.x, + egress_pos.y, + self.doctrine["EGRESS_ALT"] + ) + egress_point.name = "EGRESS" + egress_point.pretty_name = "EGRESS from " + location.name + egress_point.description = "EGRESS from " + location.name + flight.points.append(egress_point) + + descend = self.generate_descend_point(flight.from_cp) + flight.points.append(descend) + + rtb = self.generate_rtb_waypoint(flight.from_cp) + flight.points.append(rtb) + + def generate_cas(self, flight: Flight, location: MissionTarget) -> 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. + """ + if not isinstance(location, FrontLine): + raise InvalidObjectiveLocation(flight.flight_type, location) + + from_cp, location = location.control_points + is_helo = getattr(flight.unit_type, "helicopter", False) + cap_alt = 1000 + flight.points = [] + flight.flight_type = FlightType.CAS + + 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) + + ascend = self.generate_ascend_point(flight.from_cp) + if is_helo: + cap_alt = 500 + ascend.alt = 500 + flight.points.append(ascend) + + ingress_point = FlightWaypoint( + FlightWaypointType.INGRESS_CAS, + ingress.x, + ingress.y, + cap_alt + ) + ingress_point.alt_type = "RADIO" + ingress_point.name = "INGRESS" + ingress_point.pretty_name = "INGRESS" + ingress_point.description = "Ingress into CAS area" + flight.points.append(ingress_point) + + center_point = FlightWaypoint( + FlightWaypointType.CAS, + center.x, + center.y, + cap_alt + ) + center_point.alt_type = "RADIO" + center_point.description = "Provide CAS" + center_point.name = "CAS" + center_point.pretty_name = "CAS" + flight.points.append(center_point) + + egress_point = FlightWaypoint( + FlightWaypointType.EGRESS, + egress.x, + egress.y, + cap_alt + ) + egress_point.alt_type = "RADIO" + egress_point.description = "Egress from CAS area" + egress_point.name = "EGRESS" + egress_point.pretty_name = "EGRESS" + flight.points.append(egress_point) + + descend = self.generate_descend_point(flight.from_cp) + if is_helo: + descend.alt = 300 + flight.points.append(descend) + + rtb = self.generate_rtb_waypoint(flight.from_cp) + flight.points.append(rtb) + + def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint: + """Generate ascend point. + + Args: + departure: Departure airfield or carrier. + """ + ascend_heading = departure.heading + pos_ascend = departure.position.point_from_heading( + ascend_heading, 10000 + ) + ascend = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + pos_ascend.x, + pos_ascend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) + ascend.name = "ASCEND" + ascend.alt_type = "RADIO" + ascend.description = "Ascend" + ascend.pretty_name = "Ascend" + return ascend + + def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: + """Generate approach/descend point. + + Args: + arrival: Arrival airfield or carrier. + """ + ascend_heading = arrival.heading + descend = arrival.position.point_from_heading( + ascend_heading - 180, 10000 + ) + descend = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + descend.x, + descend.y, + self.doctrine["PATTERN_ALTITUDE"] + ) + descend.name = "DESCEND" + descend.alt_type = "RADIO" + descend.description = "Descend to pattern alt" + descend.pretty_name = "Descend to pattern alt" + return descend + + @staticmethod + def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint: + """Generate RTB landing point. + + Args: + arrival: Arrival airfield or carrier. + """ + rtb = arrival.position + rtb = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + rtb.x, + rtb.y, + 0 + ) + rtb.name = "LANDING" + rtb.alt_type = "RADIO" + rtb.description = "RTB" + rtb.pretty_name = "RTB" + return rtb diff --git a/qt_ui/windows/mission/QChooseAirbase.py b/qt_ui/windows/mission/QChooseAirbase.py deleted file mode 100644 index 50a86538..00000000 --- a/qt_ui/windows/mission/QChooseAirbase.py +++ /dev/null @@ -1,32 +0,0 @@ -from PySide2.QtCore import Signal -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel - -from game import Game - - -class QChooseAirbase(QGroupBox): - - selected_airbase_changed = Signal(str) - - def __init__(self, game:Game, title=""): - super(QChooseAirbase, self).__init__(title) - self.game = game - - self.layout = QHBoxLayout() - self.depart_from_label = QLabel("Airbase : ") - self.depart_from = QComboBox() - - for i, cp in enumerate([b for b in self.game.theater.controlpoints if b.captured and b.id in self.game.planners]): - self.depart_from.addItem(str(cp.name), cp) - self.depart_from.setCurrentIndex(0) - self.depart_from.currentTextChanged.connect(self._on_airbase_selected) - self.layout.addWidget(self.depart_from_label) - self.layout.addWidget(self.depart_from) - self.setLayout(self.layout) - - def _on_airbase_selected(self): - selected = self.depart_from.currentText() - self.selected_airbase_changed.emit(selected) - - - diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index f1041071..24fb684f 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -11,7 +11,7 @@ from dcs.planes import PlaneType from game import Game from gen.ato import Package -from gen.flights.ai_flight_planner import FlightPlanner +from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flight import Flight, FlightType from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner @@ -31,6 +31,8 @@ class QFlightCreator(QDialog): self.game = game self.package = package + self.planner = FlightPlanBuilder(self.game, is_player=True) + self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) @@ -93,7 +95,7 @@ class QFlightCreator(QDialog): size = self.flight_size_spinner.value() flight = Flight(aircraft, size, origin, task) - self.populate_flight_plan(flight, task) + self.planner.populate_flight_plan(flight, self.package.target) # noinspection PyUnresolvedReferences self.created.emit(flight) @@ -102,77 +104,3 @@ class QFlightCreator(QDialog): def on_aircraft_changed(self, index: int) -> None: new_aircraft = self.aircraft_selector.itemData(index) self.airfield_selector.change_aircraft(new_aircraft) - - @property - def planner(self) -> FlightPlanner: - return self.game.planners[self.airfield_selector.currentData().id] - - def populate_flight_plan(self, flight: Flight, task: FlightType) -> None: - # TODO: Flesh out mission types. - if task == FlightType.ANTISHIP: - logging.error("Anti-ship flight plan generation not implemented") - elif task == FlightType.BAI: - logging.error("BAI flight plan generation not implemented") - elif task == FlightType.BARCAP: - self.generate_cap(flight) - elif task == FlightType.CAP: - self.generate_cap(flight) - elif task == FlightType.CAS: - self.generate_cas(flight) - elif task == FlightType.DEAD: - self.generate_sead(flight) - elif task == FlightType.ELINT: - logging.error("ELINT flight plan generation not implemented") - elif task == FlightType.EVAC: - logging.error("Evac flight plan generation not implemented") - elif task == FlightType.EWAR: - logging.error("EWar flight plan generation not implemented") - elif task == FlightType.INTERCEPTION: - logging.error("Intercept flight plan generation not implemented") - elif task == FlightType.LOGISTICS: - logging.error("Logistics flight plan generation not implemented") - elif task == FlightType.RECON: - logging.error("Recon flight plan generation not implemented") - elif task == FlightType.SEAD: - self.generate_sead(flight) - elif task == FlightType.STRIKE: - self.generate_strike(flight) - elif task == FlightType.TARCAP: - self.generate_cap(flight) - elif task == FlightType.TROOP_TRANSPORT: - logging.error( - "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 isinstance(self.package.target, TheaterGroundObject): - logging.error( - "Could not create flight plan: CAP missions for strike targets " - "not implemented" - ) - return - 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) - - def generate_strike(self, flight: Flight) -> None: - if not isinstance(self.package.target, TheaterGroundObject): - logging.error( - "Could not create flight plan: strike missions for capture " - "points not implemented" - ) - return - self.planner.generate_strike(flight, self.package.target) diff --git a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py index 8a69d4cd..c18729c6 100644 --- a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py +++ b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py @@ -3,6 +3,7 @@ from PySide2.QtWidgets import QDialog, QPushButton from game import Game from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox @@ -19,7 +20,7 @@ class QAbstractMissionGenerator(QDialog): self.setWindowTitle(title) self.setWindowIcon(EVENT_ICONS["strike"]) self.flight_waypoint_list = flight_waypoint_list - self.planner = self.game.planners[self.flight.from_cp.id] + self.planner = FlightPlanBuilder(self.game, is_player=True) self.selected_waypoints = [] self.wpt_info = QFlightWaypointInfoBox() diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 69870d1c..9db48a09 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -3,6 +3,7 @@ from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLay from game import Game from gen.flights.flight import Flight +from gen.flights.flightplan import FlightPlanBuilder from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator @@ -19,7 +20,7 @@ class QFlightWaypointTab(QFrame): super(QFlightWaypointTab, self).__init__() self.flight = flight self.game = game - self.planner = self.game.planners[self.flight.from_cp.id] + self.planner = FlightPlanBuilder(self.game, is_player=True) self.init_ui() def init_ui(self): diff --git a/theater/controlpoint.py b/theater/controlpoint.py index a35cd698..fa211f47 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -52,7 +52,7 @@ class ControlPoint(MissionTarget): self.id = id self.name = " ".join(re.split(r" |-", name)[:2]) self.full_name = name - self.position = position + self.position: Point = position self.at = at self.ground_objects = [] self.ships = [] @@ -212,3 +212,6 @@ class ControlPoint(MissionTarget): if g.obj_name == obj_name: found.append(g) return found + + def is_friendly(self, to_player: bool) -> bool: + return self.captured == to_player diff --git a/theater/frontline.py b/theater/frontline.py index 6350e1ab..c71ec4e3 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -1,9 +1,14 @@ """Battlefield front lines.""" from typing import Tuple +from dcs.mapping import Point from . import ControlPoint, MissionTarget +# TODO: Dedup by moving everything to using this class. +FRONTLINE_MIN_CP_DISTANCE = 5000 + + class FrontLine(MissionTarget): """Defines a front line location between two control points. @@ -25,3 +30,16 @@ class FrontLine(MissionTarget): a = self.control_point_a.name b = self.control_point_b.name return f"Front line {a}/{b}" + + @property + def position(self) -> Point: + a = self.control_point_a.position + b = self.control_point_b.position + attack_heading = a.heading_between_point(b) + attack_distance = a.distance_to_point(b) + middle_point = a.point_from_heading(attack_heading, attack_distance / 2) + + strength_delta = (self.control_point_a.base.strength - self.control_point_b.base.strength) / 1.0 + position = middle_point.point_from_heading(attack_heading, + strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE) + return position diff --git a/theater/missiontarget.py b/theater/missiontarget.py index 41c90ef9..b0a30aa0 100644 --- a/theater/missiontarget.py +++ b/theater/missiontarget.py @@ -1,4 +1,7 @@ +from __future__ import annotations + from abc import ABC, abstractmethod +from dcs.mapping import Point class MissionTarget(ABC): @@ -9,3 +12,12 @@ class MissionTarget(ABC): @abstractmethod def name(self) -> str: """The name of the mission target.""" + + @property + @abstractmethod + def position(self) -> Point: + """The location of the mission target.""" + + def distance_to(self, other: MissionTarget) -> int: + """Computes the distance to the given mission target.""" + return self.position.distance_to_point(other.position) From aa309af0151dca72efd56ace1040286a461a589b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 27 Sep 2020 18:35:16 -0700 Subject: [PATCH 18/48] Redraw flight plans when they change. --- qt_ui/widgets/ato.py | 3 ++ qt_ui/widgets/map/QLiberationMap.py | 36 ++++++++++++++++------ qt_ui/windows/GameUpdateSignal.py | 14 ++++++++- qt_ui/windows/mission/QEditFlightDialog.py | 6 ++++ qt_ui/windows/mission/QPackageDialog.py | 7 +++++ 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/qt_ui/widgets/ato.py b/qt_ui/widgets/ato.py index bee61a43..ae9a5db3 100644 --- a/qt_ui/widgets/ato.py +++ b/qt_ui/widgets/ato.py @@ -16,6 +16,7 @@ from PySide2.QtWidgets import ( from gen.ato import Package from gen.flights.flight import Flight from ..models import AtoModel, GameModel, NullListModel, PackageModel +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal class QFlightList(QListView): @@ -134,6 +135,7 @@ class QFlightPanel(QGroupBox): self.game_model.game.aircraft_inventory.return_from_flight( self.flight_list.selected_item) self.package_model.delete_flight_at_index(index) + GameUpdateSignal.get_instance().redraw_flight_paths() class QPackageList(QListView): @@ -217,6 +219,7 @@ class QPackagePanel(QGroupBox): logging.error(f"Cannot delete package when no package is selected.") return self.ato_model.delete_package_at_index(index) + GameUpdateSignal.get_instance().redraw_flight_paths() class QAirTaskingOrderPanel(QSplitter): diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index a36382b5..3f5b026a 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,10 +1,10 @@ -import typing -from typing import Dict, Tuple +from typing import Dict, List, Tuple from PySide2.QtCore import Qt from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent from PySide2.QtWidgets import ( QFrame, + QGraphicsItem, QGraphicsOpacityEffect, QGraphicsScene, QGraphicsView, @@ -44,6 +44,8 @@ class QLiberationMap(QGraphicsView): QLiberationMap.instance = self self.game_model = game_model + self.flight_path_items: List[QGraphicsItem] = [] + self.setMinimumSize(800,600) self.setMaximumHeight(2160) self._zoom = 0 @@ -53,6 +55,10 @@ class QLiberationMap(QGraphicsView): self.connectSignals() self.setGame(game_model.game) + GameUpdateSignal.get_instance().flight_paths_changed.connect( + lambda: self.draw_flight_plans(self.scene()) + ) + def init_scene(self): scene = QLiberationScene(self) self.setScene(scene) @@ -176,8 +182,7 @@ class QLiberationMap(QGraphicsView): if self.get_display_rule("lines"): self.scene_create_lines_for_cp(cp, playerColor, enemyColor) - if self.get_display_rule("flight_paths"): - self.draw_flight_plans(scene) + self.draw_flight_plans(scene) for cp in self.game.theater.controlpoints: pos = self._transform_point(cp.position) @@ -188,6 +193,15 @@ class QLiberationMap(QGraphicsView): text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1) def draw_flight_plans(self, scene) -> None: + for item in self.flight_path_items: + try: + scene.removeItem(item) + except RuntimeError: + # Something may have caused those items to already be removed. + pass + self.flight_path_items.clear() + if not self.get_display_rule("flight_paths"): + return for package in self.game_model.ato_model.packages: for flight in package.flights: self.draw_flight_plan(scene, flight) @@ -209,17 +223,21 @@ class QLiberationMap(QGraphicsView): player: bool) -> None: waypoint_pen = self.waypoint_pen(player) waypoint_brush = self.waypoint_brush(player) - scene.addEllipse(position[0], position[1], self.WAYPOINT_SIZE, - self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush) + self.flight_path_items.append(scene.addEllipse( + position[0], position[1], self.WAYPOINT_SIZE, + self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush + )) def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], pos1: Tuple[int, int], player: bool): flight_path_pen = self.flight_path_pen(player) # Draw the line to the *middle* of the waypoint. offset = self.WAYPOINT_SIZE // 2 - scene.addLine(pos0[0] + offset, pos0[1] + offset, - pos1[0] + offset, pos1[1] + offset, - flight_path_pen) + self.flight_path_items.append(scene.addLine( + pos0[0] + offset, pos0[1] + offset, + pos1[0] + offset, pos1[1] + offset, + flight_path_pen + )) def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor): scene = self.scene() diff --git a/qt_ui/windows/GameUpdateSignal.py b/qt_ui/windows/GameUpdateSignal.py index dd32dd58..3e855149 100644 --- a/qt_ui/windows/GameUpdateSignal.py +++ b/qt_ui/windows/GameUpdateSignal.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from PySide2.QtCore import QObject, Signal from game import Game @@ -19,21 +21,31 @@ class GameUpdateSignal(QObject): budgetupdated = Signal(Game) debriefingReceived = Signal(DebriefingSignal) + flight_paths_changed = Signal() + def __init__(self): super(GameUpdateSignal, self).__init__() GameUpdateSignal.instance = self + def redraw_flight_paths(self) -> None: + # noinspection PyUnresolvedReferences + self.flight_paths_changed.emit() + def updateGame(self, game: Game): + # noinspection PyUnresolvedReferences self.gameupdated.emit(game) def updateBudget(self, game: Game): + # noinspection PyUnresolvedReferences self.budgetupdated.emit(game) def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing): sig = DebriefingSignal(game, gameEvent, debriefing) + # noinspection PyUnresolvedReferences self.gameupdated.emit(game) + # noinspection PyUnresolvedReferences self.debriefingReceived.emit(sig) @staticmethod - def get_instance(): + def get_instance() -> GameUpdateSignal: return GameUpdateSignal.instance diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py index 29e4eafb..24fdfae2 100644 --- a/qt_ui/windows/mission/QEditFlightDialog.py +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -7,6 +7,7 @@ from PySide2.QtWidgets import ( from game import Game from gen.flights.flight import Flight from qt_ui.uiconstants import EVENT_ICONS +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner @@ -27,3 +28,8 @@ class QEditFlightDialog(QDialog): layout.addWidget(self.flight_planner) self.setLayout(layout) + self.finished.connect(self.on_close) + + @staticmethod + def on_close(_result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 2b27a035..37b73d04 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -17,6 +17,7 @@ from gen.flights.flight import Flight from qt_ui.models import AtoModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.ato import QFlightList +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator from theater.missiontarget import MissionTarget @@ -86,6 +87,12 @@ class QPackageDialog(QDialog): self.setLayout(self.layout) + self.finished.connect(self.on_close) + + @staticmethod + def on_close(_result) -> None: + GameUpdateSignal.get_instance().redraw_flight_paths() + def on_selection_changed(self, selected: QItemSelection, _deselected: QItemSelection) -> None: """Updates the state of the delete button.""" From 8b717c4f4c2ed29281f637b5ae9c406701c6d25d Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 27 Sep 2020 19:55:40 -0700 Subject: [PATCH 19/48] Replace doctrine dict with a real type. --- game/data/doctrine.py | 163 ++++++++++++++++++-------------------- gen/flights/flightplan.py | 40 +++++----- 2 files changed, 98 insertions(+), 105 deletions(-) diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 866ae897..e7333096 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -1,95 +1,88 @@ +from dataclasses import dataclass + from game.utils import nm_to_meter, feet_to_meter -MODERN_DOCTRINE = { - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": True, - "STRIKE": True, - "ANTISHIP": True, - }, +@dataclass(frozen=True) +class Doctrine: + cas: bool + cap: bool + sead: bool + strike: bool + antiship: bool - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, + strike_max_range: int + sead_max_range: int - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, + ingress_egress_distance: int + ingress_altitude: int + egress_altitude: int + min_patrol_altitude: int + max_patrol_altitude: int + pattern_altitude: int - "INGRESS_EGRESS_DISTANCE": nm_to_meter(45), - "INGRESS_ALT": feet_to_meter(20000), - "EGRESS_ALT": feet_to_meter(20000), - "PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), + cap_min_track_length: int + cap_max_track_length: int + cap_min_distance_from_cp: int + cap_max_distance_from_cp: int - "CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)), - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, -} +MODERN_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=True, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + ingress_egress_distance=nm_to_meter(45), + ingress_altitude=feet_to_meter(20000), + egress_altitude=feet_to_meter(20000), + min_patrol_altitude=feet_to_meter(15000), + max_patrol_altitude=feet_to_meter(33000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(15), + cap_max_track_length=nm_to_meter(40), + cap_min_distance_from_cp=nm_to_meter(10), + cap_max_distance_from_cp=nm_to_meter(40), +) -COLDWAR_DOCTRINE = { +COLDWAR_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=True, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + ingress_egress_distance=nm_to_meter(30), + ingress_altitude=feet_to_meter(18000), + egress_altitude=feet_to_meter(18000), + min_patrol_altitude=feet_to_meter(10000), + max_patrol_altitude=feet_to_meter(24000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(12), + cap_max_track_length=nm_to_meter(24), + cap_min_distance_from_cp=nm_to_meter(8), + cap_max_distance_from_cp=nm_to_meter(25), +) - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": True, - "STRIKE": True, - "ANTISHIP": True, - }, - - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, - - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, - - "INGRESS_EGRESS_DISTANCE": nm_to_meter(30), - "INGRESS_ALT": feet_to_meter(18000), - "EGRESS_ALT": feet_to_meter(18000), - "PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), - - "CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)), - - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, -} - -WWII_DOCTRINE = { - - "GENERATORS": { - "CAS": True, - "CAP": True, - "SEAD": False, - "STRIKE": True, - "ANTISHIP": True, - }, - - "STRIKE_MAX_RANGE": 1500000, - "SEAD_MAX_RANGE": 1500000, - - "CAP_EVERY_X_MINUTES": 20, - "CAS_EVERY_X_MINUTES": 30, - "SEAD_EVERY_X_MINUTES": 40, - "STRIKE_EVERY_X_MINUTES": 40, - - "INGRESS_EGRESS_DISTANCE": nm_to_meter(7), - "INGRESS_ALT": feet_to_meter(8000), - "EGRESS_ALT": feet_to_meter(8000), - "PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)), - "PATTERN_ALTITUDE": feet_to_meter(5000), - - "CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)), - "FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)), - "CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)), - - "MAX_NUMBER_OF_INTERCEPTION_GROUP": 3, - -} +WWII_DOCTRINE = Doctrine( + cap=True, + cas=True, + sead=False, + strike=True, + antiship=True, + strike_max_range=1500000, + sead_max_range=1500000, + ingress_egress_distance=nm_to_meter(7), + ingress_altitude=feet_to_meter(8000), + egress_altitude=feet_to_meter(8000), + min_patrol_altitude=feet_to_meter(4000), + max_patrol_altitude=feet_to_meter(15000), + pattern_altitude=feet_to_meter(5000), + cap_min_track_length=nm_to_meter(8), + cap_max_track_length=nm_to_meter(18), + cap_min_distance_from_cp=nm_to_meter(0), + cap_max_distance_from_cp=nm_to_meter(5), +) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index c8bd988a..0c631b31 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -11,7 +11,7 @@ import logging import random from typing import List, Optional, TYPE_CHECKING -from game.data.doctrine import MODERN_DOCTRINE +from game.data.doctrine import Doctrine, MODERN_DOCTRINE from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint from ..conflictgen import Conflict from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject @@ -39,7 +39,7 @@ class FlightPlanBuilder: faction = self.game.player_faction else: faction = self.game.enemy_faction - self.doctrine = faction.get("doctrine", MODERN_DOCTRINE) + self.doctrine: Doctrine = faction.get("doctrine", MODERN_DOCTRINE) def populate_flight_plan(self, flight: Flight, objective_location: MissionTarget) -> None: @@ -113,13 +113,13 @@ class FlightPlanBuilder: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading( - ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ingress_heading, self.doctrine.ingress_egress_distance ) ingress_point = FlightWaypoint( FlightWaypointType.INGRESS_STRIKE, ingress_pos.x, ingress_pos.y, - self.doctrine["INGRESS_ALT"] + self.doctrine.ingress_altitude ) ingress_point.pretty_name = "INGRESS on " + location.name ingress_point.description = "INGRESS on " + location.name @@ -187,13 +187,13 @@ class FlightPlanBuilder: flight.points.append(point) egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + egress_heading, self.doctrine.ingress_egress_distance ) egress_point = FlightWaypoint( FlightWaypointType.EGRESS, egress_pos.x, egress_pos.y, - self.doctrine["EGRESS_ALT"] + self.doctrine.egress_altitude ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.name @@ -222,19 +222,19 @@ class FlightPlanBuilder: flight.flight_type = FlightType.CAP patrol_alt = random.randint( - self.doctrine["PATROL_ALT_RANGE"][0], - self.doctrine["PATROL_ALT_RANGE"][1] + self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude ) loc = location.position.point_from_heading( random.randint(0, 360), - random.randint(self.doctrine["CAP_DISTANCE_FROM_CP"][0], - self.doctrine["CAP_DISTANCE_FROM_CP"][1]) + random.randint(self.doctrine.cap_min_distance_from_cp, + self.doctrine.cap_max_distance_from_cp) ) hdg = location.position.heading_between_point(loc) radius = random.randint( - self.doctrine["CAP_PATTERN_LENGTH"][0], - self.doctrine["CAP_PATTERN_LENGTH"][1] + self.doctrine.cap_min_track_length, + self.doctrine.cap_max_track_length ) orbit0p = loc.point_from_heading(hdg - 90, radius) orbit1p = loc.point_from_heading(hdg + 90, radius) @@ -286,8 +286,8 @@ class FlightPlanBuilder: ally_cp, enemy_cp = location.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.min_patrol_altitude, + self.doctrine.max_patrol_altitude) # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector( @@ -372,13 +372,13 @@ class FlightPlanBuilder: egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading( - ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + ingress_heading, self.doctrine.ingress_egress_distance ) ingress_point = FlightWaypoint( FlightWaypointType.INGRESS_SEAD, ingress_pos.x, ingress_pos.y, - self.doctrine["INGRESS_ALT"] + self.doctrine.ingress_altitude ) ingress_point.name = "INGRESS" ingress_point.pretty_name = "INGRESS on " + location.name @@ -426,13 +426,13 @@ class FlightPlanBuilder: flight.points.append(point) egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"] + egress_heading, self.doctrine.ingress_egress_distance ) egress_point = FlightWaypoint( FlightWaypointType.EGRESS, egress_pos.x, egress_pos.y, - self.doctrine["EGRESS_ALT"] + self.doctrine.egress_altitude ) egress_point.name = "EGRESS" egress_point.pretty_name = "EGRESS from " + location.name @@ -531,7 +531,7 @@ class FlightPlanBuilder: FlightWaypointType.ASCEND_POINT, pos_ascend.x, pos_ascend.y, - self.doctrine["PATTERN_ALTITUDE"] + self.doctrine.pattern_altitude ) ascend.name = "ASCEND" ascend.alt_type = "RADIO" @@ -553,7 +553,7 @@ class FlightPlanBuilder: FlightWaypointType.DESCENT_POINT, descend.x, descend.y, - self.doctrine["PATTERN_ALTITUDE"] + self.doctrine.pattern_altitude ) descend.name = "DESCEND" descend.alt_type = "RADIO" From cc7c2cc707ac4d743576aa3b4f4b1739ea005418 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 00:58:19 -0700 Subject: [PATCH 20/48] Refactor flight plan generation. --- game/game.py | 2 + game/operation/operation.py | 24 +- gen/aircraft.py | 5 +- gen/briefinggen.py | 2 +- gen/flights/ai_flight_planner.py | 36 +-- gen/flights/closestairfields.py | 51 ++++ gen/flights/flightplan.py | 402 +++++++------------------------ gen/flights/waypointbuilder.py | 270 +++++++++++++++++++++ 8 files changed, 433 insertions(+), 359 deletions(-) create mode 100644 gen/flights/closestairfields.py create mode 100644 gen/flights/waypointbuilder.py diff --git a/game/game.py b/game/game.py index 0226de89..b9aead14 100644 --- a/game/game.py +++ b/game/game.py @@ -5,6 +5,7 @@ from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats from gen.ato import AirTaskingOrder from gen.flights.ai_flight_planner import CoalitionMissionPlanner +from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner import GroundPlanner from .event import * from .settings import Settings @@ -204,6 +205,7 @@ class Game: return event and event.name and event.name == self.player_name def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): + ObjectiveDistanceCache.set_theater(self.theater) logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) diff --git a/game/operation/operation.py b/game/operation/operation.py index c86efacb..86a63870 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -185,20 +185,16 @@ class Operation: self.airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map - for cp in self.game.theater.controlpoints: - side = cp.captured - if side: - country = self.current_mission.country(self.game.player_country) - ato = self.game.blue_ato - else: - country = self.current_mission.country(self.game.enemy_country) - ato = self.game.red_ato - self.airgen.generate_flights( - cp, - country, - ato, - self.groundobjectgen.runways - ) + self.airgen.generate_flights( + self.current_mission.country(self.game.player_country), + self.game.blue_ato, + self.groundobjectgen.runways + ) + self.airgen.generate_flights( + self.current_mission.country(self.game.enemy_country), + self.game.red_ato, + self.groundobjectgen.runways + ) # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] diff --git a/gen/aircraft.py b/gen/aircraft.py index 04301de7..f4581989 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -757,7 +757,7 @@ class AircraftConflictGenerator: for parking_slot in cp.airport.parking_slots: parking_slot.unit_id = None - def generate_flights(self, cp, country, ato: AirTaskingOrder, + def generate_flights(self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]) -> None: self.clear_parking_slots() @@ -768,7 +768,8 @@ class AircraftConflictGenerator: logging.info("Flight not generated: culled") continue logging.info(f"Generating flight: {flight.unit_type}") - group = self.generate_planned_flight(cp, country, flight) + group = self.generate_planned_flight(flight.from_cp, country, + flight) self.setup_flight_group(group, flight, flight.flight_type, dynamic_runways) self.setup_group_activation_trigger(flight, group) diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 10e07001..82744a8a 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -106,7 +106,7 @@ class BriefingGenerator(MissionInfoGenerator): aircraft = flight.aircraft_type flight_unit_name = db.unit_type_name(aircraft) self.description += "-" * 50 + "\n" - self.description += f"{flight_unit_name} x {flight.size + 2}\n\n" + self.description += f"{flight_unit_name} x {flight.size}\n\n" for i, wpt in enumerate(flight.waypoints): self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 6e97e91d..e65dc74a 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -19,6 +19,10 @@ from gen.flights.ai_flight_planner_db import ( SEAD_CAPABLE, STRIKE_CAPABLE, ) +from gen.flights.closestairfields import ( + ClosestAirfields, + ObjectiveDistanceCache, +) from gen.flights.flight import ( Flight, FlightType, @@ -37,29 +41,6 @@ if TYPE_CHECKING: from game.inventory import GlobalAircraftInventory -class ClosestAirfields: - """Precalculates which control points are closes to the given target.""" - - def __init__(self, target: MissionTarget, - all_control_points: List[ControlPoint]) -> None: - self.target = target - self.closest_airfields: List[ControlPoint] = sorted( - all_control_points, key=lambda c: self.target.distance_to(c) - ) - - def airfields_within(self, meters: int) -> Iterator[ControlPoint]: - """Iterates over all airfields within the given range of the target. - - Note that this iterates over *all* airfields, not just friendly - airfields. - """ - for cp in self.closest_airfields: - if cp.distance_to(self.target) < meters: - yield cp - else: - break - - @dataclass(frozen=True) class ProposedFlight: """A flight outline proposed by the mission planner. @@ -208,11 +189,6 @@ class ObjectiveFinder: def __init__(self, game: Game, is_player: bool) -> None: self.game = game self.is_player = is_player - # TODO: Cache globally at startup to avoid generating twice per turn? - self.closest_airfields: Dict[str, ClosestAirfields] = { - t.name: ClosestAirfields(t, self.game.theater.controlpoints) - for t in self.all_possible_targets() - } def enemy_sams(self) -> Iterator[TheaterGroundObject]: """Iterates over all enemy SAM sites.""" @@ -303,7 +279,7 @@ class ObjectiveFinder: CP. """ for cp in self.friendly_control_points(): - airfields_in_proximity = self.closest_airfields[cp.name] + airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_threat_range = airfields_in_proximity.airfields_within( self.AIRFIELD_THREAT_RANGE ) @@ -336,7 +312,7 @@ class ObjectiveFinder: def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" - return self.closest_airfields[location.name] + return ObjectiveDistanceCache.get_closest_airfields(location) class CoalitionMissionPlanner: diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py new file mode 100644 index 00000000..a6045dde --- /dev/null +++ b/gen/flights/closestairfields.py @@ -0,0 +1,51 @@ +"""Objective adjacency lists.""" +from typing import Dict, Iterator, List, Optional + +from theater import ConflictTheater, ControlPoint, MissionTarget + + +class ClosestAirfields: + """Precalculates which control points are closes to the given target.""" + + def __init__(self, target: MissionTarget, + all_control_points: List[ControlPoint]) -> None: + self.target = target + self.closest_airfields: List[ControlPoint] = sorted( + all_control_points, key=lambda c: self.target.distance_to(c) + ) + + def airfields_within(self, meters: int) -> Iterator[ControlPoint]: + """Iterates over all airfields within the given range of the target. + + Note that this iterates over *all* airfields, not just friendly + airfields. + """ + for cp in self.closest_airfields: + if cp.distance_to(self.target) < meters: + yield cp + else: + break + + +class ObjectiveDistanceCache: + theater: Optional[ConflictTheater] = None + closest_airfields: Dict[str, ClosestAirfields] = {} + + @classmethod + def set_theater(cls, theater: ConflictTheater) -> None: + if cls.theater is not None: + cls.closest_airfields = {} + cls.theater = theater + + @classmethod + def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields: + if cls.theater is None: + raise RuntimeError( + "Call ObjectiveDistanceCache.set_theater before using" + ) + + if location.name not in cls.closest_airfields: + cls.closest_airfields[location.name] = ClosestAirfields( + location, cls.theater.controlpoints + ) + return cls.closest_airfields[location.name] diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 0c631b31..a6e787a4 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -11,13 +11,15 @@ import logging import random from typing import List, Optional, TYPE_CHECKING -from game.data.doctrine import Doctrine, MODERN_DOCTRINE -from .flight import Flight, FlightType, FlightWaypointType, FlightWaypoint -from ..conflictgen import Conflict -from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject -from game.utils import nm_to_meter from dcs.unit import Unit +from game.data.doctrine import Doctrine, MODERN_DOCTRINE +from game.utils import nm_to_meter +from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType +from .waypointbuilder import WaypointBuilder +from ..conflictgen import Conflict + if TYPE_CHECKING: from game import Game @@ -103,108 +105,54 @@ class FlightPlanBuilder: # TODO: Stop clobbering flight type. flight.flight_type = FlightType.STRIKE - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) heading = flight.from_cp.position.heading_between_point( location.position ) ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading( ingress_heading, self.doctrine.ingress_egress_distance ) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_STRIKE, - ingress_pos.x, - ingress_pos.y, - self.doctrine.ingress_altitude - ) - ingress_point.pretty_name = "INGRESS on " + location.name - ingress_point.description = "INGRESS on " + location.name - ingress_point.name = "INGRESS" - flight.points.append(ingress_point) - - if len(location.groups) > 0 and location.dcs_identifier == "AA": - for g in location.groups: - for j, u in enumerate(g.units): - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - u.position.x, - u.position.y, - 0 - ) - point.description = ( - f"STRIKE [{location.name}] : {u.type} #{j}" - ) - point.pretty_name = ( - f"STRIKE [{location.name}] : {u.type} #{j}" - ) - point.name = f"{location.name} #{j}" - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) - else: - if hasattr(location, "obj_name"): - buildings = self.game.theater.find_ground_objects_by_obj_name( - location.obj_name - ) - for building in buildings: - if building.is_dead: - continue - - point = FlightWaypoint( - FlightWaypointType.TARGET_POINT, - building.position.x, - building.position.y, - 0 - ) - point.description = ( - f"STRIKE on {building.obj_name} {building.category} " - f"[{building.dcs_identifier}]" - ) - point.pretty_name = ( - f"STRIKE on {building.obj_name} {building.category} " - f"[{building.dcs_identifier}]" - ) - point.name = building.obj_name - point.only_for_player = True - ingress_point.targets.append(building) - flight.points.append(point) - else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 - ) - point.description = "STRIKE on " + location.name - point.pretty_name = "STRIKE on " + location.name - point.name = location.name - point.only_for_player = True - ingress_point.targets.append(location) - flight.points.append(point) + egress_heading = heading - 180 - 25 egress_pos = location.position.point_from_heading( egress_heading, self.doctrine.ingress_egress_distance ) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine.egress_altitude - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.name - egress_point.description = "EGRESS from " + location.name - flight.points.append(egress_point) - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.ingress_strike(ingress_pos, location) - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + if len(location.groups) > 0 and location.dcs_identifier == "AA": + # TODO: Replace with DEAD? + # Strike missions on SEAD targets target units. + for g in location.groups: + for j, u in enumerate(g.units): + builder.strike_point(u, f"{u.type} #{j}", location) + else: + # TODO: Does this actually happen? + # ConflictTheater is built with the belief that multiple ground + # objects have the same name. If that's the case, + # TheaterGroundObject needs some refactoring because it behaves very + # differently for SAM sites than it does for strike targets. + buildings = self.game.theater.find_ground_objects_by_obj_name( + location.obj_name + ) + for building in buildings: + if building.is_dead: + continue + + builder.strike_point( + building, + f"{building.obj_name} {building.category}", + location + ) + + builder.egress(egress_pos, location) + builder.rtb(flight.from_cp) + + flight.points = builder.build() def generate_barcap(self, flight: Flight, location: MissionTarget) -> None: """Generate a BARCAP flight at a given location. @@ -239,39 +187,11 @@ class FlightPlanBuilder: orbit0p = loc.point_from_heading(hdg - 90, radius) orbit1p = loc.point_from_heading(hdg + 90, radius) - # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - orbit0.targets.append(location) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.race_track(orbit0p, orbit1p, patrol_alt) + builder.rtb(flight.from_cp) + flight.points = builder.build() def generate_frontline_cap(self, flight: Flight, location: MissionTarget) -> None: @@ -309,40 +229,11 @@ class FlightPlanBuilder: orbit1p = orbit_center.point_from_heading(heading + 180, radius) # Create points - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - - orbit0 = FlightWaypoint( - FlightWaypointType.PATROL_TRACK, - orbit0p.x, - orbit0p.y, - patrol_alt - ) - orbit0.name = "ORBIT 0" - orbit0.description = "Standby between this point and the next one" - orbit0.pretty_name = "Race-track start" - flight.points.append(orbit0) - - orbit1 = FlightWaypoint( - FlightWaypointType.PATROL, - orbit1p.x, - orbit1p.y, - patrol_alt - ) - orbit1.name = "ORBIT 1" - orbit1.description = "Standby between this point and the previous one" - orbit1.pretty_name = "Race-track end" - flight.points.append(orbit1) - - # Note: Targets of PATROL TRACK waypoints are the points to be defended. - orbit0.targets.append(flight.from_cp) - orbit0.targets.append(center) - - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.race_track(orbit0p, orbit1p, patrol_alt) + builder.rtb(flight.from_cp) + flight.points = builder.build() def generate_sead(self, flight: Flight, location: MissionTarget, custom_targets: Optional[List[Unit]] = None) -> None: @@ -359,33 +250,30 @@ class FlightPlanBuilder: if custom_targets is None: custom_targets = [] - flight.points = [] flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) - ascend = self.generate_ascend_point(flight.from_cp) - flight.points.append(ascend) - heading = flight.from_cp.position.heading_between_point( location.position ) ingress_heading = heading - 180 + 25 - egress_heading = heading - 180 - 25 ingress_pos = location.position.point_from_heading( ingress_heading, self.doctrine.ingress_egress_distance ) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_SEAD, - ingress_pos.x, - ingress_pos.y, - self.doctrine.ingress_altitude - ) - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS on " + location.name - ingress_point.description = "INGRESS on " + location.name - flight.points.append(ingress_point) - if len(custom_targets) > 0: + egress_heading = heading - 180 - 25 + egress_pos = location.position.point_from_heading( + egress_heading, self.doctrine.ingress_egress_distance + ) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.ingress_sead(ingress_pos, location) + + # TODO: Unify these. + # There doesn't seem to be any reason to treat the UI fragged missions + # different from the automatic missions. + if custom_targets: for target in custom_targets: point = FlightWaypoint( FlightWaypointType.TARGET_POINT, @@ -395,55 +283,19 @@ class FlightPlanBuilder: ) point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + target.type - point.pretty_name = "DEAD on " + location.name - point.only_for_player = True + builder.dead_point(target, location.name, location) else: - point.description = "SEAD on " + location.name - point.pretty_name = "SEAD on " + location.name - point.only_for_player = True - flight.points.append(point) - ingress_point.targets.append(location) - ingress_point.targetGroup = location + builder.sead_point(target, location.name, location) else: - point = FlightWaypoint( - FlightWaypointType.TARGET_GROUP_LOC, - location.position.x, - location.position.y, - 0 - ) - point.alt_type = "RADIO" if flight.flight_type == FlightType.DEAD: - point.description = "DEAD on " + location.name - point.pretty_name = "DEAD on " + location.name - point.only_for_player = True + builder.dead_area(location) else: - point.description = "SEAD on " + location.name - point.pretty_name = "SEAD on " + location.name - point.only_for_player = True - ingress_point.targets.append(location) - ingress_point.targetGroup = location - flight.points.append(point) + builder.sead_area(location) - egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine.ingress_egress_distance - ) - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress_pos.x, - egress_pos.y, - self.doctrine.egress_altitude - ) - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS from " + location.name - egress_point.description = "EGRESS from " + location.name - flight.points.append(egress_point) + builder.egress(egress_pos, location) + builder.rtb(flight.from_cp) - descend = self.generate_descend_point(flight.from_cp) - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + flight.points = builder.build() def generate_cas(self, flight: Flight, location: MissionTarget) -> None: """Generate a CAS flight plan for the given target. @@ -455,89 +307,36 @@ class FlightPlanBuilder: if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - from_cp, location = location.control_points is_helo = getattr(flight.unit_type, "helicopter", False) - cap_alt = 1000 - flight.points = [] + cap_alt = 500 if is_helo else 1000 flight.flight_type = FlightType.CAS ingress, heading, distance = Conflict.frontline_vector( - from_cp, location, self.game.theater + location.control_points[0], location.control_points[1], + self.game.theater ) center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) - ascend = self.generate_ascend_point(flight.from_cp) - if is_helo: - cap_alt = 500 - ascend.alt = 500 - flight.points.append(ascend) + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp, is_helo) + builder.ingress_cas(ingress, location) + builder.cas(center, cap_alt) + builder.egress(egress, location) + builder.rtb(flight.from_cp, is_helo) - ingress_point = FlightWaypoint( - FlightWaypointType.INGRESS_CAS, - ingress.x, - ingress.y, - cap_alt - ) - ingress_point.alt_type = "RADIO" - ingress_point.name = "INGRESS" - ingress_point.pretty_name = "INGRESS" - ingress_point.description = "Ingress into CAS area" - flight.points.append(ingress_point) - - center_point = FlightWaypoint( - FlightWaypointType.CAS, - center.x, - center.y, - cap_alt - ) - center_point.alt_type = "RADIO" - center_point.description = "Provide CAS" - center_point.name = "CAS" - center_point.pretty_name = "CAS" - flight.points.append(center_point) - - egress_point = FlightWaypoint( - FlightWaypointType.EGRESS, - egress.x, - egress.y, - cap_alt - ) - egress_point.alt_type = "RADIO" - egress_point.description = "Egress from CAS area" - egress_point.name = "EGRESS" - egress_point.pretty_name = "EGRESS" - flight.points.append(egress_point) - - descend = self.generate_descend_point(flight.from_cp) - if is_helo: - descend.alt = 300 - flight.points.append(descend) - - rtb = self.generate_rtb_waypoint(flight.from_cp) - flight.points.append(rtb) + flight.points = builder.build() + # TODO: Make a model for the waypoint builder and use that in the UI. def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint: """Generate ascend point. Args: departure: Departure airfield or carrier. """ - ascend_heading = departure.heading - pos_ascend = departure.position.point_from_heading( - ascend_heading, 10000 - ) - ascend = FlightWaypoint( - FlightWaypointType.ASCEND_POINT, - pos_ascend.x, - pos_ascend.y, - self.doctrine.pattern_altitude - ) - ascend.name = "ASCEND" - ascend.alt_type = "RADIO" - ascend.description = "Ascend" - ascend.pretty_name = "Ascend" - return ascend + builder = WaypointBuilder(self.doctrine) + builder.ascent(departure) + return builder.build()[0] def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint: """Generate approach/descend point. @@ -545,21 +344,9 @@ class FlightPlanBuilder: Args: arrival: Arrival airfield or carrier. """ - ascend_heading = arrival.heading - descend = arrival.position.point_from_heading( - ascend_heading - 180, 10000 - ) - descend = FlightWaypoint( - FlightWaypointType.DESCENT_POINT, - descend.x, - descend.y, - self.doctrine.pattern_altitude - ) - descend.name = "DESCEND" - descend.alt_type = "RADIO" - descend.description = "Descend to pattern alt" - descend.pretty_name = "Descend to pattern alt" - return descend + builder = WaypointBuilder(self.doctrine) + builder.descent(arrival) + return builder.build()[0] @staticmethod def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint: @@ -568,15 +355,6 @@ class FlightPlanBuilder: Args: arrival: Arrival airfield or carrier. """ - rtb = arrival.position - rtb = FlightWaypoint( - FlightWaypointType.LANDING_POINT, - rtb.x, - rtb.y, - 0 - ) - rtb.name = "LANDING" - rtb.alt_type = "RADIO" - rtb.description = "RTB" - rtb.pretty_name = "RTB" - return rtb + builder = WaypointBuilder(self.doctrine) + builder.land(arrival) + return builder.build()[0] diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py new file mode 100644 index 00000000..7ef4a30e --- /dev/null +++ b/gen/flights/waypointbuilder.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +from typing import List, Optional, Union + +from dcs.mapping import Point +from dcs.unit import Unit + +from game.data.doctrine import Doctrine +from game.utils import nm_to_meter +from theater import ControlPoint, MissionTarget, TheaterGroundObject +from .flight import FlightWaypoint, FlightWaypointType + + +class WaypointBuilder: + def __init__(self, doctrine: Doctrine) -> None: + self.doctrine = doctrine + self.waypoints: List[FlightWaypoint] = [] + self.ingress_point: Optional[FlightWaypoint] = None + + def build(self) -> List[FlightWaypoint]: + return self.waypoints + + def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None: + """Create ascent waypoint for the given departure airfield or carrier. + + Args: + departure: Departure airfield or carrier. + """ + # TODO: Pick runway based on wind direction. + heading = departure.heading + position = departure.position.point_from_heading( + heading, nm_to_meter(5) + ) + waypoint = FlightWaypoint( + FlightWaypointType.ASCEND_POINT, + position.x, + position.y, + 500 if is_helo else self.doctrine.pattern_altitude + ) + waypoint.name = "ASCEND" + waypoint.alt_type = "RADIO" + waypoint.description = "Ascend" + waypoint.pretty_name = "Ascend" + self.waypoints.append(waypoint) + + def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None: + """Create descent waypoint for the given arrival airfield or carrier. + + Args: + arrival: Arrival airfield or carrier. + """ + # TODO: Pick runway based on wind direction. + # ControlPoint.heading is the departure heading. + heading = (arrival.heading + 180) % 360 + position = arrival.position.point_from_heading( + heading, nm_to_meter(5) + ) + waypoint = FlightWaypoint( + FlightWaypointType.DESCENT_POINT, + position.x, + position.y, + 300 if is_helo else self.doctrine.pattern_altitude + ) + waypoint.name = "DESCEND" + waypoint.alt_type = "RADIO" + waypoint.description = "Descend to pattern altitude" + waypoint.pretty_name = "Ascend" + self.waypoints.append(waypoint) + + def land(self, arrival: ControlPoint) -> None: + """Create descent waypoint for the given arrival airfield or carrier. + + Args: + arrival: Arrival airfield or carrier. + """ + position = arrival.position + waypoint = FlightWaypoint( + FlightWaypointType.LANDING_POINT, + position.x, + position.y, + 0 + ) + waypoint.name = "LANDING" + waypoint.alt_type = "RADIO" + waypoint.description = "Land" + waypoint.pretty_name = "Land" + self.waypoints.append(waypoint) + + def ingress_cas(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_CAS, position, objective) + + def ingress_sead(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective) + + def ingress_strike(self, position: Point, objective: MissionTarget) -> None: + self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective) + + def _ingress(self, ingress_type: FlightWaypointType, position: Point, + objective: MissionTarget) -> None: + if self.ingress_point is not None: + raise RuntimeError("A flight plan can have only one ingress point.") + + waypoint = FlightWaypoint( + ingress_type, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "INGRESS on " + objective.name + waypoint.description = "INGRESS on " + objective.name + waypoint.name = "INGRESS" + self.waypoints.append(waypoint) + self.ingress_point = waypoint + + def egress(self, position: Point, target: MissionTarget) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.EGRESS, + position.x, + position.y, + self.doctrine.ingress_altitude + ) + waypoint.pretty_name = "EGRESS from " + target.name + waypoint.description = "EGRESS from " + target.name + waypoint.name = "EGRESS" + self.waypoints.append(waypoint) + + def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + # TODO: Seems fishy. + self.ingress_point.targetGroup = location + + def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + # TODO: Seems fishy. + self.ingress_point.targetGroup = location + + def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str, + location: MissionTarget) -> None: + self._target_point(target, name, f"STRIKE [{location.name}]: {name}", + location) + + def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str, + description: str, location: MissionTarget) -> None: + if self.ingress_point is None: + raise RuntimeError( + "An ingress point must be added before target points." + ) + + waypoint = FlightWaypoint( + FlightWaypointType.TARGET_POINT, + target.position.x, + target.position.y, + 0 + ) + waypoint.description = description + waypoint.pretty_name = description + waypoint.name = name + waypoint.only_for_player = True + self.waypoints.append(waypoint) + # TODO: This seems wrong, but it's what was there before. + self.ingress_point.targets.append(location) + + def sead_area(self, target: MissionTarget) -> None: + self._target_area(f"SEAD on {target.name}", target) + # TODO: Seems fishy. + self.ingress_point.targetGroup = target + + def dead_area(self, target: MissionTarget) -> None: + self._target_area(f"DEAD on {target.name}", target) + # TODO: Seems fishy. + self.ingress_point.targetGroup = target + + def _target_area(self, name: str, location: MissionTarget) -> None: + if self.ingress_point is None: + raise RuntimeError( + "An ingress point must be added before target points." + ) + + waypoint = FlightWaypoint( + FlightWaypointType.TARGET_GROUP_LOC, + location.position.x, + location.position.y, + 0 + ) + waypoint.description = name + waypoint.pretty_name = name + waypoint.name = name + waypoint.only_for_player = True + self.waypoints.append(waypoint) + # TODO: This seems wrong, but it's what was there before. + self.ingress_point.targets.append(location) + + def cas(self, position: Point, altitude: int) -> None: + waypoint = FlightWaypoint( + FlightWaypointType.CAS, + position.x, + position.y, + altitude + ) + waypoint.alt_type = "RADIO" + waypoint.description = "Provide CAS" + waypoint.name = "CAS" + waypoint.pretty_name = "CAS" + self.waypoints.append(waypoint) + + def race_track_start(self, position: Point, altitude: int) -> None: + """Creates a racetrack start waypoint. + + Args: + position: Position of the waypoint. + altitude: Altitude of the racetrack in meters. + """ + waypoint = FlightWaypoint( + FlightWaypointType.PATROL_TRACK, + position.x, + position.y, + altitude + ) + waypoint.name = "RACETRACK START" + waypoint.description = "Orbit between this point and the next point" + waypoint.pretty_name = "Race-track start" + self.waypoints.append(waypoint) + + # TODO: Does this actually do anything? + # orbit0.targets.append(location) + # Note: Targets of PATROL TRACK waypoints are the points to be defended. + # orbit0.targets.append(flight.from_cp) + # orbit0.targets.append(center) + + def race_track_end(self, position: Point, altitude: int) -> None: + """Creates a racetrack end waypoint. + + Args: + position: Position of the waypoint. + altitude: Altitude of the racetrack in meters. + """ + waypoint = FlightWaypoint( + FlightWaypointType.PATROL, + position.x, + position.y, + altitude + ) + waypoint.name = "RACETRACK END" + waypoint.description = "Orbit between this point and the previous point" + waypoint.pretty_name = "Race-track end" + self.waypoints.append(waypoint) + + def race_track(self, start: Point, end: Point, altitude: int) -> None: + """Creates two waypoint for a racetrack orbit. + + Args: + start: The beginning racetrack waypoint. + end: The ending racetrack waypoint. + altitude: The racetrack altitude. + """ + self.race_track_start(start, altitude) + self.race_track_end(end, altitude) + + def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None: + """Creates descent ant landing waypoints for the given control point. + + Args: + arrival: Arrival airfield or carrier. + """ + self.descent(arrival, is_helo) + self.land(arrival) From 582c43fb6cd0d4cbea53786f5eed65b1b924732c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 00:51:27 -0700 Subject: [PATCH 21/48] Generate CAP missions in useful locations. CAP missions should be between the protected location and the nearest threat. Find the closest enemy airfield and ensure that the CAP race track is between it and the protected location. --- gen/flights/flightplan.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index a6e787a4..45bf2a13 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -16,6 +16,7 @@ from dcs.unit import Unit from game.data.doctrine import Doctrine, MODERN_DOCTRINE from game.utils import nm_to_meter from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject +from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .waypointbuilder import WaypointBuilder from ..conflictgen import Conflict @@ -37,6 +38,7 @@ class FlightPlanBuilder: def __init__(self, game: Game, is_player: bool) -> None: self.game = game + self.is_player = is_player if is_player: faction = self.game.player_faction else: @@ -174,18 +176,30 @@ class FlightPlanBuilder: self.doctrine.max_patrol_altitude ) + closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) + for airfield in closest_cache.closest_airfields: + if airfield.captured != self.is_player: + closest_airfield = airfield + break + else: + logging.error("Could not find any enemy airfields") + return + + heading = location.position.heading_between_point( + closest_airfield.position + ) + loc = location.position.point_from_heading( - random.randint(0, 360), + heading, random.randint(self.doctrine.cap_min_distance_from_cp, self.doctrine.cap_max_distance_from_cp) ) - hdg = location.position.heading_between_point(loc) radius = random.randint( self.doctrine.cap_min_track_length, self.doctrine.cap_max_track_length ) - orbit0p = loc.point_from_heading(hdg - 90, radius) - orbit1p = loc.point_from_heading(hdg + 90, radius) + orbit0p = loc.point_from_heading(heading - 90, radius) + orbit1p = loc.point_from_heading(heading + 90, radius) builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) From 2aecea88b06fa750dc65c8a148c6ac2dc05df81d Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 00:44:09 -0700 Subject: [PATCH 22/48] Orient CAP tracks toward the enemy. Pointing the race track 90 degrees away from where the enemy is expected means the radar can't see much. CAP flights normally fly *toward* the expected direction of contact and alternate approaching and retreating legs with their wingman. --- gen/flights/flightplan.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 45bf2a13..ab22c13d 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -189,21 +189,20 @@ class FlightPlanBuilder: closest_airfield.position ) - loc = location.position.point_from_heading( + end = location.position.point_from_heading( heading, random.randint(self.doctrine.cap_min_distance_from_cp, self.doctrine.cap_max_distance_from_cp) ) - radius = random.randint( + diameter = random.randint( self.doctrine.cap_min_track_length, self.doctrine.cap_max_track_length ) - orbit0p = loc.point_from_heading(heading - 90, radius) - orbit1p = loc.point_from_heading(heading + 90, radius) + start = end.point_from_heading(heading - 180, diameter) builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.race_track(orbit0p, orbit1p, patrol_alt) + builder.race_track(start, end, patrol_alt) builder.rtb(flight.from_cp) flight.points = builder.build() From 07cbaa3e7069f4c369b0dbbcff1e0cdca7433b26 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 01:16:50 -0700 Subject: [PATCH 23/48] Plan escort flights. TODO: UI --- gen/aircraft.py | 8 ++++++++ gen/flights/ai_flight_planner.py | 6 ++++-- gen/flights/flightplan.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/gen/aircraft.py b/gen/aircraft.py index f4581989..9eeb1836 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -929,6 +929,14 @@ class AircraftConflictGenerator: group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) group.points[0].tasks.append(OptRestrictJettison(True)) + elif flight_type == FlightType.ESCORT: + group.task = Escort.name + self._setup_group(group, Escort, flight, dynamic_runways) + # TODO: Cleanup duplication... + group.points[0].tasks.clear() + group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)) + group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire)) + group.points[0].tasks.append(OptRestrictJettison(True)) group.points[0].tasks.append(OptRTBOnBingoFuel(True)) group.points[0].tasks.append(OptRestrictAfterburner(True)) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index e65dc74a..98ef2bd2 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -116,6 +116,8 @@ class AircraftAllocator: types = SEAD_CAPABLE elif flight.task == FlightType.STRIKE: types = STRIKE_CAPABLE + elif flight.task == FlightType.ESCORT: + types = CAP_CAPABLE else: logging.error(f"Unplannable flight type: {flight.task}") return None @@ -373,7 +375,7 @@ class CoalitionMissionPlanner: yield ProposedMission(sam, [ ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE), # TODO: Max escort range. - ProposedFlight(FlightType.CAP, 2, self.MAX_SEAD_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE), ]) # Plan strike missions. @@ -382,7 +384,7 @@ class CoalitionMissionPlanner: ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE), # TODO: Max escort range. ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE), - ProposedFlight(FlightType.CAP, 2, self.MAX_STRIKE_RANGE), + ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE), ]) def plan_missions(self) -> None: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index ab22c13d..6a213f7a 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -67,6 +67,8 @@ class FlightPlanBuilder: self.generate_sead(flight, objective_location) elif task == FlightType.ELINT: logging.error("ELINT flight plan generation not implemented") + elif task == FlightType.ESCORT: + self.generate_escort(flight, objective_location) elif task == FlightType.EVAC: logging.error("Evac flight plan generation not implemented") elif task == FlightType.EWAR: @@ -310,6 +312,39 @@ class FlightPlanBuilder: flight.points = builder.build() + def generate_escort(self, flight: Flight, location: MissionTarget) -> None: + flight.flight_type = FlightType.ESCORT + + # 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 + # flights. + heading = flight.from_cp.position.heading_between_point( + location.position + ) + ingress_heading = heading - 180 + 25 + + ingress_pos = location.position.point_from_heading( + ingress_heading, self.doctrine.ingress_egress_distance + ) + + egress_heading = heading - 180 - 25 + egress_pos = location.position.point_from_heading( + egress_heading, self.doctrine.ingress_egress_distance + ) + + patrol_alt = random.randint( + self.doctrine.min_patrol_altitude, + self.doctrine.max_patrol_altitude + ) + + builder = WaypointBuilder(self.doctrine) + builder.ascent(flight.from_cp) + builder.race_track(ingress_pos, egress_pos, patrol_alt) + builder.rtb(flight.from_cp) + + flight.points = builder.build() + def generate_cas(self, flight: Flight, location: MissionTarget) -> None: """Generate a CAS flight plan for the given target. From 56a58646004d42b47429eaa1618e94eca8a63ea2 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 01:51:00 -0700 Subject: [PATCH 24/48] Generate common ingress/egress points. This still isn't very good because it doesn't work well for anything but the automatically planned package. Instead, should be a part of the Package itself, generated the first time it is needed, and resettable by the user. --- gen/ato.py | 19 +++++-- gen/flights/ai_flight_planner.py | 2 +- gen/flights/flightplan.py | 96 ++++++++++++++++---------------- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/gen/ato.py b/gen/ato.py index e82930fe..2ad8b6ec 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -11,7 +11,7 @@ the single CAP flight. from collections import defaultdict from dataclasses import dataclass, field import logging -from typing import Dict, List +from typing import Dict, Iterator, List, Optional from .flights.flight import Flight, FlightType from theater.missiontarget import MissionTarget @@ -48,10 +48,9 @@ class Package: self.flights.remove(flight) @property - def package_description(self) -> str: - """Generates a package description based on flight composition.""" + def primary_task(self) -> Optional[FlightType]: if not self.flights: - return "No mission" + return None flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0) for flight in self.flights: @@ -84,13 +83,21 @@ class Package: ] for task in task_priorities: if flight_counts[task]: - return task.name + return task # If we get here, our task_priorities list above is incomplete. Log the # issue and return the type of *any* flight in the package. some_mission = next(iter(self.flights)).flight_type logging.warning(f"Unhandled mission type: {some_mission}") - return some_mission.name + return some_mission + + @property + def package_description(self) -> str: + """Generates a package description based on flight composition.""" + task = self.primary_task + if task is None: + return "No mission" + return task.name def __hash__(self) -> int: # TODO: Far from perfect. Number packages? diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 98ef2bd2..d2abd7fd 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -414,8 +414,8 @@ class CoalitionMissionPlanner: return package = builder.build() + builder = FlightPlanBuilder(self.game, self.is_player, package) for flight in package.flights: - builder = FlightPlanBuilder(self.game, self.is_player) builder.populate_flight_plan(flight, package.target) self.ato.add_package(package) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 6a213f7a..5e3dea03 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -11,10 +11,12 @@ import logging import random from typing import List, Optional, TYPE_CHECKING +from dcs.mapping import Point from dcs.unit import Unit from game.data.doctrine import Doctrine, MODERN_DOCTRINE from game.utils import nm_to_meter +from gen.ato import Package from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType @@ -36,8 +38,10 @@ class InvalidObjectiveLocation(RuntimeError): class FlightPlanBuilder: """Generates flight plans for flights.""" - def __init__(self, game: Game, is_player: bool) -> None: + def __init__(self, game: Game, is_player: bool, + package: Optional[Package] = None) -> None: self.game = game + self.package = package self.is_player = is_player if is_player: faction = self.game.player_faction @@ -110,23 +114,9 @@ class FlightPlanBuilder: # TODO: Stop clobbering flight type. flight.flight_type = FlightType.STRIKE - heading = flight.from_cp.position.heading_between_point( - location.position - ) - ingress_heading = heading - 180 + 25 - - ingress_pos = location.position.point_from_heading( - ingress_heading, self.doctrine.ingress_egress_distance - ) - - egress_heading = heading - 180 - 25 - egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine.ingress_egress_distance - ) - builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.ingress_strike(ingress_pos, location) + builder.ingress_strike(self.ingress_point(flight, location), location) if len(location.groups) > 0 and location.dcs_identifier == "AA": # TODO: Replace with DEAD? @@ -153,7 +143,7 @@ class FlightPlanBuilder: location ) - builder.egress(egress_pos, location) + builder.egress(self.egress_point(flight, location), location) builder.rtb(flight.from_cp) flight.points = builder.build() @@ -267,23 +257,9 @@ class FlightPlanBuilder: flight.flight_type = random.choice([FlightType.SEAD, FlightType.DEAD]) - heading = flight.from_cp.position.heading_between_point( - location.position - ) - ingress_heading = heading - 180 + 25 - - ingress_pos = location.position.point_from_heading( - ingress_heading, self.doctrine.ingress_egress_distance - ) - - egress_heading = heading - 180 - 25 - egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine.ingress_egress_distance - ) - builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.ingress_sead(ingress_pos, location) + builder.ingress_sead(self.ingress_point(flight, location), location) # TODO: Unify these. # There doesn't seem to be any reason to treat the UI fragged missions @@ -307,7 +283,7 @@ class FlightPlanBuilder: else: builder.sead_area(location) - builder.egress(egress_pos, location) + builder.egress(self.egress_point(flight, location), location) builder.rtb(flight.from_cp) flight.points = builder.build() @@ -319,19 +295,6 @@ class FlightPlanBuilder: # Packages should determine some common points like push, ingress, # egress, and split points ahead of time so they can be shared by all # flights. - heading = flight.from_cp.position.heading_between_point( - location.position - ) - ingress_heading = heading - 180 + 25 - - ingress_pos = location.position.point_from_heading( - ingress_heading, self.doctrine.ingress_egress_distance - ) - - egress_heading = heading - 180 - 25 - egress_pos = location.position.point_from_heading( - egress_heading, self.doctrine.ingress_egress_distance - ) patrol_alt = random.randint( self.doctrine.min_patrol_altitude, @@ -340,7 +303,8 @@ class FlightPlanBuilder: builder = WaypointBuilder(self.doctrine) builder.ascent(flight.from_cp) - builder.race_track(ingress_pos, egress_pos, patrol_alt) + builder.race_track(self.ingress_point(flight, location), + self.egress_point(flight, location), patrol_alt) builder.rtb(flight.from_cp) flight.points = builder.build() @@ -396,8 +360,7 @@ class FlightPlanBuilder: builder.descent(arrival) return builder.build()[0] - @staticmethod - def generate_rtb_waypoint(arrival: ControlPoint) -> FlightWaypoint: + def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint: """Generate RTB landing point. Args: @@ -406,3 +369,38 @@ class FlightPlanBuilder: builder = WaypointBuilder(self.doctrine) 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( + 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( + 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) + + # TODO: Set ingress/egress/join/split points in the Package. + def package_airfield(self, flight: Flight, + target: MissionTarget) -> ControlPoint: + # 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) + for airfield in cache.closest_airfields: + for flight in self.package.flights: + if flight.from_cp == airfield: + return airfield + raise RuntimeError( + "Could not find any airfield assigned to this package" + ) From 6ce82be46b655a044723bfc3d95d73dd107f19c3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 18:22:20 -0700 Subject: [PATCH 25/48] 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() From b13711ddef5c166eb6cbad523389cf13a8ceecce Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 29 Sep 2020 23:29:30 -0700 Subject: [PATCH 26/48] Update the waypoint builder UI. Changing targets doesn't make sense now that flights belong to a package. Change all the "generate" dialogs to simply confirm dialogs to make sure the user is okay with us clobbering the flight plan. --- .../generator/QAbstractMissionGenerator.py | 47 ----- .../flight/generator/QCAPMissionGenerator.py | 69 -------- .../flight/generator/QCASMissionGenerator.py | 73 -------- .../flight/generator/QSEADMissionGenerator.py | 92 ---------- .../generator/QSTRIKEMissionGenerator.py | 72 -------- .../flight/waypoints/QFlightWaypointTab.py | 162 +++++++++--------- 6 files changed, 82 insertions(+), 433 deletions(-) delete mode 100644 qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py delete mode 100644 qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py delete mode 100644 qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py delete mode 100644 qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py delete mode 100644 qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py diff --git a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py deleted file mode 100644 index 4811da1d..00000000 --- a/qt_ui/windows/mission/flight/generator/QAbstractMissionGenerator.py +++ /dev/null @@ -1,47 +0,0 @@ -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 -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox - - -class QAbstractMissionGenerator(QDialog): - - 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) - self.setModal(True) - self.setWindowTitle(title) - self.setWindowIcon(EVENT_ICONS["strike"]) - self.flight_waypoint_list = flight_waypoint_list - self.planner = FlightPlanBuilder(self.game, package, is_player=True) - - self.selected_waypoints = [] - self.wpt_info = QFlightWaypointInfoBox() - - self.ok_button = QPushButton("Ok") - self.ok_button.clicked.connect(self.apply) - - def on_select_wpt_changed(self): - self.selected_waypoints = self.wpt_selection_box.get_selected_waypoints(False) - if self.selected_waypoints is None or len(self.selected_waypoints) <= 0: - self.ok_button.setDisabled(True) - else: - self.wpt_info.set_flight_waypoint(self.selected_waypoints[0]) - self.ok_button.setDisabled(False) - - def apply(self): - raise NotImplementedError() - - - - diff --git a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py b/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py deleted file mode 100644 index b376851d..00000000 --- a/qt_ui/windows/mission/flight/generator/QCAPMissionGenerator.py +++ /dev/null @@ -1,69 +0,0 @@ -import logging - -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout - -from game import Game -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, 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) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.init_ui() - self.on_select_wpt_changed() - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAP mission on : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - 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 deleted file mode 100644 index 76177772..00000000 --- a/qt_ui/windows/mission/flight/generator/QCASMissionGenerator.py +++ /dev/null @@ -1,73 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox -from dcs import Point - -from game import Game -from game.utils import meter_to_nm -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, 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) - self.wpt_selection_box.currentTextChanged.connect(self.on_select_wpt_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.init_ui() - self.on_select_wpt_changed() - - def on_select_wpt_changed(self): - super(QCASMissionGenerator, self).on_select_wpt_changed() - wpts = self.wpt_selection_box.get_selected_waypoints() - - if len(wpts) > 0: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(Point(wpts[0].x, wpts[0].y)))) + " nm") - else: - self.distanceToTargetLabel.setText("??? nm") - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("CAS : ")) - wpt_layout.addWidget(self.wpt_selection_box) - wpt_layout.addStretch() - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.wpt_info) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - 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 deleted file mode 100644 index dd3a31f8..00000000 --- a/qt_ui/windows/mission/flight/generator/QSEADMissionGenerator.py +++ /dev/null @@ -1,92 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -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 - - -class QSEADMissionGenerator(QAbstractMissionGenerator): - - 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) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - self.distanceToTargetLabel = QLabel("0 nm") - self.threatRangeLabel = QLabel("0 nm") - self.detectionRangeLabel = QLabel("0 nm") - self.seadTargetInfoView = QSeadTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - if target is not None: - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.threatRangeLabel.setText(str(meter_to_nm(target.threat_range)) + " nm") - self.detectionRangeLabel.setText(str(meter_to_nm(target.detection_range)) + " nm") - self.seadTargetInfoView.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("SEAD/DEAD target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distThreatBox = QGroupBox("Infos :") - threatLayout = QVBoxLayout() - - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to site : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - - threatRangeLayout = QHBoxLayout() - threatRangeLayout.addWidget(QLabel("Site threat range : ")) - threatRangeLayout.addStretch() - threatRangeLayout.addWidget(self.threatRangeLabel, alignment=Qt.AlignRight) - - detectionRangeLayout = QHBoxLayout() - detectionRangeLayout.addWidget(QLabel("Site radar detection range: ")) - detectionRangeLayout.addStretch() - detectionRangeLayout.addWidget(self.detectionRangeLabel, alignment=Qt.AlignRight) - - threatLayout.addLayout(distToTarget) - threatLayout.addLayout(threatRangeLayout) - threatLayout.addLayout(detectionRangeLayout) - distThreatBox.setLayout(threatLayout) - - layout.addLayout(wpt_layout) - layout.addWidget(self.seadTargetInfoView) - layout.addWidget(distThreatBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - target = self.tgt_selection_box.get_selected_target() - 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 deleted file mode 100644 index c210208c..00000000 --- a/qt_ui/windows/mission/flight/generator/QSTRIKEMissionGenerator.py +++ /dev/null @@ -1,72 +0,0 @@ -from PySide2.QtGui import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QGroupBox - -from game import Game -from game.utils import meter_to_nm -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 - - -class QSTRIKEMissionGenerator(QAbstractMissionGenerator): - - 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) - self.tgt_selection_box.currentTextChanged.connect(self.on_selected_target_changed) - - - self.distanceToTargetLabel = QLabel("0 nm") - self.strike_infos = QStrikeTargetInfoView(None) - self.init_ui() - self.on_selected_target_changed() - - def on_selected_target_changed(self): - target = self.tgt_selection_box.get_selected_target() - self.distanceToTargetLabel.setText("~" + str(meter_to_nm(self.flight.from_cp.position.distance_to_point(target.location.position))) + " nm") - self.strike_infos.setTarget(target) - - def init_ui(self): - layout = QVBoxLayout() - - wpt_layout = QHBoxLayout() - wpt_layout.addWidget(QLabel("Target : ")) - wpt_layout.addStretch() - wpt_layout.addWidget(self.tgt_selection_box, alignment=Qt.AlignRight) - - distToTargetBox = QGroupBox("Infos :") - distToTarget = QHBoxLayout() - distToTarget.addWidget(QLabel("Distance to target : ")) - distToTarget.addStretch() - distToTarget.addWidget(self.distanceToTargetLabel, alignment=Qt.AlignRight) - distToTargetBox.setLayout(distToTarget) - - layout.addLayout(wpt_layout) - layout.addWidget(self.strike_infos) - layout.addWidget(distToTargetBox) - layout.addStretch() - layout.addWidget(self.ok_button) - - self.setLayout(layout) - - def apply(self): - target = self.tgt_selection_box.get_selected_target() - 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 7561d639..2879c356 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -1,16 +1,24 @@ +from typing import List, Optional + from PySide2.QtCore import Signal -from PySide2.QtWidgets import QFrame, QGridLayout, QLabel, QPushButton, QVBoxLayout +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QLabel, + QMessageBox, + QPushButton, + QVBoxLayout, +) from game import Game from gen.ato import Package -from gen.flights.flight import Flight +from gen.flights.flight import Flight, FlightType from gen.flights.flightplan import FlightPlanBuilder -from qt_ui.windows.mission.flight.generator.QCAPMissionGenerator import QCAPMissionGenerator -from qt_ui.windows.mission.flight.generator.QCASMissionGenerator import QCASMissionGenerator -from qt_ui.windows.mission.flight.generator.QSEADMissionGenerator import QSEADMissionGenerator -from qt_ui.windows.mission.flight.generator.QSTRIKEMissionGenerator import QSTRIKEMissionGenerator -from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import QFlightWaypointList -from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import QPredefinedWaypointSelectionWindow +from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \ + QFlightWaypointList +from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import \ + QPredefinedWaypointSelectionWindow +from theater import ControlPoint, FrontLine class QFlightWaypointTab(QFrame): @@ -23,56 +31,68 @@ class QFlightWaypointTab(QFrame): self.package = package self.flight = flight self.planner = FlightPlanBuilder(self.game, package, is_player=True) + + self.flight_waypoint_list: Optional[QFlightWaypointList] = None + self.ascend_waypoint: Optional[QPushButton] = None + self.descend_waypoint: Optional[QPushButton] = None + self.rtb_waypoint: Optional[QPushButton] = None + self.delete_selected: Optional[QPushButton] = None + self.open_fast_waypoint_button: Optional[QPushButton] = None + self.recreate_buttons: List[QPushButton] = [] self.init_ui() def init_ui(self): layout = QGridLayout() - rlayout = QVBoxLayout() + self.flight_waypoint_list = QFlightWaypointList(self.flight) - self.open_fast_waypoint_button = QPushButton("Add Waypoint") - self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) - - self.cas_generator = QPushButton("Gen. CAS") - self.cas_generator.clicked.connect(self.on_cas_generator) - - self.cap_generator = QPushButton("Gen. CAP") - self.cap_generator.clicked.connect(self.on_cap_generator) - - self.sead_generator = QPushButton("Gen. SEAD/DEAD") - self.sead_generator.clicked.connect(self.on_sead_generator) - - self.strike_generator = QPushButton("Gen. STRIKE") - self.strike_generator.clicked.connect(self.on_strike_generator) - - self.rtb_waypoint = QPushButton("Add RTB Waypoint") - self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) - - self.ascend_waypoint = QPushButton("Add Ascend Waypoint") - self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) - - self.descend_waypoint = QPushButton("Add Descend Waypoint") - self.descend_waypoint.clicked.connect(self.on_descend_waypoint) - - self.delete_selected = QPushButton("Delete Selected") - self.delete_selected.clicked.connect(self.on_delete_waypoint) - layout.addWidget(self.flight_waypoint_list, 0, 0) + rlayout = QVBoxLayout() + layout.addLayout(rlayout, 0, 1) + rlayout.addWidget(QLabel("Generator :")) rlayout.addWidget(QLabel("AI compatible")) - rlayout.addWidget(self.cas_generator) - rlayout.addWidget(self.cap_generator) - rlayout.addWidget(self.sead_generator) - rlayout.addWidget(self.strike_generator) + + self.recreate_buttons.clear() + recreate_types = [ + FlightType.CAS, + FlightType.CAP, + FlightType.SEAD, + FlightType.STRIKE + ] + for task in recreate_types: + def make_closure(arg): + def closure(): + return self.confirm_recreate(arg) + return closure + button = QPushButton(f"Recreate as {task.name}") + button.clicked.connect(make_closure(task)) + rlayout.addWidget(button) + self.recreate_buttons.append(button) + rlayout.addWidget(QLabel("Advanced : ")) rlayout.addWidget(QLabel("Do not use for AI flights")) + + self.ascend_waypoint = QPushButton("Add Ascend Waypoint") + self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint) rlayout.addWidget(self.ascend_waypoint) + + self.descend_waypoint = QPushButton("Add Descend Waypoint") + self.descend_waypoint.clicked.connect(self.on_descend_waypoint) rlayout.addWidget(self.descend_waypoint) + + self.rtb_waypoint = QPushButton("Add RTB Waypoint") + self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint) rlayout.addWidget(self.rtb_waypoint) - rlayout.addWidget(self.open_fast_waypoint_button) + + self.delete_selected = QPushButton("Delete Selected") + self.delete_selected.clicked.connect(self.on_delete_waypoint) rlayout.addWidget(self.delete_selected) + + self.open_fast_waypoint_button = QPushButton("Add Waypoint") + self.open_fast_waypoint_button.clicked.connect(self.on_fast_waypoint) + rlayout.addWidget(self.open_fast_waypoint_button) rlayout.addStretch() - layout.addLayout(rlayout, 0, 1) self.setLayout(layout) def on_delete_waypoint(self): @@ -105,45 +125,27 @@ class QFlightWaypointTab(QFrame): self.flight_waypoint_list.update_list() self.on_change() - def on_cas_generator(self): - self.subwindow = QCASMissionGenerator( - self.game, - self.package, - self.flight, - self.flight_waypoint_list + def confirm_recreate(self, task: FlightType) -> None: + result = QMessageBox.question( + self, + "Regenerate flight?", + ("Changing the flight type will reset its flight plan. Do you want " + "to continue?"), + QMessageBox.No, + QMessageBox.Yes ) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() - - def on_cap_generator(self): - 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.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.package, - self.flight, - self.flight_waypoint_list - ) - self.subwindow.finished.connect(self.on_change) - self.subwindow.show() + if result == QMessageBox.Yes: + # TODO: These should all be just CAP. + if task == FlightType.CAP: + if isinstance(self.package.target, FrontLine): + task = FlightType.TARCAP + elif isinstance(self.package.target, ControlPoint): + if self.package.target.is_fleet: + task = FlightType.BARCAP + self.flight.flight_type = task + self.planner.populate_flight_plan(self.flight) + self.flight_waypoint_list.update_list() + self.on_change() def on_change(self): self.flight_waypoint_list.update_list() From e27625556cfce7f28dafe132386e0cc21c36aab4 Mon Sep 17 00:00:00 2001 From: Khopa Date: Sun, 4 Oct 2020 22:09:57 +0200 Subject: [PATCH 27/48] Exported existing campaigns to json objects. --- resources/campaigns/battle_of_britain.json | 82 ++++++++ resources/campaigns/desert_war.json | 58 ++++++ resources/campaigns/dunkirk.json | 72 +++++++ resources/campaigns/emirates.json | 103 ++++++++++ resources/campaigns/full_map.json | 180 ++++++++++++++++++ resources/campaigns/golan_heights_battle.json | 78 ++++++++ resources/campaigns/inherent_resolve.json | 78 ++++++++ resources/campaigns/invasion_from_turkey.json | 84 ++++++++ resources/campaigns/invasion_of_iran.json | 140 ++++++++++++++ .../campaigns/invasion_of_iran_[lite].json | 74 +++++++ resources/campaigns/normandy.json | 82 ++++++++ resources/campaigns/normandy_small.json | 56 ++++++ resources/campaigns/north_caucasus.json | 96 ++++++++++ resources/campaigns/north_nevada.json | 70 +++++++ resources/campaigns/russia_small.json | 36 ++++ resources/campaigns/syrian_civil_war.json | 88 +++++++++ resources/campaigns/western_georgia.json | 108 +++++++++++ 17 files changed, 1485 insertions(+) create mode 100644 resources/campaigns/battle_of_britain.json create mode 100644 resources/campaigns/desert_war.json create mode 100644 resources/campaigns/dunkirk.json create mode 100644 resources/campaigns/emirates.json create mode 100644 resources/campaigns/full_map.json create mode 100644 resources/campaigns/golan_heights_battle.json create mode 100644 resources/campaigns/inherent_resolve.json create mode 100644 resources/campaigns/invasion_from_turkey.json create mode 100644 resources/campaigns/invasion_of_iran.json create mode 100644 resources/campaigns/invasion_of_iran_[lite].json create mode 100644 resources/campaigns/normandy.json create mode 100644 resources/campaigns/normandy_small.json create mode 100644 resources/campaigns/north_caucasus.json create mode 100644 resources/campaigns/north_nevada.json create mode 100644 resources/campaigns/russia_small.json create mode 100644 resources/campaigns/syrian_civil_war.json create mode 100644 resources/campaigns/western_georgia.json diff --git a/resources/campaigns/battle_of_britain.json b/resources/campaigns/battle_of_britain.json new file mode 100644 index 00000000..388208dc --- /dev/null +++ b/resources/campaigns/battle_of_britain.json @@ -0,0 +1,82 @@ +{ + "name": "The Channel - Battle of Britain", + "theater": "The Channel", + "player_points": [ + { + "type": "airbase", + "id": "Hawkinge", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lympne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Manston", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "High Halden", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Dunkirk Mardyck", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Saint Omer Longuenesse", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Merville Calonne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Abbeville Drucat", + "size": 600, + "importance": 1 + } + ], + "links": [ + [ + "Hawkinge", + "Lympne" + ], + [ + "Hawkinge", + "Manston" + ], + [ + "High Halden", + "Lympne" + ], + [ + "Dunkirk Mardyck", + "Saint Omer Longuenesse" + ], + [ + "Merville Calonne", + "Saint Omer Longuenesse" + ], + [ + "Abbeville Drucat", + "Saint Omer Longuenesse" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json new file mode 100644 index 00000000..06e96a06 --- /dev/null +++ b/resources/campaigns/desert_war.json @@ -0,0 +1,58 @@ +{ + "name": "Persian Gulf - Desert War", + "theater": "Persian Gulf", + "player_points": [ + { + "type": "airbase", + "id": "Liwa Airbase", + "size": 2000, + "importance": 1.2 + }, + { + "type": "lha", + "id": 1002, + "x": -164000, + "y": -257000 + }, + { + "type": "carrier", + "id": 1001, + "x": -124000, + "y": -303000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Al Ain International Airport", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Maktoum Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Minhad AB", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Al Ain International Airport", + "Liwa Airbase" + ], + [ + "Al Ain International Airport", + "Al Maktoum Intl" + ], + [ + "Al Maktoum Intl", + "Al Minhad AB" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json new file mode 100644 index 00000000..74417636 --- /dev/null +++ b/resources/campaigns/dunkirk.json @@ -0,0 +1,72 @@ +{ + "name": "The Channel - Dunkirk", + "theater": "The Channel", + "player_points": [ + { + "type": "airbase", + "id": "Hawkinge", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lympne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Manston", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Dunkirk Mardyck", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Saint Omer Longuenesse", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Merville Calonne", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Abbeville Drucat", + "size": 600, + "importance": 1 + } + ], + "links": [ + [ + "Hawkinge", + "Lympne" + ], + [ + "Hawkinge", + "Manston" + ], + [ + "Dunkirk Mardyck", + "Saint Omer Longuenesse" + ], + [ + "Merville Calonne", + "Saint Omer Longuenesse" + ], + [ + "Abbeville Drucat", + "Saint Omer Longuenesse" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/emirates.json b/resources/campaigns/emirates.json new file mode 100644 index 00000000..0453a489 --- /dev/null +++ b/resources/campaigns/emirates.json @@ -0,0 +1,103 @@ +{ + "name": "Persian Gulf - Emirates", + "theater": "Persian Gulf", + "player_points": [ + { + "type": "airbase", + "id": "Fujairah Intl", + "radials": [ + 180, + 225, + 270, + 315, + 0 + ], + "size": 1000, + "importance": 1 + }, + { + "type": "lha", + "id": 1002, + "x": -79770, + "y": 49430 + }, + { + "type": "carrier", + "id": 1001, + "x": -61770, + "y": 69039 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Al Dhafra AB", + "size": 2000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Ain International Airport", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Maktoum Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al Minhad AB", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sharjah Intl", + "size": 2000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Ras Al Khaimah", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Al Ain International Airport", + "Al Dhafra AB" + ], + [ + "Al Dhafra AB", + "Al Maktoum Intl" + ], + [ + "Al Ain International Airport", + "Fujairah Intl" + ], + [ + "Al Ain International Airport", + "Al Maktoum Intl" + ], + [ + "Al Maktoum Intl", + "Al Minhad AB" + ], + [ + "Al Minhad AB", + "Sharjah Intl" + ], + [ + "Ras Al Khaimah", + "Sharjah Intl" + ], + [ + "Fujairah Intl", + "Sharjah Intl" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/full_map.json b/resources/campaigns/full_map.json new file mode 100644 index 00000000..73ce78c0 --- /dev/null +++ b/resources/campaigns/full_map.json @@ -0,0 +1,180 @@ +{ + "name": "Syria - Full Map", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -151000, + "y": -106000 + }, + { + "type": "lha", + "id": 1002, + "x": -131000, + "y": -161000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Rene Mouawad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Minakh", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Hatay", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4 + } + ], + "links": [ + [ + "King Hussein Air College", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Al-Dumayr", + "Khalkhalah" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Palmyra" + ], + [ + "Al Qusayr", + "Rene Mouawad" + ], + [ + "Bassel Al-Assad", + "Rene Mouawad" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Bassel Al-Assad", + "Hatay" + ], + [ + "Palmyra", + "Tabqa" + ], + [ + "Jirah", + "Tabqa" + ], + [ + "Aleppo", + "Jirah" + ], + [ + "Aleppo", + "Minakh" + ], + [ + "Hatay", + "Minakh" + ], + [ + "Incirlik", + "Minakh" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/golan_heights_battle.json b/resources/campaigns/golan_heights_battle.json new file mode 100644 index 00000000..4f109c9d --- /dev/null +++ b/resources/campaigns/golan_heights_battle.json @@ -0,0 +1,78 @@ +{ + "name": "Syria - Golan heights battle", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Ramat David", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -280000, + "y": -238000 + }, + { + "type": "lha", + "id": 1002, + "x": -237000, + "y": -89800 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Mezzeh", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + } + ], + "links": [ + [ + "Khalkhalah", + "Ramat David" + ], + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Khalkhalah", + "Marj Ruhayyil" + ], + [ + "Marj Ruhayyil", + "Mezzeh" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json new file mode 100644 index 00000000..4882be93 --- /dev/null +++ b/resources/campaigns/inherent_resolve.json @@ -0,0 +1,78 @@ +{ + "name": "Syria - Inherent Resolve", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "King Hussein Air College", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": -210000, + "y": -200000 + }, + { + "type": "lha", + "id": 1002, + "x": -131000, + "y": -161000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Khalkhalah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Khalkhalah", + "King Hussein Air College" + ], + [ + "Incirlik", + "Incirlik" + ], + [ + "Khalkhalah", + "Palmyra" + ], + [ + "Palmyra", + "Tabqa" + ], + [ + "Jirah", + "Tabqa" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json new file mode 100644 index 00000000..d380582d --- /dev/null +++ b/resources/campaigns/invasion_from_turkey.json @@ -0,0 +1,84 @@ +{ + "name": "Syria - Invasion from Turkey", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Incirlik", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Hatay", + "size": 1000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 133000, + "y": -54000 + }, + { + "type": "lha", + "id": 1002, + "x": 155000, + "y": -19000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Minakh", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Kuweires", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Jirah", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Tabqa", + "size": 1000, + "importance": 1 + } + ], + "links": [ + [ + "Hatay", + "Minakh" + ], + [ + "Aleppo", + "Minakh" + ], + [ + "Aleppo", + "Kuweires" + ], + [ + "Jirah", + "Kuweires" + ], + [ + "Jirah", + "Tabqa" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json new file mode 100644 index 00000000..87227808 --- /dev/null +++ b/resources/campaigns/invasion_of_iran.json @@ -0,0 +1,140 @@ +{ + "name": "Persian Gulf - Invasion of Iran", + "theater": "Persian Gulf", + "player_points": [ + { + "type": "airbase", + "id": "Ras Al Khaimah", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Khasab", + "size": 600, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Qeshm Island", + "radials": [ + 270, + 315, + 0, + 45, + 90, + 135, + 180 + ], + "size": 600, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Havadarya", + "radials": [ + 225, + 270, + 315, + 0, + 45 + ], + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Bandar Abbas Intl", + "size": 2000, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 59514.324335475, + "y": 28165.517980635 + }, + { + "type": "lha", + "id": 1002, + "x": -27500.813952358, + "y": -147000.65947136 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Bandar Lengeh", + "radials": [ + 270, + 315, + 0, + 45 + ], + "size": 600, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Shiraz International Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Jiroft Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Kerman Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Lar Airbase", + "size": 1000, + "importance": 1.4 + } + ], + "links": [ + [ + "Khasab", + "Ras Al Khaimah" + ], + [ + "Bandar Lengeh", + "Lar Airbase" + ], + [ + "Havadarya", + "Lar Airbase" + ], + [ + "Bandar Abbas Intl", + "Havadarya" + ], + [ + "Bandar Abbas Intl", + "Jiroft Airport" + ], + [ + "Lar Airbase", + "Shiraz International Airport" + ], + [ + "Kerman Airport", + "Shiraz International Airport" + ], + [ + "Jiroft Airport", + "Kerman Airport" + ], + [ + "Kerman Airport", + "Lar Airbase" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json new file mode 100644 index 00000000..8bc8ff4f --- /dev/null +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -0,0 +1,74 @@ +{ + "name": "Persian Gulf - Invasion of Iran [Lite]", + "theater": "Persian Gulf", + "player_points": [ + { + "type": "airbase", + "id": "Bandar Lengeh", + "radials": [ + 270, + 315, + 0, + 45 + ], + "size": 600, + "importance": 1.4 + }, + { + "type": "carrier", + "id": 1001, + "x": 72000.324335475, + "y": -376000 + }, + { + "type": "lha", + "id": 1002, + "x": -27500.813952358, + "y": -147000.65947136 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Shiraz International Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Jiroft Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Kerman Airport", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Lar Airbase", + "size": 1000, + "importance": 1.4 + } + ], + "links": [ + [ + "Bandar Lengeh", + "Lar Airbase" + ], + [ + "Lar Airbase", + "Shiraz International Airport" + ], + [ + "Kerman Airport", + "Shiraz International Airport" + ], + [ + "Jiroft Airport", + "Kerman Airport" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json new file mode 100644 index 00000000..3aad62dd --- /dev/null +++ b/resources/campaigns/normandy.json @@ -0,0 +1,82 @@ +{ + "name": "Normandy - Normandy", + "theater": "Normandy", + "player_points": [ + { + "type": "airbase", + "id": "Chailey", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Needs Oar Point", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Deux Jumeaux", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Lignerolles", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lessay", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Carpiquet", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Maupertus", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Evreux", + "size": 600, + "importance": 1 + } + ], + "links": [ + [ + "Chailey", + "Needs Oar Point" + ], + [ + "Deux Jumeaux", + "Lignerolles" + ], + [ + "Lessay", + "Lignerolles" + ], + [ + "Carpiquet", + "Lignerolles" + ], + [ + "Lessay", + "Maupertus" + ], + [ + "Carpiquet", + "Evreux" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json new file mode 100644 index 00000000..7350e40c --- /dev/null +++ b/resources/campaigns/normandy_small.json @@ -0,0 +1,56 @@ +{ + "name": "Normandy - Normandy Small", + "theater": "Normandy", + "player_points": [ + { + "type": "airbase", + "id": "Needs Oar Point", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Deux Jumeaux", + "size": 600, + "importance": 1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Lignerolles", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Carpiquet", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Evreux", + "size": 600, + "importance": 1 + } + ], + "links": [ + [ + "Needs Oar Point", + "Needs Oar Point" + ], + [ + "Deux Jumeaux", + "Lignerolles" + ], + [ + "Carpiquet", + "Lignerolles" + ], + [ + "Carpiquet", + "Evreux" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_caucasus.json b/resources/campaigns/north_caucasus.json new file mode 100644 index 00000000..67f1a0ff --- /dev/null +++ b/resources/campaigns/north_caucasus.json @@ -0,0 +1,96 @@ +{ + "name": "Caucasus - North Caucasus", + "theater": "Caucasus", + "player_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Vaziani", + "size": 600, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875 + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Beslan", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Nalchik", + "size": 1000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mozdok", + "size": 2000, + "importance": 1.1 + }, + { + "type": "airbase", + "id": "Mineralnye Vody", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.4 + } + ], + "links": [ + [ + "Kutaisi", + "Vaziani" + ], + [ + "Beslan", + "Vaziani" + ], + [ + "Beslan", + "Mozdok" + ], + [ + "Beslan", + "Nalchik" + ], + [ + "Mozdok", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Nalchik" + ], + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/north_nevada.json b/resources/campaigns/north_nevada.json new file mode 100644 index 00000000..e1ddd17d --- /dev/null +++ b/resources/campaigns/north_nevada.json @@ -0,0 +1,70 @@ +{ + "name": "Nevada - North Nevada", + "theater": "Nevada", + "player_points": [ + { + "type": "airbase", + "id": "Nellis AFB", + "size": 2000, + "importance": 1.4 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Tonopah Test Range Airfield", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Lincoln County", + "size": 600, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Groom Lake AFB", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Creech AFB", + "size": 2000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Mesquite", + "size": 1000, + "importance": 1.3 + } + ], + "links": [ + [ + "Lincoln County", + "Tonopah Test Range Airfield" + ], + [ + "Groom Lake AFB", + "Tonopah Test Range Airfield" + ], + [ + "Lincoln County", + "Mesquite" + ], + [ + "Groom Lake AFB", + "Mesquite" + ], + [ + "Creech AFB", + "Groom Lake AFB" + ], + [ + "Creech AFB", + "Nellis AFB" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json new file mode 100644 index 00000000..207ff7fe --- /dev/null +++ b/resources/campaigns/russia_small.json @@ -0,0 +1,36 @@ +{ + "name": "Caucasus - Russia Small", + "theater": "Caucasus", + "player_points": [ + { + "type": "airbase", + "id": "Mozdok", + "size": 2000, + "importance": 1.1 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Mineralnye Vody", + "size": 2000, + "importance": 1.3 + }, + { + "type": "airbase", + "id": "Maykop-Khanskaya", + "size": 3000, + "importance": 1.4 + } + ], + "links": [ + [ + "Mineralnye Vody", + "Mozdok" + ], + [ + "Maykop-Khanskaya", + "Mineralnye Vody" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/syrian_civil_war.json b/resources/campaigns/syrian_civil_war.json new file mode 100644 index 00000000..18e999ce --- /dev/null +++ b/resources/campaigns/syrian_civil_war.json @@ -0,0 +1,88 @@ +{ + "name": "Syria - Syrian Civil War", + "theater": "Syria", + "player_points": [ + { + "type": "airbase", + "id": "Bassel Al-Assad", + "size": 1000, + "importance": 1.4 + }, + { + "type": "airbase", + "id": "Marj Ruhayyil", + "size": 1000, + "importance": 1 + }, + { + "type": "carrier", + "id": 1001, + "x": 18537, + "y": -52000 + }, + { + "type": "lha", + "id": 1002, + "x": 116000, + "y": -30000 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Hama", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Aleppo", + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Al Qusayr", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Palmyra", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Al-Dumayr", + "size": 1000, + "importance": 1.2 + } + ], + "links": [ + [ + "Bassel Al-Assad", + "Hama" + ], + [ + "Al-Dumayr", + "Marj Ruhayyil" + ], + [ + "Aleppo", + "Hama" + ], + [ + "Al Qusayr", + "Hama" + ], + [ + "Al Qusayr", + "Al-Dumayr" + ], + [ + "Al Qusayr", + "Palmyra" + ] + ] +} \ No newline at end of file diff --git a/resources/campaigns/western_georgia.json b/resources/campaigns/western_georgia.json new file mode 100644 index 00000000..c539cbf8 --- /dev/null +++ b/resources/campaigns/western_georgia.json @@ -0,0 +1,108 @@ +{ + "name": "Caucasus - Western Georgia", + "theater": "Caucasus", + "player_points": [ + { + "type": "airbase", + "id": "Kobuleti", + "radials": [ + 0, + 45, + 90, + 135, + 180, + 225, + 315 + ], + "size": 600, + "importance": 1.1 + }, + { + "type": "carrier", + "id": 1001, + "x": -285810.6875, + "y": 496399.1875 + }, + { + "type": "lha", + "id": 1002, + "x": -326050.6875, + "y": 519452.1875 + } + ], + "enemy_points": [ + { + "type": "airbase", + "id": "Kutaisi", + "size": 600, + "importance": 1 + }, + { + "type": "airbase", + "id": "Senaki-Kolkhi", + "size": 1000, + "importance": 1 + }, + { + "type": "airbase", + "id": "Sukhumi-Babushara", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Gudauta", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 1000, + "importance": 1.2 + }, + { + "type": "airbase", + "id": "Sochi-Adler", + "radials": [ + 315, + 0, + 45, + 90, + 135 + ], + "size": 2000, + "importance": 1.4 + } + ], + "links": [ + [ + "Kutaisi", + "Senaki-Kolkhi" + ], + [ + "Kobuleti", + "Senaki-Kolkhi" + ], + [ + "Senaki-Kolkhi", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sukhumi-Babushara" + ], + [ + "Gudauta", + "Sochi-Adler" + ] + ] +} \ No newline at end of file From f040804d0273fec544d61e60173678b79a25b5e7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 4 Oct 2020 12:48:35 -0700 Subject: [PATCH 28/48] Fix save issues after aborting mission. When the mission is aborted the pending mission is still in the event list, which is part of the game option. That event has a reference to the operation, which in turn contains all the mission generator objects. Two of these objects are the radio/TACAN allocators, which use a generator to track the next free channel. Generators cannot be picked, so because these are transitively part of the game object the game cannot be saved. Aside from the briefing generator, none of those objects are actually needed outside the generation function itself, so just make them locals instead. This probably needs a larger refactor at some point. It doesn't look like we need so many calls into the operation type (it has an initialize, a prepare, and a generate, and it doesn't seem to need anything but the last one). The only reason breifinggen needs to remain a part of the class is because the briefing title and description are filled in from the derived class, where title and description should probably be overridden properties instead. I'm also not sure if we need to make the event list a part of game at all, and also don't think that the mission needs to be one of these events. --- game/operation/operation.py | 98 ++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index c0ce5e67..b23c219e 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -68,26 +68,8 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.radio_registry = RadioRegistry() - self.tacan_registry = TacanRegistry() - self.airgen = AircraftConflictGenerator( - mission, conflict, self.game.settings, self.game, - self.radio_registry) - self.airsupportgen = AirSupportConflictGenerator( - mission, conflict, self.game, self.radio_registry, - self.tacan_registry) - self.triggersgen = TriggersGenerator(mission, conflict, self.game) - self.visualgen = VisualGenerator(mission, conflict, self.game) - self.envgen = EnviromentGenerator(mission, conflict, self.game) - self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator( - mission, - conflict, - self.game, - self.radio_registry, - self.tacan_registry - ) - self.briefinggen = BriefingGenerator(mission, conflict, self.game) + self.briefinggen = BriefingGenerator(self.current_mission, + self.conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): with open("resources/default_options.lua", "r") as f: @@ -127,6 +109,9 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): + radio_registry = RadioRegistry() + tacan_registry = TacanRegistry() + # Dedup beacon/radio frequencies, since some maps have some frequencies # used multiple times. beacons = load_beacons_for_terrain(self.game.theater.terrain.name) @@ -138,7 +123,7 @@ class Operation: logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: - self.tacan_registry.reserve(beacon.tacan_channel) + tacan_registry.reserve(beacon.tacan_channel) for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: @@ -150,16 +135,26 @@ class Operation: # beacon list. for frequency in unique_map_frequencies: - self.radio_registry.reserve(frequency) + radio_registry.reserve(frequency) # Generate meteo + envgen = EnviromentGenerator(self.current_mission, self.conflict, + self.game) if self.environment_settings is None: - self.environment_settings = self.envgen.generate() + self.environment_settings = envgen.generate() else: - self.envgen.load(self.environment_settings) + envgen.load(self.environment_settings) # Generate ground object first - self.groundobjectgen.generate() + + groundobjectgen = GroundObjectsGenerator( + self.current_mission, + self.conflict, + self.game, + radio_registry, + tacan_registry + ) + groundobjectgen.generate() # Generate destroyed units for d in self.game.get_destroyed_units(): @@ -180,11 +175,16 @@ class Operation: dead=True, ) - # Air Support (Tanker & Awacs) - self.airsupportgen.generate(self.is_awacs_enabled) + airsupportgen = AirSupportConflictGenerator( + self.current_mission, self.conflict, self.game, radio_registry, + tacan_registry) + airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map + airgen = AircraftConflictGenerator( + self.current_mission, self.conflict, self.game.settings, self.game, + radio_registry) for cp in self.game.theater.controlpoints: side = cp.captured if side: @@ -192,11 +192,11 @@ class Operation: else: country = self.current_mission.country(self.game.enemy_country) if cp.id in self.game.planners.keys(): - self.airgen.generate_flights( + airgen.generate_flights( cp, country, self.game.planners[cp.id], - self.groundobjectgen.runways + groundobjectgen.runways ) # Generate ground units on frontline everywhere @@ -221,18 +221,20 @@ class Operation: self.current_mission.groundControl.red_tactical_commander = self.ca_slots # Triggers - if self.game.is_player_attack(self.conflict.attackers_country): - cp = self.conflict.from_cp - else: - cp = self.conflict.to_cp - self.triggersgen.generate() + triggersgen = TriggersGenerator(self.current_mission, self.conflict, + self.game) + triggersgen.generate() # Options - self.forcedoptionsgen.generate() + forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, + self.conflict, self.game) + forcedoptionsgen.generate() # Generate Visuals Smoke Effects + visualgen = VisualGenerator(self.current_mission, self.conflict, + self.game) if self.game.settings.perf_smoke_gen: - self.visualgen.generate() + visualgen.generate() # Inject Plugins Lua Scripts listOfPluginsScripts = [] @@ -327,19 +329,20 @@ class Operation: trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - self.assign_channels_to_flights() + self.assign_channels_to_flights(airgen.flights, + airsupportgen.air_support) kneeboard_generator = KneeboardGenerator(self.current_mission) - for dynamic_runway in self.groundobjectgen.runways.values(): + for dynamic_runway in groundobjectgen.runways.values(): self.briefinggen.add_dynamic_runway(dynamic_runway) - for tanker in self.airsupportgen.air_support.tankers: + for tanker in airsupportgen.air_support.tankers: self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - for awacs in self.airsupportgen.air_support.awacs: + for awacs in airsupportgen.air_support.awacs: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) @@ -347,21 +350,23 @@ class Operation: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) - for flight in self.airgen.flights: + for flight in airgen.flights: self.briefinggen.add_flight(flight) kneeboard_generator.add_flight(flight) self.briefinggen.generate() kneeboard_generator.generate() - def assign_channels_to_flights(self) -> None: + def assign_channels_to_flights(self, flights: List[FlightData], + air_support: AirSupport) -> None: """Assigns preset radio channels for client flights.""" - for flight in self.airgen.flights: + for flight in flights: if not flight.client_units: continue - self.assign_channels_to_flight(flight) + self.assign_channels_to_flight(flight, air_support) - def assign_channels_to_flight(self, flight: FlightData) -> None: + def assign_channels_to_flight(self, flight: FlightData, + air_support: AirSupport) -> None: """Assigns preset radio channels for a client flight.""" airframe = flight.aircraft_type @@ -372,4 +377,5 @@ class Operation: return aircraft_data.channel_allocator.assign_channels_for_flight( - flight, self.airsupportgen.air_support) + flight, air_support + ) From 71f77dd8fb761ff66db8acfbfc90230ebc29f429 Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 00:09:11 +0200 Subject: [PATCH 29/48] Added moddable campaigns through json files. --- qt_ui/uiconstants.py | 4 +- qt_ui/windows/newgame/QCampaignList.py | 49 ++-- qt_ui/windows/newgame/QNewGameWizard.py | 34 +-- theater/caucasus.py | 212 ----------------- theater/conflicttheater.py | 160 ++++++++++++- theater/nevada.py | 42 ---- theater/normandy.py | 83 ------- theater/persiangulf.py | 298 ------------------------ theater/syria.py | 225 ------------------ theater/thechannel.py | 109 --------- 10 files changed, 185 insertions(+), 1031 deletions(-) delete mode 100644 theater/caucasus.py delete mode 100644 theater/nevada.py delete mode 100644 theater/normandy.py delete mode 100644 theater/persiangulf.py delete mode 100644 theater/syria.py delete mode 100644 theater/thechannel.py diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 30f61ef1..f822b0fc 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -85,10 +85,10 @@ def load_icons(): ICONS["Hangar"] = QPixmap("./resources/ui/misc/hangar.png") ICONS["Terrain_Caucasus"] = QPixmap("./resources/ui/terrain_caucasus.gif") - ICONS["Terrain_Persian_Gulf"] = QPixmap("./resources/ui/terrain_pg.gif") + ICONS["Terrain_PersianGulf"] = QPixmap("./resources/ui/terrain_pg.gif") ICONS["Terrain_Nevada"] = QPixmap("./resources/ui/terrain_nevada.gif") ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif") - ICONS["Terrain_Channel"] = QPixmap("./resources/ui/terrain_channel.gif") + ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif") ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif") ICONS["Dawn"] = QPixmap("./resources/ui/daytime/dawn.png") diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 06c42d9f..e83fae46 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -1,41 +1,38 @@ +import json +import logging +import os + from PySide2 import QtGui from PySide2.QtCore import QSize, QItemSelectionModel from PySide2.QtGui import QStandardItemModel, QStandardItem from PySide2.QtWidgets import QListView, QAbstractItemView -from theater import caucasus, nevada, persiangulf, normandy, thechannel, syria +from theater import caucasus, nevada, persiangulf, normandy, thechannel, syria, ConflictTheater import qt_ui.uiconstants as CONST -CAMPAIGNS = [ - ("Caucasus - Western Georgia", caucasus.WesternGeorgia, "Terrain_Caucasus"), - ("Caucasus - Russia Small", caucasus.RussiaSmall, "Terrain_Caucasus"), - ("Caucasus - North Caucasus", caucasus.NorthCaucasus, "Terrain_Caucasus"), - ("Caucasus - Full Map", caucasus.CaucasusTheater, "Terrain_Caucasus"), - ("Nevada - North Nevada", nevada.NevadaTheater, "Terrain_Nevada"), - ("Persian Gulf - Invasion of Iran", persiangulf.IranianCampaign, "Terrain_Persian_Gulf"), - ("Persian Gulf - Invasion of Iran [Lite]", persiangulf.IranInvasionLite, "Terrain_Persian_Gulf"), - ("Persian Gulf - Emirates", persiangulf.Emirates, "Terrain_Persian_Gulf"), - ("Persian Gulf - Desert War", persiangulf.DesertWar, "Terrain_Persian_Gulf"), - ("Persian Gulf - Full Map", persiangulf.PersianGulfTheater, "Terrain_Persian_Gulf"), - - ("Syria - Golan heights battle", syria.GolanHeights, "Terrain_Syria"), - ("Syria - Invasion from Turkey", syria.TurkishInvasion, "Terrain_Syria"), - ("Syria - Syrian Civil War", syria.SyrianCivilWar, "Terrain_Syria"), - ("Syria - Inherent Resolve", syria.InherentResolve, "Terrain_Syria"), - ("Syria - Full Map", syria.SyriaFullMap, "Terrain_Syria"), - - ("Normandy - Normandy", normandy.NormandyTheater, "Terrain_Normandy"), - ("Normandy - Normandy Small", normandy.NormandySmall, "Terrain_Normandy"), - ("The Channel - Battle of Britain", thechannel.BattleOfBritain, "Terrain_Channel"), - ("The Channel - Dunkirk", thechannel.Dunkirk, "Terrain_Channel"), -] +CAMPAIGN_DIR = ".\\resources\\campaigns" +CAMPAIGNS = [] +# Load the campaigns files from the directory +campaign_files = os.listdir(CAMPAIGN_DIR) +for f in campaign_files: + try: + ff = os.path.join(CAMPAIGN_DIR, f) + with open(ff, "r") as campaign_data: + data = json.load(campaign_data) + choice = (data["name"], ff, "Terrain_" + data["theater"].replace(" ", "")) + logging.info("Loaded campaign : " + data["name"]) + CAMPAIGNS.append(choice) + ConflictTheater.from_file(choice[1]) + logging.info("Loaded campaign :" + ff) + except Exception as e: + logging.info("Unable to load campaign :" + f) class QCampaignItem(QStandardItem): - def __init__(self, text, theater, icon): + def __init__(self, text, filename, icon): super(QCampaignItem, self).__init__() - self.theater = theater + self.filename = filename self.setIcon(QtGui.QIcon(CONST.ICONS[icon])) self.setEditable(False) self.setText(text) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index cba58371..9b82f14f 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -44,7 +44,8 @@ class NewGameWizard(QtWidgets.QWizard): selectedCampaign = self.field("selectedCampaign") if selectedCampaign is None: selectedCampaign = CAMPAIGNS[0] - conflictTheater = selectedCampaign[1]() + + conflictTheater = ConflictTheater.from_file(selectedCampaign[1]) timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] midGame = self.field("midGame") @@ -242,35 +243,6 @@ class TheaterConfiguration(QtWidgets.QWizardPage): self.setPixmap(QtWidgets.QWizard.WatermarkPixmap, QtGui.QPixmap('./resources/ui/wizard/watermark3.png')) - # Terrain selection - terrainGroup = QtWidgets.QGroupBox("Terrain") - terrainCaucasusSmall = QtWidgets.QRadioButton("Caucasus - Western Georgia") - terrainCaucasusSmall.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainRussia = QtWidgets.QRadioButton("Caucasus - Russia Small") - terrainRussia.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainCaucasus = QtWidgets.QRadioButton("Caucasus - Full map [NOT RECOMMENDED]") - terrainCaucasus.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - terrainCaucasusNorth = QtWidgets.QRadioButton("Caucasus - North") - terrainCaucasusNorth.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Caucasus"])) - - terrainPg = QtWidgets.QRadioButton("Persian Gulf - Full Map [NOT RECOMMENDED]") - terrainPg.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainIran = QtWidgets.QRadioButton("Persian Gulf - Invasion of Iran") - terrainIran.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainEmirates = QtWidgets.QRadioButton("Persian Gulf - Emirates") - terrainEmirates.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Persian_Gulf"])) - terrainNttr = QtWidgets.QRadioButton("Nevada - North Nevada") - terrainNttr.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Nevada"])) - terrainNormandy = QtWidgets.QRadioButton("Normandy") - terrainNormandy.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Normandy"])) - terrainNormandySmall = QtWidgets.QRadioButton("Normandy Small") - terrainNormandySmall.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Normandy"])) - terrainChannel = QtWidgets.QRadioButton("The Channel : Start in Dunkirk") - terrainChannel.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Channel"])) - terrainChannelComplete = QtWidgets.QRadioButton("The Channel : Battle of Britain") - terrainChannelComplete.setIcon(QtGui.QIcon(CONST.ICONS["Terrain_Channel"])) - terrainCaucasusSmall.setChecked(True) - # List of campaigns campaignList = QCampaignList() self.registerField("selectedCampaign", campaignList) @@ -284,8 +256,6 @@ class TheaterConfiguration(QtWidgets.QWizardPage): campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) on_campaign_selected() - - # Campaign settings mapSettingsGroup = QtWidgets.QGroupBox("Map Settings") invertMap = QtWidgets.QCheckBox() diff --git a/theater/caucasus.py b/theater/caucasus.py deleted file mode 100644 index 1ebad24d..00000000 --- a/theater/caucasus.py +++ /dev/null @@ -1,212 +0,0 @@ -from dcs import mapping -from dcs.terrain import caucasus - -from .conflicttheater import * -from .landmap import * - - -class CaucasusTheater(ConflictTheater): - terrain = caucasus.Caucasus() - overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5*4, 319*4), - (-355692.3067714, 617269.96285781): (263*4, 352*4), } - - landmap = load_landmap("resources\\caulandmap.p") - daytime_map = { - "dawn": (6, 9), - "day": (9, 18), - "dusk": (18, 20), - "night": (0, 5), - } - - - - def __init__(self, load_ground_objects=True): - super(CaucasusTheater, self).__init__() - - self.vaziani = ControlPoint.from_airport(caucasus.Vaziani, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.senaki = ControlPoint.from_airport(caucasus.Senaki_Kolkhi, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.kobuleti = ControlPoint.from_airport(caucasus.Kobuleti, COAST_A_E, SIZE_SMALL, 1.1) - self.batumi = ControlPoint.from_airport(caucasus.Batumi, COAST_DL_E, SIZE_SMALL, 1.3) - self.sukhumi = ControlPoint.from_airport(caucasus.Sukhumi_Babushara, COAST_DR_E, SIZE_REGULAR, 1.2) - self.gudauta = ControlPoint.from_airport(caucasus.Gudauta, COAST_DR_E, SIZE_REGULAR, 1.2) - self.sochi = ControlPoint.from_airport(caucasus.Sochi_Adler, COAST_DR_E, SIZE_BIG, IMPORTANCE_HIGH) - self.gelendzhik = ControlPoint.from_airport(caucasus.Gelendzhik, COAST_DR_E, SIZE_BIG, 1.1) - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.krasnodar = ControlPoint.from_airport(caucasus.Krasnodar_Center, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.krymsk = ControlPoint.from_airport(caucasus.Krymsk, LAND, SIZE_LARGE, 1.2) - self.anapa = ControlPoint.from_airport(caucasus.Anapa_Vityazevo, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.beslan = ControlPoint.from_airport(caucasus.Beslan, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.nalchik = ControlPoint.from_airport(caucasus.Nalchik, LAND, SIZE_REGULAR, 1.1) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-305810.6875, 406399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.vaziani.frontline_offset = 0.5 - self.vaziani.base.strength = 1 - - self.add_controlpoint(self.vaziani, connected_to=[self.kutaisi, self.beslan]) - self.add_controlpoint(self.beslan, connected_to=[self.vaziani, self.mozdok, self.nalchik]) - self.add_controlpoint(self.nalchik, connected_to=[self.beslan, self.mozdok, self.mineralnye]) - self.add_controlpoint(self.mozdok, connected_to=[self.nalchik, self.beslan, self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.nalchik, self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye, self.krasnodar]) - - self.add_controlpoint(self.kutaisi, connected_to=[self.vaziani, self.senaki]) - self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi, self.kutaisi]) - self.add_controlpoint(self.kobuleti, connected_to=[self.batumi, self.senaki]) - self.add_controlpoint(self.batumi, connected_to=[self.kobuleti]) - self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta, self.senaki]) - self.add_controlpoint(self.gudauta, connected_to=[self.sochi, self.sukhumi]) - self.add_controlpoint(self.sochi, connected_to=[self.gudauta, self.gelendzhik]) - - self.add_controlpoint(self.gelendzhik, connected_to=[self.sochi, self.krymsk]) - self.add_controlpoint(self.krymsk, connected_to=[self.anapa, self.krasnodar, self.gelendzhik]) - self.add_controlpoint(self.anapa, connected_to=[self.krymsk]) - self.add_controlpoint(self.krasnodar, connected_to=[self.krymsk, self.maykop]) - - self.add_controlpoint(self.carrier_1) - self.add_controlpoint(self.lha) - - self.carrier_1.captured = True - self.carrier_1.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - - self.batumi.captured = True - self.anapa.captured_invert = True - - -""" -A smaller version of the caucasus map in western georgia. -Ideal for smaller scale campaign -""" -class WesternGeorgia(ConflictTheater): - - terrain = caucasus.Caucasus() - overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), - (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } - landmap = load_landmap("resources\\caulandmap.p") - daytime_map = { - "dawn": (6, 9), - "day": (9, 18), - "dusk": (18, 20), - "night": (0, 5), - } - - - def __init__(self, load_ground_objects=True): - super(WesternGeorgia, self).__init__() - - self.kobuleti = ControlPoint.from_airport(caucasus.Kobuleti, COAST_A_E, SIZE_SMALL, 1.1) - self.senaki = ControlPoint.from_airport(caucasus.Senaki_Kolkhi, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.sukhumi = ControlPoint.from_airport(caucasus.Sukhumi_Babushara, COAST_DR_E, SIZE_REGULAR, 1.2) - self.gudauta = ControlPoint.from_airport(caucasus.Gudauta, COAST_DR_E, SIZE_REGULAR, 1.2) - self.sochi = ControlPoint.from_airport(caucasus.Sochi_Adler, COAST_DR_E, SIZE_BIG, IMPORTANCE_HIGH) - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-285810.6875, 496399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.add_controlpoint(self.kutaisi, connected_to=[self.senaki]) - self.add_controlpoint(self.senaki, connected_to=[self.kobuleti, self.sukhumi, self.kutaisi]) - self.add_controlpoint(self.kobuleti, connected_to=[self.senaki]) - self.add_controlpoint(self.sukhumi, connected_to=[self.gudauta, self.senaki]) - self.add_controlpoint(self.gudauta, connected_to=[self.sochi, self.sukhumi]) - self.add_controlpoint(self.sochi, connected_to=[self.gudauta]) - self.add_controlpoint(self.carrier_1) - self.add_controlpoint(self.lha) - - self.carrier_1.captured = True - self.carrier_1.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - self.kobuleti.captured = True - self.sochi.captured_invert = True - - -""" -Georgian Theather [inverted starting position] -Ideal for smaller scale campaign -""" -class RussiaSmall(ConflictTheater): - terrain = caucasus.Caucasus() - overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), - (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } - - landmap = load_landmap("resources\\caulandmap.p") - daytime_map = { - "dawn": (6, 9), - "day": (9, 18), - "dusk": (18, 20), - "night": (0, 5), - } - - def __init__(self, load_ground_objects=True): - super(RussiaSmall, self).__init__() - - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - - self.add_controlpoint(self.mozdok, connected_to=[self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye]) - - self.mozdok.captured = True - self.maykop.captured_invert = True - - -class NorthCaucasus(ConflictTheater): - terrain = caucasus.Caucasus() - overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5*4, 319*4), - (-355692.3067714, 617269.96285781): (263*4, 352*4), } - - landmap = load_landmap("resources\\caulandmap.p") - daytime_map = { - "dawn": (6, 9), - "day": (9, 18), - "dusk": (18, 20), - "night": (0, 5), - } - - def __init__(self, load_ground_objects=True): - super(NorthCaucasus, self).__init__() - - self.kutaisi = ControlPoint.from_airport(caucasus.Kutaisi, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.vaziani = ControlPoint.from_airport(caucasus.Vaziani, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.maykop = ControlPoint.from_airport(caucasus.Maykop_Khanskaya, LAND, SIZE_LARGE, IMPORTANCE_HIGH) - self.beslan = ControlPoint.from_airport(caucasus.Beslan, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.nalchik = ControlPoint.from_airport(caucasus.Nalchik, LAND, SIZE_REGULAR, 1.1) - self.mineralnye = ControlPoint.from_airport(caucasus.Mineralnye_Vody, LAND, SIZE_BIG, 1.3) - self.mozdok = ControlPoint.from_airport(caucasus.Mozdok, LAND, SIZE_BIG, 1.1) - self.carrier_1 = ControlPoint.carrier("Carrier", mapping.Point(-285810.6875, 496399.1875), 1001) - self.lha = ControlPoint.lha("Tarawa", mapping.Point(-326050.6875, 519452.1875), 1002) - - self.vaziani.frontline_offset = 0.5 - self.vaziani.base.strength = 1 - - self.add_controlpoint(self.kutaisi, connected_to=[self.vaziani]) - self.add_controlpoint(self.vaziani, connected_to=[self.beslan, self.kutaisi]) - self.add_controlpoint(self.beslan, connected_to=[self.vaziani, self.mozdok, self.nalchik]) - self.add_controlpoint(self.nalchik, connected_to=[self.beslan, self.mozdok, self.mineralnye]) - self.add_controlpoint(self.mozdok, connected_to=[self.nalchik, self.beslan, self.mineralnye]) - self.add_controlpoint(self.mineralnye, connected_to=[self.nalchik, self.mozdok, self.maykop]) - self.add_controlpoint(self.maykop, connected_to=[self.mineralnye]) - self.add_controlpoint(self.carrier_1, connected_to=[]) - self.add_controlpoint(self.lha, connected_to=[]) - - self.carrier_1.captured = True - self.vaziani.captured = True - self.kutaisi.captured = True - - self.carrier_1.captured_invert = True - self.maykop.captured_invert = True - self.lha.captured = True - self.lha.captured_invert = True - self.mineralnye.captured_invert = True diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index b796ed3e..e21a4e6d 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,10 +1,12 @@ +import json import typing import dcs from dcs.mapping import Point +from dcs.terrain import caucasus, persiangulf, nevada, normandy, thechannel, syria from .controlpoint import ControlPoint -from .landmap import poly_contains +from .landmap import poly_contains, load_landmap SIZE_TINY = 150 SIZE_SMALL = 600 @@ -32,7 +34,7 @@ LAND = [0, 45, 90, 135, 180, 225, 270, 315, ] COAST_V_E = [0, 45, 90, 135, 180] COAST_V_W = [180, 225, 270, 315, 0] -COAST_A_W = [315, 0, 45, 135, 180, 225, 270] +COAST_A_W = [315, 0, 45, 135, 180, 225, 270] COAST_A_E = [0, 45, 90, 135, 180, 225, 315] COAST_H_N = [270, 315, 0, 45, 90] @@ -117,4 +119,158 @@ class ConflictTheater: def enemy_points(self) -> typing.Collection[ControlPoint]: return [point for point in self.controlpoints if not point.captured] + def add_json_cp(self, theater, p: dict) -> ControlPoint: + if p["type"] == "airbase": + + airbase = theater.terrain.airports[p["id"]].__class__ + + if "radials" in p.keys(): + radials = p["radials"] + else: + radials = LAND + + if "size" in p.keys(): + size = p["size"] + else: + size = SIZE_REGULAR + + if "importance" in p.keys(): + importance = p["importance"] + else: + importance = IMPORTANCE_MEDIUM + + cp = ControlPoint.from_airport(airbase, radials, size, importance) + elif p["type"] == "carrier": + cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"]) + else: + cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"]) + + if "captured_invert" in p.keys(): + cp.captured_invert = p["captured_invert"] + else: + cp.captured_invert = False + + return cp + + @staticmethod + def from_file(filename): + with open(filename, "r") as content: + json_data = json.loads(content.read()) + + + theaters = { + "Caucasus": CaucasusTheater, + "Nevada": NevadaTheater, + "Persian Gulf": PersianGulfTheater, + "Normandy": NormandyTheater, + "The Channel": TheChannelTheater, + "Syria": SyriaTheater, + } + theater = theaters[json_data["theater"]] + t = theater() + cps = {} + + for p in json_data["player_points"]: + cp = t.add_json_cp(theater, p) + cp.captured = True + cps[p["id"]] = cp + t.add_controlpoint(cp) + + for p in json_data["enemy_points"]: + cp = t.add_json_cp(theater, p) + cps[p["id"]] = cp + t.add_controlpoint(cp) + + for l in json_data["links"]: + cps[l[0]].connect(cps[l[1]]) + cps[l[1]].connect(cps[l[0]]) + + return t + + +class CaucasusTheater(ConflictTheater): + terrain = caucasus.Caucasus() + overview_image = "caumap.gif" + reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), + (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } + + landmap = load_landmap("resources\\caulandmap.p") + daytime_map = { + "dawn": (6, 9), + "day": (9, 18), + "dusk": (18, 20), + "night": (0, 5), + } + + +class PersianGulfTheater(ConflictTheater): + terrain = dcs.terrain.PersianGulf() + overview_image = "persiangulf.gif" + reference_points = { + (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( + 772, -1970), + (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } + landmap = load_landmap("resources\\gulflandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (8, 16), + "dusk": (16, 18), + "night": (0, 5), + } + + +class NevadaTheater(ConflictTheater): + terrain = dcs.terrain.Nevada() + overview_image = "nevada.gif" + reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), + (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } + landmap = load_landmap("resources\\nev_landmap.p") + daytime_map = { + "dawn": (4, 6), + "day": (6, 17), + "dusk": (17, 18), + "night": (0, 5), + } + + +class NormandyTheater(ConflictTheater): + terrain = dcs.terrain.Normandy() + overview_image = "normandy.gif" + reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), + (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} + landmap = load_landmap("resources\\normandylandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (10, 17), + "dusk": (17, 18), + "night": (0, 5), + } + + +class TheChannelTheater(ConflictTheater): + terrain = dcs.terrain.TheChannel() + overview_image = "thechannel.gif" + reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), + (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} + landmap = load_landmap("resources\\channellandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (10, 17), + "dusk": (17, 18), + "night": (0, 5), + } + + +class SyriaTheater(ConflictTheater): + terrain = dcs.terrain.Syria() + overview_image = "syria.gif" + reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), + (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} + landmap = load_landmap("resources\\syrialandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (8, 16), + "dusk": (16, 18), + "night": (0, 5), + } diff --git a/theater/nevada.py b/theater/nevada.py deleted file mode 100644 index a7b7d030..00000000 --- a/theater/nevada.py +++ /dev/null @@ -1,42 +0,0 @@ -from dcs.terrain import nevada -from dcs import mapping - -from .landmap import * -from .conflicttheater import * -from .base import * - - -class NevadaTheater(ConflictTheater): - terrain = dcs.terrain.Nevada() - overview_image = "nevada.gif" - reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45*2, -360*2), - (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440*2, 80*2), } - landmap = load_landmap("resources\\nev_landmap.p") - daytime_map = { - "dawn": (4, 6), - "day": (6, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(NevadaTheater, self).__init__() - - self.tonopah_test_range = ControlPoint.from_airport(nevada.Tonopah_Test_Range_Airfield, LAND, SIZE_SMALL,IMPORTANCE_LOW) - self.lincoln_conty = ControlPoint.from_airport(nevada.Lincoln_County, LAND, SIZE_SMALL, 1.2) - self.groom_lake = ControlPoint.from_airport(nevada.Groom_Lake_AFB, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.mesquite = ControlPoint.from_airport(nevada.Mesquite, LAND, SIZE_REGULAR, 1.3) - self.creech = ControlPoint.from_airport(nevada.Creech_AFB, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.nellis = ControlPoint.from_airport(nevada.Nellis_AFB, LAND, SIZE_BIG, IMPORTANCE_HIGH) - - self.add_controlpoint(self.tonopah_test_range, connected_to=[self.lincoln_conty, self.groom_lake]) - self.add_controlpoint(self.lincoln_conty, connected_to=[self.tonopah_test_range, self.mesquite]) - self.add_controlpoint(self.groom_lake, connected_to=[self.mesquite, self.creech, self.tonopah_test_range]) - - self.add_controlpoint(self.creech, connected_to=[self.groom_lake, self.nellis]) - self.add_controlpoint(self.mesquite, connected_to=[self.lincoln_conty, self.groom_lake]) - self.add_controlpoint(self.nellis, connected_to=[self.creech]) - - self.nellis.captured = True - self.tonopah_test_range.captured_invert = True - diff --git a/theater/normandy.py b/theater/normandy.py deleted file mode 100644 index dd67e9a6..00000000 --- a/theater/normandy.py +++ /dev/null @@ -1,83 +0,0 @@ -from dcs.terrain import normandy - -from .conflicttheater import * -from .landmap import * - - -class NormandyTheater(ConflictTheater): - terrain = dcs.terrain.Normandy() - overview_image = "normandy.gif" - reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), - (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} - landmap = load_landmap("resources\\normandylandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(NormandyTheater, self).__init__() - - self.needOarPoint = ControlPoint.from_airport(normandy.Needs_Oar_Point, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.chailey = ControlPoint.from_airport(normandy.Chailey, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.deuxjumeaux = ControlPoint.from_airport(normandy.Deux_Jumeaux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lignerolles = ControlPoint.from_airport(normandy.Lignerolles, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.carpiquet = ControlPoint.from_airport(normandy.Carpiquet, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lessay = ControlPoint.from_airport(normandy.Lessay, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.maupertus = ControlPoint.from_airport(normandy.Maupertus, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.evreux = ControlPoint.from_airport(normandy.Evreux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.add_controlpoint(self.chailey, connected_to=[self.needOarPoint]) - self.add_controlpoint(self.needOarPoint, connected_to=[self.chailey]) - - self.add_controlpoint(self.deuxjumeaux, connected_to=[self.lignerolles]) - self.add_controlpoint(self.lignerolles, connected_to=[self.deuxjumeaux, self.lessay, self.carpiquet]) - self.add_controlpoint(self.lessay, connected_to=[self.lignerolles, self.maupertus]) - self.add_controlpoint(self.carpiquet, connected_to=[self.lignerolles, self.evreux]) - self.add_controlpoint(self.maupertus, connected_to=[self.lessay]) - self.add_controlpoint(self.evreux, connected_to=[self.carpiquet]) - - self.deuxjumeaux.captured = True - self.chailey.captured = True - self.needOarPoint.captured = True - - self.evreux.captured_invert = True - - -class NormandySmall(ConflictTheater): - terrain = dcs.terrain.Normandy() - overview_image = "normandy.gif" - reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), - (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} - landmap = load_landmap("resources\\normandylandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(NormandySmall, self).__init__() - - self.needOarPoint = ControlPoint.from_airport(normandy.Needs_Oar_Point, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.deuxjumeaux = ControlPoint.from_airport(normandy.Deux_Jumeaux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lignerolles = ControlPoint.from_airport(normandy.Lignerolles, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.carpiquet = ControlPoint.from_airport(normandy.Carpiquet, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.evreux = ControlPoint.from_airport(normandy.Evreux, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.add_controlpoint(self.needOarPoint, connected_to=[self.needOarPoint]) - - self.add_controlpoint(self.deuxjumeaux, connected_to=[self.lignerolles]) - self.add_controlpoint(self.lignerolles, connected_to=[self.deuxjumeaux, self.carpiquet]) - self.add_controlpoint(self.carpiquet, connected_to=[self.lignerolles, self.evreux]) - self.add_controlpoint(self.evreux, connected_to=[self.carpiquet]) - - self.deuxjumeaux.captured = True - self.needOarPoint.captured = True - - self.evreux.captured_invert = True diff --git a/theater/persiangulf.py b/theater/persiangulf.py deleted file mode 100644 index 0960c947..00000000 --- a/theater/persiangulf.py +++ /dev/null @@ -1,298 +0,0 @@ -from dcs.terrain import persiangulf -from dcs import mapping - -from .conflicttheater import * -from .base import * -from .landmap import load_landmap - - -class PersianGulfTheater(ConflictTheater): - terrain = dcs.terrain.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): (772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - - def __init__(self): - super(PersianGulfTheater, self).__init__() - - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, 1.1) - self.sir_abu_nuayr = ControlPoint.from_airport(persiangulf.Sir_Abu_Nuayr, [0, 330], SIZE_SMALL, 1.1,has_frontline=False) - self.dubai = ControlPoint.from_airport(persiangulf.Dubai_Intl, COAST_DL_E, SIZE_LARGE, IMPORTANCE_MEDIUM) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, 1.0) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, 1.0) - self.khasab = ControlPoint.from_airport(persiangulf.Khasab, LAND, SIZE_SMALL, IMPORTANCE_MEDIUM) - self.sirri = ControlPoint.from_airport(persiangulf.Sirri_Island, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_LOW,has_frontline=False) - self.abu_musa = ControlPoint.from_airport(persiangulf.Abu_Musa_Island_Airport, LAND, SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_island = ControlPoint.from_airport(persiangulf.Tunb_Island_AFB, [0, 270, 330], SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_kochak = ControlPoint.from_airport(persiangulf.Tunb_Kochak, [135, 180], SIZE_SMALL, 1.1,has_frontline=False) - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL,IMPORTANCE_HIGH) - self.qeshm = ControlPoint.from_airport(persiangulf.Qeshm_Island, [270, 315, 0, 45, 90, 135, 180], SIZE_SMALL,1.1, has_frontline=False) - self.havadarya = ControlPoint.from_airport(persiangulf.Havadarya, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_HIGH) - self.bandar_abbas = ControlPoint.from_airport(persiangulf.Bandar_Abbas_Intl, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_MEDIUM) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.liwa = ControlPoint.from_airport(persiangulf.Liwa_Airbase, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.bandar_e_jask = ControlPoint.from_airport(persiangulf.Bandar_e_Jask_airfield, LAND, SIZE_TINY,IMPORTANCE_LOW) - self.west_carrier = ControlPoint.carrier("West carrier", Point(-69043.813952358, -159916.65947136), 1001) - self.east_carrier = ControlPoint.carrier("East carrier", Point(59514.324335475, 28165.517980635), 1002) - - self.add_controlpoint(self.liwa, connected_to=[self.al_dhafra]) - self.add_controlpoint(self.al_dhafra, connected_to=[self.liwa, self.al_maktoum, self.al_ain]) - self.add_controlpoint(self.al_ain, connected_to=[self.al_dhafra, self.al_maktoum]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_dhafra, self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum, self.dubai]) - self.add_controlpoint(self.dubai, connected_to=[self.al_minhad, self.sharjah, self.fujairah]) - self.add_controlpoint(self.sharjah, connected_to=[self.dubai, self.ras_al_khaimah]) - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.sharjah, self.khasab]) - self.add_controlpoint(self.fujairah, connected_to=[self.dubai, self.khasab]) - self.add_controlpoint(self.khasab, connected_to=[self.ras_al_khaimah, self.fujairah]) - - self.add_controlpoint(self.sir_abu_nuayr, connected_to=[]) - self.add_controlpoint(self.sirri, connected_to=[]) - self.add_controlpoint(self.abu_musa, connected_to=[]) - self.add_controlpoint(self.tunb_kochak, connected_to=[]) - - self.add_controlpoint(self.tunb_island, connected_to=[]) - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar, self.qeshm]) - self.add_controlpoint(self.qeshm, connected_to=[self.bandar_lengeh, self.havadarya]) - self.add_controlpoint(self.havadarya, connected_to=[self.lar, self.qeshm, self.bandar_abbas]) - self.add_controlpoint(self.bandar_abbas, connected_to=[self.havadarya, self.kerman]) - - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.kerman, connected_to=[self.lar, self.shiraz, self.bandar_abbas]) - self.add_controlpoint(self.lar, connected_to=[self.havadarya, self.shiraz, self.kerman]) - - self.add_controlpoint(self.west_carrier) - self.add_controlpoint(self.east_carrier) - - self.west_carrier.captured = True - self.east_carrier.captured = True - self.liwa.captured = True - - self.west_carrier.captured_invert = True - self.east_carrier.captured_invert = True - self.shiraz.captured_invert = True - - -class IranianCampaign(ConflictTheater): - - terrain = dcs.terrain.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - def __init__(self): - super(IranianCampaign, self).__init__() - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, 1.1) - self.sir_abu_nuayr = ControlPoint.from_airport(persiangulf.Sir_Abu_Nuayr, [0, 330], SIZE_SMALL, 1.1,has_frontline=False) - self.dubai = ControlPoint.from_airport(persiangulf.Dubai_Intl, COAST_DL_E, SIZE_LARGE, IMPORTANCE_MEDIUM) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, 1.0) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, 1.0) - self.khasab = ControlPoint.from_airport(persiangulf.Khasab, LAND, SIZE_SMALL, IMPORTANCE_MEDIUM) - self.sirri = ControlPoint.from_airport(persiangulf.Sirri_Island, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_LOW,has_frontline=False) - self.abu_musa = ControlPoint.from_airport(persiangulf.Abu_Musa_Island_Airport, LAND, SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_island = ControlPoint.from_airport(persiangulf.Tunb_Island_AFB, [0, 270, 330], SIZE_SMALL,IMPORTANCE_MEDIUM, has_frontline=False) - self.tunb_kochak = ControlPoint.from_airport(persiangulf.Tunb_Kochak, [135, 180], SIZE_SMALL, 1.1,has_frontline=False) - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL,IMPORTANCE_HIGH) - self.qeshm = ControlPoint.from_airport(persiangulf.Qeshm_Island, [270, 315, 0, 45, 90, 135, 180], SIZE_SMALL,1.1, has_frontline=False) - self.havadarya = ControlPoint.from_airport(persiangulf.Havadarya, COAST_DL_W, SIZE_REGULAR, IMPORTANCE_HIGH) - self.bandar_abbas = ControlPoint.from_airport(persiangulf.Bandar_Abbas_Intl, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG,IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.bandar_e_jask = ControlPoint.from_airport(persiangulf.Bandar_e_Jask_airfield, LAND, SIZE_TINY,IMPORTANCE_LOW) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_MEDIUM) - - self.east_carrier = ControlPoint.carrier("East carrier", Point(59514.324335475, 28165.517980635), 1001) - self.west_carrier = ControlPoint.lha("Tarawa", Point(-27500.813952358, -147000.65947136), 1002) - - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.khasab]) - self.add_controlpoint(self.khasab, connected_to=[self.ras_al_khaimah]) - - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar]) - self.add_controlpoint(self.qeshm, connected_to=[]) - self.add_controlpoint(self.havadarya, connected_to=[self.lar, self.bandar_abbas]) - self.add_controlpoint(self.bandar_abbas, connected_to=[self.havadarya, self.jiroft]) - - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.jiroft, connected_to=[self.kerman, self.bandar_abbas]) - self.add_controlpoint(self.kerman, connected_to=[self.lar, self.shiraz, self.jiroft]) - self.add_controlpoint(self.lar, connected_to=[self.bandar_lengeh, self.havadarya, self.shiraz, self.kerman]) - - self.add_controlpoint(self.east_carrier) - self.add_controlpoint(self.west_carrier) - - self.east_carrier.captured = True - self.west_carrier.captured = True - self.al_dhafra.captured = True - self.ras_al_khaimah.captured = True - self.khasab.captured = True - self.qeshm.captured = True - self.havadarya.captured = True - self.bandar_abbas.captured = True - - self.shiraz.captured_invert = True - - -class Emirates(ConflictTheater): - terrain = dcs.terrain.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - - def __init__(self): - super(Emirates, self).__init__() - - self.al_dhafra = ControlPoint.from_airport(persiangulf.Al_Dhafra_AB, LAND, SIZE_BIG, IMPORTANCE_MEDIUM) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.sharjah = ControlPoint.from_airport(persiangulf.Sharjah_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.fujairah = ControlPoint.from_airport(persiangulf.Fujairah_Intl, COAST_V_W, SIZE_REGULAR, IMPORTANCE_LOW) - self.ras_al_khaimah = ControlPoint.from_airport(persiangulf.Ras_Al_Khaimah, LAND, SIZE_REGULAR,IMPORTANCE_LOW) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_LOW) - - self.east_carrier = ControlPoint.carrier("Carrier", Point(-61770, 69039), 1001) - self.tarawa_carrier = ControlPoint.lha("LHA Carrier", Point(-79770, 49430), 1002) - - self.add_controlpoint(self.al_dhafra, connected_to=[self.al_ain, self.al_maktoum]) - self.add_controlpoint(self.al_ain, connected_to=[self.fujairah, self.al_maktoum, self.al_dhafra]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_dhafra, self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum, self.sharjah]) - self.add_controlpoint(self.sharjah, connected_to=[self.al_minhad, self.ras_al_khaimah, self.fujairah]) - self.add_controlpoint(self.ras_al_khaimah, connected_to=[self.sharjah]) - self.add_controlpoint(self.fujairah, connected_to=[self.sharjah, self.al_ain]) - - self.add_controlpoint(self.tarawa_carrier) - self.add_controlpoint(self.east_carrier) - - self.tarawa_carrier.captured = True - self.east_carrier.captured = True - self.fujairah.captured = True - - self.tarawa_carrier.captured_invert = True - self.east_carrier.captured_invert = True - self.fujairah.captured_invert = True - - -class DesertWar(ConflictTheater): - terrain = dcs.terrain.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - - def __init__(self): - super(DesertWar, self).__init__() - - self.liwa = ControlPoint.from_airport(persiangulf.Liwa_Airbase, LAND, SIZE_BIG, IMPORTANCE_MEDIUM) - self.al_maktoum = ControlPoint.from_airport(persiangulf.Al_Maktoum_Intl, LAND, SIZE_BIG, IMPORTANCE_LOW) - self.al_minhad = ControlPoint.from_airport(persiangulf.Al_Minhad_AB, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.al_ain = ControlPoint.from_airport(persiangulf.Al_Ain_International_Airport, LAND, SIZE_BIG,IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(-124000, -303000), 1001) - self.tarawa_carrier = ControlPoint.lha("LHA Carrier", Point(-164000, -257000), 1002) - - self.add_controlpoint(self.liwa, connected_to=[self.al_ain]) - self.add_controlpoint(self.al_ain, connected_to=[self.al_maktoum, self.liwa]) - self.add_controlpoint(self.al_maktoum, connected_to=[self.al_minhad, self.al_ain]) - self.add_controlpoint(self.al_minhad, connected_to=[self.al_maktoum]) - - self.add_controlpoint(self.tarawa_carrier) - self.add_controlpoint(self.carrier) - - self.tarawa_carrier.captured = True - self.carrier.captured = True - self.liwa.captured = True - - self.tarawa_carrier.captured_invert = True - self.carrier.captured_invert = True - self.al_ain.captured_invert = True - - -class IranInvasionLite(ConflictTheater): - terrain = dcs.terrain.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - def __init__(self): - super(IranInvasionLite, self).__init__() - - self.bandar_lengeh = ControlPoint.from_airport(persiangulf.Bandar_Lengeh, [270, 315, 0, 45], SIZE_SMALL, IMPORTANCE_HIGH) - self.lar = ControlPoint.from_airport(persiangulf.Lar_Airbase, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.shiraz = ControlPoint.from_airport(persiangulf.Shiraz_International_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.kerman = ControlPoint.from_airport(persiangulf.Kerman_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.jiroft = ControlPoint.from_airport(persiangulf.Jiroft_Airport, LAND, SIZE_BIG, IMPORTANCE_HIGH) - self.carrier = ControlPoint.carrier("Carrier", Point(72000.324335475, -376000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-27500.813952358, -147000.65947136), 1002) - - self.add_controlpoint(self.bandar_lengeh, connected_to=[self.lar]) - self.add_controlpoint(self.shiraz, connected_to=[self.lar, self.kerman]) - self.add_controlpoint(self.jiroft, connected_to=[self.kerman]) - self.add_controlpoint(self.kerman, connected_to=[self.shiraz, self.jiroft]) - self.add_controlpoint(self.lar, connected_to=[self.bandar_lengeh, self.shiraz]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.carrier.captured = True - self.lha.captured = True - - self.shiraz.captured_invert = True - self.bandar_lengeh.captured = True - diff --git a/theater/syria.py b/theater/syria.py deleted file mode 100644 index 1465f58e..00000000 --- a/theater/syria.py +++ /dev/null @@ -1,225 +0,0 @@ -from dcs.terrain import syria - -from .conflicttheater import * -from .landmap import * - - -class SyriaTheater(ConflictTheater): - terrain = dcs.terrain.Syria() - overview_image = "syria.gif" - reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), - (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} - landmap = load_landmap("resources\\syrialandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - - def __init__(self): - super(SyriaTheater, self).__init__() - - -class GolanHeights(SyriaTheater): - - def __init__(self): - super(GolanHeights, self).__init__() - - self.ramatDavid = ControlPoint.from_airport(syria.Ramat_David, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.khalkhala.allow_sea_units = False - self.ramatDavid.allow_sea_units = False - self.kinghussein.allow_sea_units = False - - self.marjruhayyil = ControlPoint.from_airport(syria.Marj_Ruhayyil, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.mezzeh = ControlPoint.from_airport(syria.Mezzeh, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.carrier = ControlPoint.carrier("Carrier", Point(-280000, -238000), 1001) - self.lha = ControlPoint.lha("LHA Carrier", Point(-237000, -89800), 1002) - - self.add_controlpoint(self.ramatDavid, connected_to=[self.khalkhala]) - self.add_controlpoint(self.khalkhala, connected_to=[self.ramatDavid, self.kinghussein, self.marjruhayyil]) - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala]) - self.add_controlpoint(self.marjruhayyil, connected_to=[self.khalkhala, self.mezzeh, self.aldumayr]) - self.add_controlpoint(self.mezzeh, connected_to=[self.marjruhayyil]) - self.add_controlpoint(self.aldumayr, connected_to=[self.marjruhayyil]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.ramatDavid.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.aldumayr.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class TurkishInvasion(SyriaTheater): - - def __init__(self): - super(TurkishInvasion, self).__init__() - - self.hatay = ControlPoint.from_airport(syria.Hatay, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.minakh = ControlPoint.from_airport(syria.Minakh, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.kuweires = ControlPoint.from_airport(syria.Kuweires, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(133000, -54000), 1001) - self.lha = ControlPoint.lha("LHA", Point(155000, -19000), 1002) - - self.add_controlpoint(self.incirlik, connected_to=[]) - self.add_controlpoint(self.hatay, connected_to=[self.minakh]) - self.add_controlpoint(self.minakh, connected_to=[self.aleppo, self.hatay]) - self.add_controlpoint(self.aleppo, connected_to=[self.kuweires, self.minakh]) - self.add_controlpoint(self.kuweires, connected_to=[self.jirah, self.aleppo]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa, self.kuweires]) - self.add_controlpoint(self.tabqa, connected_to=[self.jirah]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.incirlik.captured = True - self.hatay.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.tabqa.captured_invert = True - - -class SyrianCivilWar(SyriaTheater): - - def __init__(self): - super(SyrianCivilWar, self).__init__() - - self.basselAlAssad = ControlPoint.from_airport(syria.Bassel_Al_Assad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.marjruhayyil = ControlPoint.from_airport(syria.Marj_Ruhayyil, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.hama = ControlPoint.from_airport(syria.Hama, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.alqusair= ControlPoint.from_airport(syria.Al_Qusayr, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(18537, -52000), 1001) - self.lha = ControlPoint.lha("LHA", Point(116000, -30000), 1002) - - self.add_controlpoint(self.basselAlAssad, connected_to=[self.hama]) - self.add_controlpoint(self.marjruhayyil, connected_to=[self.aldumayr]) - - self.add_controlpoint(self.hama, connected_to=[self.basselAlAssad, self.aleppo, self.alqusair]) - self.add_controlpoint(self.aleppo, connected_to=[self.hama]) - self.add_controlpoint(self.alqusair, connected_to=[self.hama, self.aldumayr, self.palmyra]) - self.add_controlpoint(self.palmyra, connected_to=[self.alqusair]) - self.add_controlpoint(self.aldumayr, connected_to=[self.alqusair, self.marjruhayyil]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.basselAlAssad.captured = True - self.marjruhayyil.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.aleppo.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class InherentResolve(SyriaTheater): - - def __init__(self): - super(InherentResolve, self).__init__() - - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - - self.carrier = ControlPoint.carrier("Carrier", Point(-210000, -200000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-131000, -161000), 1002) - - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala]) - self.add_controlpoint(self.incirlik, connected_to=[self.incirlik]) - self.add_controlpoint(self.khalkhala, connected_to=[self.kinghussein, self.palmyra]) - self.add_controlpoint(self.palmyra, connected_to=[self.khalkhala, self.tabqa]) - self.add_controlpoint(self.tabqa, connected_to=[self.palmyra, self.jirah]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.kinghussein.captured = True - self.incirlik.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.jirah.captured_invert = True - self.incirlik.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - -class SyriaFullMap(SyriaTheater): - - def __init__(self): - super(SyriaFullMap, self).__init__() - - self.ramatDavid = ControlPoint.from_airport(syria.Ramat_David, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.kinghussein = ControlPoint.from_airport(syria.King_Hussein_Air_College, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.khalkhala = ControlPoint.from_airport(syria.Khalkhalah, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.palmyra = ControlPoint.from_airport(syria.Palmyra, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.jirah = ControlPoint.from_airport(syria.Jirah, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.tabqa = ControlPoint.from_airport(syria.Tabqa, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aldumayr = ControlPoint.from_airport(syria.Al_Dumayr, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.hama = ControlPoint.from_airport(syria.Hama, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.alqusair= ControlPoint.from_airport(syria.Al_Qusayr, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.aleppo = ControlPoint.from_airport(syria.Aleppo, LAND, SIZE_REGULAR, IMPORTANCE_MEDIUM) - self.basselAlAssad = ControlPoint.from_airport(syria.Bassel_Al_Assad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.renemouawad = ControlPoint.from_airport(syria.Rene_Mouawad, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.minakh = ControlPoint.from_airport(syria.Minakh, LAND, SIZE_REGULAR, IMPORTANCE_LOW) - self.hatay = ControlPoint.from_airport(syria.Hatay, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - self.incirlik = ControlPoint.from_airport(syria.Incirlik, LAND, SIZE_REGULAR, IMPORTANCE_HIGH) - - - self.carrier = ControlPoint.carrier("Carrier", Point(-151000, -106000), 1001) - self.lha = ControlPoint.lha("LHA", Point(-131000, -161000), 1002) - - self.add_controlpoint(self.ramatDavid, connected_to=[self.kinghussein]) - self.add_controlpoint(self.kinghussein, connected_to=[self.khalkhala, self.ramatDavid]) - self.add_controlpoint(self.khalkhala, connected_to=[self.kinghussein, self.aldumayr]) - self.add_controlpoint(self.aldumayr, connected_to=[self.khalkhala, self.alqusair]) - self.add_controlpoint(self.alqusair, connected_to=[self.hama, self.aldumayr, self.palmyra, self.renemouawad]) - self.add_controlpoint(self.renemouawad, connected_to=[self.alqusair, self.basselAlAssad]) - self.add_controlpoint(self.hama, connected_to=[self.aleppo, self.alqusair, self.basselAlAssad]) - self.add_controlpoint(self.basselAlAssad, connected_to=[self.hama, self.hatay, self.renemouawad]) - self.add_controlpoint(self.palmyra, connected_to=[self.tabqa, self.alqusair]) - self.add_controlpoint(self.tabqa, connected_to=[self.palmyra, self.jirah]) - self.add_controlpoint(self.jirah, connected_to=[self.tabqa, self.aleppo]) - self.add_controlpoint(self.aleppo, connected_to=[self.hama, self.jirah, self.minakh]) - self.add_controlpoint(self.minakh, connected_to=[self.hatay, self.aleppo, self.incirlik]) - self.add_controlpoint(self.hatay, connected_to=[self.minakh, self.basselAlAssad]) - self.add_controlpoint(self.incirlik, connected_to=[self.minakh]) - - self.add_controlpoint(self.carrier) - self.add_controlpoint(self.lha) - - self.ramatDavid.captured = True - self.carrier.captured = True - self.lha.captured = True - - self.incirlik.captured_invert = True - self.carrier.captured_invert = True - self.lha.captured_invert = True - - diff --git a/theater/thechannel.py b/theater/thechannel.py deleted file mode 100644 index b37bbca3..00000000 --- a/theater/thechannel.py +++ /dev/null @@ -1,109 +0,0 @@ -from dcs.terrain import thechannel - -from .conflicttheater import * -from .landmap import * - - -class Dunkirk(ConflictTheater): - terrain = dcs.terrain.TheChannel() - overview_image = "thechannel.gif" - reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), - (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} - landmap = load_landmap("resources\\channellandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(Dunkirk, self).__init__() - - self.abeville = ControlPoint.from_airport(thechannel.Abbeville_Drucat, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.detling = ControlPoint.from_airport(thechannel.Detling, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.stomer = ControlPoint.from_airport(thechannel.Saint_Omer_Longuenesse, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.dunkirk = ControlPoint.from_airport(thechannel.Dunkirk_Mardyck, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.hawkinge = ControlPoint.from_airport(thechannel.Hawkinge, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.highhalden = ControlPoint.from_airport(thechannel.High_Halden, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lympne = ControlPoint.from_airport(thechannel.Lympne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.manston = ControlPoint.from_airport(thechannel.Manston, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.merville = ControlPoint.from_airport(thechannel.Merville_Calonne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - - # England - self.add_controlpoint(self.hawkinge, connected_to=[self.lympne, self.manston]) - self.add_controlpoint(self.lympne, connected_to=[self.hawkinge]) - self.add_controlpoint(self.manston, connected_to=[self.hawkinge]) - - # France - self.add_controlpoint(self.dunkirk, connected_to=[self.stomer]) - self.add_controlpoint(self.stomer, connected_to=[self.dunkirk, self.merville, self.abeville]) - self.add_controlpoint(self.merville, connected_to=[self.stomer]) - self.add_controlpoint(self.abeville, connected_to=[self.stomer]) - - #self.detling.captured = True - self.hawkinge.captured = True - self.dunkirk.captured = True - #self.highhalden.captured = True - self.lympne.captured = True - self.manston.captured = True - - self.manston.captured_invert = True - self.dunkirk.captured_invert = True - self.stomer.captured_invert = True - self.merville.captured_invert = True - self.abeville.captured_invert = True - - -class BattleOfBritain(ConflictTheater): - terrain = dcs.terrain.TheChannel() - overview_image = "thechannel.gif" - reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), - (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} - landmap = load_landmap("resources\\channellandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - - def __init__(self): - super(BattleOfBritain, self).__init__() - - self.abeville = ControlPoint.from_airport(thechannel.Abbeville_Drucat, LAND, SIZE_SMALL, IMPORTANCE_LOW) - #self.detling = ControlPoint.from_airport(thechannel.Detling, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - self.stomer = ControlPoint.from_airport(thechannel.Saint_Omer_Longuenesse, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.dunkirk = ControlPoint.from_airport(thechannel.Dunkirk_Mardyck, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.hawkinge = ControlPoint.from_airport(thechannel.Hawkinge, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.highhalden = ControlPoint.from_airport(thechannel.High_Halden, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.lympne = ControlPoint.from_airport(thechannel.Lympne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.manston = ControlPoint.from_airport(thechannel.Manston, LAND, SIZE_SMALL, IMPORTANCE_LOW) - self.merville = ControlPoint.from_airport(thechannel.Merville_Calonne, LAND, SIZE_SMALL, IMPORTANCE_LOW) - - # England - self.add_controlpoint(self.hawkinge, connected_to=[self.lympne, self.manston]) - self.add_controlpoint(self.lympne, connected_to=[self.hawkinge, self.highhalden]) - self.add_controlpoint(self.manston, connected_to=[self.hawkinge]) - self.add_controlpoint(self.highhalden, connected_to=[self.lympne]) - - # France - self.add_controlpoint(self.dunkirk, connected_to=[self.stomer]) - self.add_controlpoint(self.stomer, connected_to=[self.dunkirk, self.merville, self.abeville]) - self.add_controlpoint(self.merville, connected_to=[self.stomer]) - self.add_controlpoint(self.abeville, connected_to=[self.stomer]) - - #self.detling.captured = True - self.hawkinge.captured = True - #self.dunkirk.captured = True - self.highhalden.captured = True - self.lympne.captured = True - self.manston.captured = True - - self.dunkirk.captured_invert = True - self.stomer.captured_invert = True - self.merville.captured_invert = True - self.abeville.captured_invert = True From 3bb1327a651602e11c167824266269b8c418979a Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 00:25:02 +0200 Subject: [PATCH 30/48] Manuall reintroduced inverted campaign config in json campaign files --- resources/campaigns/battle_of_britain.json | 12 ++++++++---- resources/campaigns/desert_war.json | 9 ++++++--- resources/campaigns/dunkirk.json | 15 ++++++++++----- resources/campaigns/emirates.json | 9 ++++++--- resources/campaigns/full_map.json | 9 ++++++--- resources/campaigns/golan_heights_battle.json | 9 ++++++--- resources/campaigns/inherent_resolve.json | 12 ++++++++---- resources/campaigns/invasion_from_turkey.json | 3 ++- resources/campaigns/invasion_of_iran.json | 3 ++- resources/campaigns/invasion_of_iran_[lite].json | 3 ++- resources/campaigns/normandy.json | 3 ++- resources/campaigns/normandy_small.json | 3 ++- resources/campaigns/north_caucasus.json | 9 ++++++--- resources/campaigns/north_nevada.json | 3 ++- resources/campaigns/russia_small.json | 3 ++- resources/campaigns/syrian_civil_war.json | 9 ++++++--- resources/campaigns/western_georgia.json | 9 ++++++--- 17 files changed, 82 insertions(+), 41 deletions(-) diff --git a/resources/campaigns/battle_of_britain.json b/resources/campaigns/battle_of_britain.json index 388208dc..38f5ab0f 100644 --- a/resources/campaigns/battle_of_britain.json +++ b/resources/campaigns/battle_of_britain.json @@ -32,25 +32,29 @@ "type": "airbase", "id": "Dunkirk Mardyck", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Saint Omer Longuenesse", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Merville Calonne", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Abbeville Drucat", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/desert_war.json b/resources/campaigns/desert_war.json index 06e96a06..e49f7081 100644 --- a/resources/campaigns/desert_war.json +++ b/resources/campaigns/desert_war.json @@ -12,13 +12,15 @@ "type": "lha", "id": 1002, "x": -164000, - "y": -257000 + "y": -257000, + "captured_invert": true }, { "type": "carrier", "id": 1001, "x": -124000, - "y": -303000 + "y": -303000, + "captured_invert": true } ], "enemy_points": [ @@ -26,7 +28,8 @@ "type": "airbase", "id": "Al Ain International Airport", "size": 2000, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", diff --git a/resources/campaigns/dunkirk.json b/resources/campaigns/dunkirk.json index 74417636..d3cf11b0 100644 --- a/resources/campaigns/dunkirk.json +++ b/resources/campaigns/dunkirk.json @@ -18,13 +18,15 @@ "type": "airbase", "id": "Manston", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Dunkirk Mardyck", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "enemy_points": [ @@ -32,19 +34,22 @@ "type": "airbase", "id": "Saint Omer Longuenesse", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Merville Calonne", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", "id": "Abbeville Drucat", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/emirates.json b/resources/campaigns/emirates.json index 0453a489..1e032e63 100644 --- a/resources/campaigns/emirates.json +++ b/resources/campaigns/emirates.json @@ -13,19 +13,22 @@ 0 ], "size": 1000, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -79770, - "y": 49430 + "y": 49430, + "captured_invert": true }, { "type": "carrier", "id": 1001, "x": -61770, - "y": 69039 + "y": 69039, + "captured_invert": true } ], "enemy_points": [ diff --git a/resources/campaigns/full_map.json b/resources/campaigns/full_map.json index 73ce78c0..f88963ea 100644 --- a/resources/campaigns/full_map.json +++ b/resources/campaigns/full_map.json @@ -12,13 +12,15 @@ "type": "carrier", "id": 1001, "x": -151000, - "y": -106000 + "y": -106000, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -131000, - "y": -161000 + "y": -161000, + "captured_invert": true } ], "enemy_points": [ @@ -104,7 +106,8 @@ "type": "airbase", "id": "Incirlik", "size": 1000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/golan_heights_battle.json b/resources/campaigns/golan_heights_battle.json index 4f109c9d..f498b3bb 100644 --- a/resources/campaigns/golan_heights_battle.json +++ b/resources/campaigns/golan_heights_battle.json @@ -12,13 +12,15 @@ "type": "carrier", "id": 1001, "x": -280000, - "y": -238000 + "y": -238000, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -237000, - "y": -89800 + "y": -89800, + "captured_invert": true } ], "enemy_points": [ @@ -50,7 +52,8 @@ "type": "airbase", "id": "Al-Dumayr", "size": 1000, - "importance": 1.2 + "importance": 1.2, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json index 4882be93..b968bc77 100644 --- a/resources/campaigns/inherent_resolve.json +++ b/resources/campaigns/inherent_resolve.json @@ -12,19 +12,22 @@ "type": "airbase", "id": "Incirlik", "size": 1000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true }, { "type": "carrier", "id": 1001, "x": -210000, - "y": -200000 + "y": -200000, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -131000, - "y": -161000 + "y": -161000, + "captured_invert": true } ], "enemy_points": [ @@ -50,7 +53,8 @@ "type": "airbase", "id": "Jirah", "size": 1000, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/invasion_from_turkey.json b/resources/campaigns/invasion_from_turkey.json index d380582d..de0f6189 100644 --- a/resources/campaigns/invasion_from_turkey.json +++ b/resources/campaigns/invasion_from_turkey.json @@ -56,7 +56,8 @@ "type": "airbase", "id": "Tabqa", "size": 1000, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/invasion_of_iran.json b/resources/campaigns/invasion_of_iran.json index 87227808..888d29f5 100644 --- a/resources/campaigns/invasion_of_iran.json +++ b/resources/campaigns/invasion_of_iran.json @@ -78,7 +78,8 @@ "type": "airbase", "id": "Shiraz International Airport", "size": 2000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true }, { "type": "airbase", diff --git a/resources/campaigns/invasion_of_iran_[lite].json b/resources/campaigns/invasion_of_iran_[lite].json index 8bc8ff4f..aa31a7fa 100644 --- a/resources/campaigns/invasion_of_iran_[lite].json +++ b/resources/campaigns/invasion_of_iran_[lite].json @@ -32,7 +32,8 @@ "type": "airbase", "id": "Shiraz International Airport", "size": 2000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true }, { "type": "airbase", diff --git a/resources/campaigns/normandy.json b/resources/campaigns/normandy.json index 3aad62dd..8c71716e 100644 --- a/resources/campaigns/normandy.json +++ b/resources/campaigns/normandy.json @@ -50,7 +50,8 @@ "type": "airbase", "id": "Evreux", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json index 7350e40c..4989c9c4 100644 --- a/resources/campaigns/normandy_small.json +++ b/resources/campaigns/normandy_small.json @@ -32,7 +32,8 @@ "type": "airbase", "id": "Evreux", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/north_caucasus.json b/resources/campaigns/north_caucasus.json index 67f1a0ff..0da83e72 100644 --- a/resources/campaigns/north_caucasus.json +++ b/resources/campaigns/north_caucasus.json @@ -18,13 +18,15 @@ "type": "carrier", "id": 1001, "x": -285810.6875, - "y": 496399.1875 + "y": 496399.1875, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -326050.6875, - "y": 519452.1875 + "y": 519452.1875, + "captured_invert": true } ], "enemy_points": [ @@ -56,7 +58,8 @@ "type": "airbase", "id": "Maykop-Khanskaya", "size": 3000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/north_nevada.json b/resources/campaigns/north_nevada.json index e1ddd17d..d1687a7c 100644 --- a/resources/campaigns/north_nevada.json +++ b/resources/campaigns/north_nevada.json @@ -14,7 +14,8 @@ "type": "airbase", "id": "Tonopah Test Range Airfield", "size": 600, - "importance": 1 + "importance": 1, + "captured_invert": true }, { "type": "airbase", diff --git a/resources/campaigns/russia_small.json b/resources/campaigns/russia_small.json index 207ff7fe..341d90d0 100644 --- a/resources/campaigns/russia_small.json +++ b/resources/campaigns/russia_small.json @@ -20,7 +20,8 @@ "type": "airbase", "id": "Maykop-Khanskaya", "size": 3000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true } ], "links": [ diff --git a/resources/campaigns/syrian_civil_war.json b/resources/campaigns/syrian_civil_war.json index 18e999ce..0f041d39 100644 --- a/resources/campaigns/syrian_civil_war.json +++ b/resources/campaigns/syrian_civil_war.json @@ -18,13 +18,15 @@ "type": "carrier", "id": 1001, "x": 18537, - "y": -52000 + "y": -52000, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": 116000, - "y": -30000 + "y": -30000, + "captured_invert": true } ], "enemy_points": [ @@ -38,7 +40,8 @@ "type": "airbase", "id": "Aleppo", "size": 1000, - "importance": 1.2 + "importance": 1.2, + "captured_invert": true }, { "type": "airbase", diff --git a/resources/campaigns/western_georgia.json b/resources/campaigns/western_georgia.json index c539cbf8..b0e64464 100644 --- a/resources/campaigns/western_georgia.json +++ b/resources/campaigns/western_georgia.json @@ -21,13 +21,15 @@ "type": "carrier", "id": 1001, "x": -285810.6875, - "y": 496399.1875 + "y": 496399.1875, + "captured_invert": true }, { "type": "lha", "id": 1002, "x": -326050.6875, - "y": 519452.1875 + "y": 519452.1875, + "captured_invert": true } ], "enemy_points": [ @@ -80,7 +82,8 @@ 135 ], "size": 2000, - "importance": 1.4 + "importance": 1.4, + "captured_invert": true } ], "links": [ From de5238e89a15069d582a7bf13e6464126f28981c Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 00:27:43 +0200 Subject: [PATCH 31/48] Fixed issue in Normandy small campaign --- resources/campaigns/normandy_small.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/resources/campaigns/normandy_small.json b/resources/campaigns/normandy_small.json index 4989c9c4..a0bff78e 100644 --- a/resources/campaigns/normandy_small.json +++ b/resources/campaigns/normandy_small.json @@ -37,10 +37,6 @@ } ], "links": [ - [ - "Needs Oar Point", - "Needs Oar Point" - ], [ "Deux Jumeaux", "Lignerolles" From 00ea8ac4e1366145e68d2affb5ec175392b77e90 Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 00:56:42 +0200 Subject: [PATCH 32/48] Reverted automerge errors. --- game/operation/operation.py | 122 ++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index fe50f2ef..ed2a018f 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,13 +1,14 @@ from typing import Set +from dcs.countries import country_dict +from dcs.lua.parse import loads +from dcs.terrain.terrain import Terrain + from gen import * from gen.airfields import AIRFIELD_DATA from gen.beacons import load_beacons_for_terrain from gen.radios import RadioRegistry from gen.tacan import TacanRegistry -from dcs.countries import country_dict -from dcs.lua.parse import loads -from dcs.terrain.terrain import Terrain from userdata.debriefing import * @@ -68,8 +69,26 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.briefinggen = BriefingGenerator(self.current_mission, - self.conflict, self.game) + self.radio_registry = RadioRegistry() + self.tacan_registry = TacanRegistry() + self.airgen = AircraftConflictGenerator( + mission, conflict, self.game.settings, self.game, + self.radio_registry) + self.airsupportgen = AirSupportConflictGenerator( + mission, conflict, self.game, self.radio_registry, + self.tacan_registry) + self.triggersgen = TriggersGenerator(mission, conflict, self.game) + self.visualgen = VisualGenerator(mission, conflict, self.game) + self.envgen = EnviromentGenerator(mission, conflict, self.game) + self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) + self.groundobjectgen = GroundObjectsGenerator( + mission, + conflict, + self.game, + self.radio_registry, + self.tacan_registry + ) + self.briefinggen = BriefingGenerator(mission, conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): with open("resources/default_options.lua", "r") as f: @@ -109,9 +128,6 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): - radio_registry = RadioRegistry() - tacan_registry = TacanRegistry() - # Dedup beacon/radio frequencies, since some maps have some frequencies # used multiple times. beacons = load_beacons_for_terrain(self.game.theater.terrain.name) @@ -123,7 +139,7 @@ class Operation: logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: - tacan_registry.reserve(beacon.tacan_channel) + self.tacan_registry.reserve(beacon.tacan_channel) for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: @@ -135,26 +151,16 @@ class Operation: # beacon list. for frequency in unique_map_frequencies: - radio_registry.reserve(frequency) + self.radio_registry.reserve(frequency) # Generate meteo - envgen = EnviromentGenerator(self.current_mission, self.conflict, - self.game) if self.environment_settings is None: - self.environment_settings = envgen.generate() + self.environment_settings = self.envgen.generate() else: - envgen.load(self.environment_settings) + self.envgen.load(self.environment_settings) # Generate ground object first - - groundobjectgen = GroundObjectsGenerator( - self.current_mission, - self.conflict, - self.game, - radio_registry, - tacan_registry - ) - groundobjectgen.generate() + self.groundobjectgen.generate() # Generate destroyed units for d in self.game.get_destroyed_units(): @@ -176,10 +182,7 @@ class Operation: ) # Air Support (Tanker & Awacs) - airsupportgen = AirSupportConflictGenerator( - self.current_mission, self.conflict, self.game, radio_registry, - tacan_registry) - airsupportgen.generate(self.is_awacs_enabled) + self.airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map self.airgen.generate_flights( @@ -193,7 +196,6 @@ class Operation: self.groundobjectgen.runways ) - # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): @@ -204,32 +206,32 @@ class Operation: # Generate frontline ops player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] - groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) + groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, + player_cp.stances[enemy_cp.id]) groundConflictGen.generate() jtacs.extend(groundConflictGen.jtacs) # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 - if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]: + if self.game.player_country in [country.name for country in + self.current_mission.coalition["blue"].countries.values()]: self.current_mission.groundControl.blue_tactical_commander = self.ca_slots else: self.current_mission.groundControl.red_tactical_commander = self.ca_slots # Triggers - triggersgen = TriggersGenerator(self.current_mission, self.conflict, - self.game) - triggersgen.generate() + if self.game.is_player_attack(self.conflict.attackers_country): + cp = self.conflict.from_cp + else: + cp = self.conflict.to_cp + self.triggersgen.generate() # Options - forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, - self.conflict, self.game) - forcedoptionsgen.generate() + self.forcedoptionsgen.generate() # Generate Visuals Smoke Effects - visualgen = VisualGenerator(self.current_mission, self.conflict, - self.game) if self.game.settings.perf_smoke_gen: - visualgen.generate() + self.visualgen.generate() # Inject Plugins Lua Scripts listOfPluginsScripts = [] @@ -237,7 +239,7 @@ class Operation: if plugin_file_path.exists(): for line in plugin_file_path.read_text().splitlines(): name = line.strip() - if not name.startswith( '#' ): + if not name.startswith('#'): trigger = TriggerStart(comment="Load " + name) listOfPluginsScripts.append(name) fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name) @@ -248,21 +250,21 @@ class Operation: f"Not loading plugins, {plugin_file_path} does not exist") # Inject Mist Script if not done already in the plugins - if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice + if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice trigger = TriggerStart(comment="Load Mist Lua framework") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua") trigger.add_action(DoScriptFile(fileref)) self.current_mission.triggerrules.triggers.append(trigger) # Inject JSON library if not done already in the plugins - if not "json.lua" in listOfPluginsScripts : # don't load the script twice + if not "json.lua" in listOfPluginsScripts: # don't load the script twice trigger = TriggerStart(comment="Load JSON Lua library") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua") trigger.add_action(DoScriptFile(fileref)) self.current_mission.triggerrules.triggers.append(trigger) # Inject Ciribob's JTACAutoLase if not done already in the plugins - if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice + if not "JTACAutoLase.lua" in listOfPluginsScripts: # don't load the script twice trigger = TriggerStart(comment="Load JTACAutoLase.lua script") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua") trigger.add_action(DoScriptFile(fileref)) @@ -275,20 +277,20 @@ class Operation: lua = """ -- setting configuration table env.info("DCSLiberation|: setting configuration table") - + -- all data in this table is overridable. dcsLiberation = {} - + -- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory dcsLiberation.installPath=""" + state_location + """ - + -- you can override dcsLiberation.JTACAutoLase to make it use your own function ; it will be called with these parameters : ({jtac.unit_name}, {jtac.code}, {smoke}, 'vehicle') for all JTACs if ctld then dcsLiberation.JTACAutoLase=ctld.JTACAutoLase elseif JTACAutoLase then dcsLiberation.JTACAutoLase=JTACAutoLase end - + -- later, we'll add more data to the table --dcsLiberation.POIs = {} --dcsLiberation.BASEs = {} @@ -300,7 +302,7 @@ class Operation: self.current_mission.triggerrules.triggers.append(trigger) # Inject DCS-Liberation script if not done already in the plugins - if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice + if not "dcs_liberation.lua" in listOfPluginsScripts: # don't load the script twice trigger = TriggerStart(comment="Load DCS Liberation script") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua") trigger.add_action(DoScriptFile(fileref)) @@ -324,20 +326,19 @@ class Operation: trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - self.assign_channels_to_flights(airgen.flights, - airsupportgen.air_support) + self.assign_channels_to_flights() kneeboard_generator = KneeboardGenerator(self.current_mission) - for dynamic_runway in groundobjectgen.runways.values(): + for dynamic_runway in self.groundobjectgen.runways.values(): self.briefinggen.add_dynamic_runway(dynamic_runway) - for tanker in airsupportgen.air_support.tankers: + for tanker in self.airsupportgen.air_support.tankers: self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - for awacs in airsupportgen.air_support.awacs: + for awacs in self.airsupportgen.air_support.awacs: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) @@ -345,23 +346,21 @@ class Operation: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) - for flight in airgen.flights: + for flight in self.airgen.flights: self.briefinggen.add_flight(flight) kneeboard_generator.add_flight(flight) self.briefinggen.generate() kneeboard_generator.generate() - def assign_channels_to_flights(self, flights: List[FlightData], - air_support: AirSupport) -> None: + def assign_channels_to_flights(self) -> None: """Assigns preset radio channels for client flights.""" - for flight in flights: + for flight in self.airgen.flights: if not flight.client_units: continue - self.assign_channels_to_flight(flight, air_support) + self.assign_channels_to_flight(flight) - def assign_channels_to_flight(self, flight: FlightData, - air_support: AirSupport) -> None: + def assign_channels_to_flight(self, flight: FlightData) -> None: """Assigns preset radio channels for a client flight.""" airframe = flight.aircraft_type @@ -372,5 +371,4 @@ class Operation: return aircraft_data.channel_allocator.assign_channels_for_flight( - flight, air_support - ) + flight, self.airsupportgen.air_support) From ca48a42701dce5c74fb4c1fbf0040751f34fdff5 Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 00:59:18 +0200 Subject: [PATCH 33/48] Revert "Reverted automerge errors." This reverts commit 00ea8ac4 --- game/operation/operation.py | 122 ++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index ed2a018f..fe50f2ef 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,14 +1,13 @@ from typing import Set -from dcs.countries import country_dict -from dcs.lua.parse import loads -from dcs.terrain.terrain import Terrain - from gen import * from gen.airfields import AIRFIELD_DATA from gen.beacons import load_beacons_for_terrain from gen.radios import RadioRegistry from gen.tacan import TacanRegistry +from dcs.countries import country_dict +from dcs.lua.parse import loads +from dcs.terrain.terrain import Terrain from userdata.debriefing import * @@ -69,26 +68,8 @@ class Operation: def initialize(self, mission: Mission, conflict: Conflict): self.current_mission = mission self.conflict = conflict - self.radio_registry = RadioRegistry() - self.tacan_registry = TacanRegistry() - self.airgen = AircraftConflictGenerator( - mission, conflict, self.game.settings, self.game, - self.radio_registry) - self.airsupportgen = AirSupportConflictGenerator( - mission, conflict, self.game, self.radio_registry, - self.tacan_registry) - self.triggersgen = TriggersGenerator(mission, conflict, self.game) - self.visualgen = VisualGenerator(mission, conflict, self.game) - self.envgen = EnviromentGenerator(mission, conflict, self.game) - self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game) - self.groundobjectgen = GroundObjectsGenerator( - mission, - conflict, - self.game, - self.radio_registry, - self.tacan_registry - ) - self.briefinggen = BriefingGenerator(mission, conflict, self.game) + self.briefinggen = BriefingGenerator(self.current_mission, + self.conflict, self.game) def prepare(self, terrain: Terrain, is_quick: bool): with open("resources/default_options.lua", "r") as f: @@ -128,6 +109,9 @@ class Operation: self.defenders_starting_position = self.to_cp.at def generate(self): + radio_registry = RadioRegistry() + tacan_registry = TacanRegistry() + # Dedup beacon/radio frequencies, since some maps have some frequencies # used multiple times. beacons = load_beacons_for_terrain(self.game.theater.terrain.name) @@ -139,7 +123,7 @@ class Operation: logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: - self.tacan_registry.reserve(beacon.tacan_channel) + tacan_registry.reserve(beacon.tacan_channel) for airfield, data in AIRFIELD_DATA.items(): if data.theater == self.game.theater.terrain.name: @@ -151,16 +135,26 @@ class Operation: # beacon list. for frequency in unique_map_frequencies: - self.radio_registry.reserve(frequency) + radio_registry.reserve(frequency) # Generate meteo + envgen = EnviromentGenerator(self.current_mission, self.conflict, + self.game) if self.environment_settings is None: - self.environment_settings = self.envgen.generate() + self.environment_settings = envgen.generate() else: - self.envgen.load(self.environment_settings) + envgen.load(self.environment_settings) # Generate ground object first - self.groundobjectgen.generate() + + groundobjectgen = GroundObjectsGenerator( + self.current_mission, + self.conflict, + self.game, + radio_registry, + tacan_registry + ) + groundobjectgen.generate() # Generate destroyed units for d in self.game.get_destroyed_units(): @@ -182,7 +176,10 @@ class Operation: ) # Air Support (Tanker & Awacs) - self.airsupportgen.generate(self.is_awacs_enabled) + airsupportgen = AirSupportConflictGenerator( + self.current_mission, self.conflict, self.game, radio_registry, + tacan_registry) + airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map self.airgen.generate_flights( @@ -196,6 +193,7 @@ class Operation: self.groundobjectgen.runways ) + # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): @@ -206,32 +204,32 @@ class Operation: # Generate frontline ops player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] - groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, - player_cp.stances[enemy_cp.id]) + groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) groundConflictGen.generate() jtacs.extend(groundConflictGen.jtacs) # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 - if self.game.player_country in [country.name for country in - self.current_mission.coalition["blue"].countries.values()]: + if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]: self.current_mission.groundControl.blue_tactical_commander = self.ca_slots else: self.current_mission.groundControl.red_tactical_commander = self.ca_slots # Triggers - if self.game.is_player_attack(self.conflict.attackers_country): - cp = self.conflict.from_cp - else: - cp = self.conflict.to_cp - self.triggersgen.generate() + triggersgen = TriggersGenerator(self.current_mission, self.conflict, + self.game) + triggersgen.generate() # Options - self.forcedoptionsgen.generate() + forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, + self.conflict, self.game) + forcedoptionsgen.generate() # Generate Visuals Smoke Effects + visualgen = VisualGenerator(self.current_mission, self.conflict, + self.game) if self.game.settings.perf_smoke_gen: - self.visualgen.generate() + visualgen.generate() # Inject Plugins Lua Scripts listOfPluginsScripts = [] @@ -239,7 +237,7 @@ class Operation: if plugin_file_path.exists(): for line in plugin_file_path.read_text().splitlines(): name = line.strip() - if not name.startswith('#'): + if not name.startswith( '#' ): trigger = TriggerStart(comment="Load " + name) listOfPluginsScripts.append(name) fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name) @@ -250,21 +248,21 @@ class Operation: f"Not loading plugins, {plugin_file_path} does not exist") # Inject Mist Script if not done already in the plugins - if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice + if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice trigger = TriggerStart(comment="Load Mist Lua framework") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua") trigger.add_action(DoScriptFile(fileref)) self.current_mission.triggerrules.triggers.append(trigger) # Inject JSON library if not done already in the plugins - if not "json.lua" in listOfPluginsScripts: # don't load the script twice + if not "json.lua" in listOfPluginsScripts : # don't load the script twice trigger = TriggerStart(comment="Load JSON Lua library") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua") trigger.add_action(DoScriptFile(fileref)) self.current_mission.triggerrules.triggers.append(trigger) # Inject Ciribob's JTACAutoLase if not done already in the plugins - if not "JTACAutoLase.lua" in listOfPluginsScripts: # don't load the script twice + if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice trigger = TriggerStart(comment="Load JTACAutoLase.lua script") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua") trigger.add_action(DoScriptFile(fileref)) @@ -277,20 +275,20 @@ class Operation: lua = """ -- setting configuration table env.info("DCSLiberation|: setting configuration table") - + -- all data in this table is overridable. dcsLiberation = {} - + -- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory dcsLiberation.installPath=""" + state_location + """ - + -- you can override dcsLiberation.JTACAutoLase to make it use your own function ; it will be called with these parameters : ({jtac.unit_name}, {jtac.code}, {smoke}, 'vehicle') for all JTACs if ctld then dcsLiberation.JTACAutoLase=ctld.JTACAutoLase elseif JTACAutoLase then dcsLiberation.JTACAutoLase=JTACAutoLase end - + -- later, we'll add more data to the table --dcsLiberation.POIs = {} --dcsLiberation.BASEs = {} @@ -302,7 +300,7 @@ class Operation: self.current_mission.triggerrules.triggers.append(trigger) # Inject DCS-Liberation script if not done already in the plugins - if not "dcs_liberation.lua" in listOfPluginsScripts: # don't load the script twice + if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice trigger = TriggerStart(comment="Load DCS Liberation script") fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua") trigger.add_action(DoScriptFile(fileref)) @@ -326,19 +324,20 @@ class Operation: trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - self.assign_channels_to_flights() + self.assign_channels_to_flights(airgen.flights, + airsupportgen.air_support) kneeboard_generator = KneeboardGenerator(self.current_mission) - for dynamic_runway in self.groundobjectgen.runways.values(): + for dynamic_runway in groundobjectgen.runways.values(): self.briefinggen.add_dynamic_runway(dynamic_runway) - for tanker in self.airsupportgen.air_support.tankers: + for tanker in airsupportgen.air_support.tankers: self.briefinggen.add_tanker(tanker) kneeboard_generator.add_tanker(tanker) if self.is_awacs_enabled: - for awacs in self.airsupportgen.air_support.awacs: + for awacs in airsupportgen.air_support.awacs: self.briefinggen.add_awacs(awacs) kneeboard_generator.add_awacs(awacs) @@ -346,21 +345,23 @@ class Operation: self.briefinggen.add_jtac(jtac) kneeboard_generator.add_jtac(jtac) - for flight in self.airgen.flights: + for flight in airgen.flights: self.briefinggen.add_flight(flight) kneeboard_generator.add_flight(flight) self.briefinggen.generate() kneeboard_generator.generate() - def assign_channels_to_flights(self) -> None: + def assign_channels_to_flights(self, flights: List[FlightData], + air_support: AirSupport) -> None: """Assigns preset radio channels for client flights.""" - for flight in self.airgen.flights: + for flight in flights: if not flight.client_units: continue - self.assign_channels_to_flight(flight) + self.assign_channels_to_flight(flight, air_support) - def assign_channels_to_flight(self, flight: FlightData) -> None: + def assign_channels_to_flight(self, flight: FlightData, + air_support: AirSupport) -> None: """Assigns preset radio channels for a client flight.""" airframe = flight.aircraft_type @@ -371,4 +372,5 @@ class Operation: return aircraft_data.channel_allocator.assign_channels_for_flight( - flight, self.airsupportgen.air_support) + flight, air_support + ) From e664652cc56ff87292aaf217436021fab511749e Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 01:07:56 +0200 Subject: [PATCH 34/48] Now properly merged operation.py --- game/operation/operation.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index fe50f2ef..370de8a5 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -182,18 +182,21 @@ class Operation: airsupportgen.generate(self.is_awacs_enabled) # Generate Activity on the map - self.airgen.generate_flights( + airgen = AircraftConflictGenerator( + self.current_mission, self.conflict, self.game.settings, self.game, + radio_registry) + + airgen.generate_flights( self.current_mission.country(self.game.player_country), self.game.blue_ato, - self.groundobjectgen.runways + groundobjectgen.runways ) - self.airgen.generate_flights( + airgen.generate_flights( self.current_mission.country(self.game.enemy_country), self.game.red_ato, - self.groundobjectgen.runways + groundobjectgen.runways ) - # Generate ground units on frontline everywhere jtacs: List[JtacInfo] = [] for player_cp, enemy_cp in self.game.theater.conflicts(True): From 60ce6658ad1a011712022cb53c03a2992801c212 Mon Sep 17 00:00:00 2001 From: Khopa Date: Wed, 7 Oct 2020 01:12:16 +0200 Subject: [PATCH 35/48] Campaigns are sorted by terrain in new game wizard --- qt_ui/windows/newgame/QCampaignList.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index e83fae46..9329edfd 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -27,6 +27,7 @@ for f in campaign_files: logging.info("Loaded campaign :" + ff) except Exception as e: logging.info("Unable to load campaign :" + f) +CAMPAIGNS = sorted(CAMPAIGNS, key=lambda x: x[0]) class QCampaignItem(QStandardItem): From 5f1601a2dad037ed0bff0d63a86e6498a852a982 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 3 Oct 2020 13:34:40 -0700 Subject: [PATCH 36/48] Remove the userdata package. --- {userdata => game}/debriefing.py | 0 game/event/event.py | 19 ++++----------- game/event/frontlineattack.py | 2 +- game/game.py | 1 + game/operation/operation.py | 2 +- {userdata => game}/persistency.py | 0 {userdata => qt_ui}/liberation_install.py | 3 ++- {userdata => qt_ui}/liberation_theme.py | 24 +++++++++++++++---- {userdata => qt_ui}/logging_config.py | 0 qt_ui/main.py | 14 +++++++---- qt_ui/uiconstants.py | 18 +------------- qt_ui/windows/QDebriefingWindow.py | 13 +++++++--- qt_ui/windows/QLiberationWindow.py | 4 +--- .../windows/QWaitingForMissionResultWindow.py | 21 +++++++++++----- .../preferences/QLiberationPreferences.py | 23 +++++++++++------- userdata/__init__.py | 0 userdata/state.py | 10 -------- 17 files changed, 78 insertions(+), 76 deletions(-) rename {userdata => game}/debriefing.py (100%) rename {userdata => game}/persistency.py (100%) rename {userdata => qt_ui}/liberation_install.py (99%) rename {userdata => qt_ui}/liberation_theme.py (78%) rename {userdata => qt_ui}/logging_config.py (100%) delete mode 100644 userdata/__init__.py delete mode 100644 userdata/state.py diff --git a/userdata/debriefing.py b/game/debriefing.py similarity index 100% rename from userdata/debriefing.py rename to game/debriefing.py diff --git a/game/event/event.py b/game/event/event.py index 8146deb3..ea5cbb4c 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -1,25 +1,13 @@ -import typing -import logging - -from dcs.action import Coalition -from dcs.unittype import UnitType -from dcs.task import * -from dcs.vehicles import AirDefence from dcs.unittype import UnitType from game import * +from game import persistency +from game.debriefing import Debriefing from game.infos.information import Information -from theater import * from gen.environmentgen import EnvironmentSettings -from gen.conflictgen import Conflict -from game.db import assigned_units_from, unitdict_from +from theater import * from theater.start_generator import generate_airbase_defense_group -from userdata.debriefing import Debriefing -from userdata import persistency - -import game.db as db - DIFFICULTY_LOG_BASE = 1.1 EVENT_DEPARTURE_MAX_DISTANCE = 340000 @@ -28,6 +16,7 @@ MINOR_DEFEAT_INFLUENCE = 0.1 DEFEAT_INFLUENCE = 0.3 STRONG_DEFEAT_INFLUENCE = 0.5 + class Event: silent = False informational = False diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index e548440f..bde6c62d 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -1,6 +1,6 @@ from game.event import * from game.operation.frontlineattack import FrontlineAttackOperation -from userdata.debriefing import Debriefing +from ..debriefing import Debriefing class FrontlineAttackEvent(Event): diff --git a/game/game.py b/game/game.py index fc3e2305..d1177f62 100644 --- a/game/game.py +++ b/game/game.py @@ -4,6 +4,7 @@ from game.db import REWARDS, PLAYER_BUDGET_BASE, sys from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats from gen.ato import AirTaskingOrder +from gen.conflictgen import Conflict from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner import GroundPlanner diff --git a/game/operation/operation.py b/game/operation/operation.py index 370de8a5..56c262e0 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -8,7 +8,7 @@ from gen.tacan import TacanRegistry from dcs.countries import country_dict from dcs.lua.parse import loads from dcs.terrain.terrain import Terrain -from userdata.debriefing import * +from ..debriefing import Debriefing class Operation: diff --git a/userdata/persistency.py b/game/persistency.py similarity index 100% rename from userdata/persistency.py rename to game/persistency.py diff --git a/userdata/liberation_install.py b/qt_ui/liberation_install.py similarity index 99% rename from userdata/liberation_install.py rename to qt_ui/liberation_install.py index 5f19ec0a..0440043d 100644 --- a/userdata/liberation_install.py +++ b/qt_ui/liberation_install.py @@ -4,13 +4,14 @@ from shutil import copyfile import dcs -from userdata import persistency +from game import persistency global __dcs_saved_game_directory global __dcs_installation_directory PREFERENCES_FILE_PATH = "liberation_preferences.json" + def init(): global __dcs_saved_game_directory global __dcs_installation_directory diff --git a/userdata/liberation_theme.py b/qt_ui/liberation_theme.py similarity index 78% rename from userdata/liberation_theme.py rename to qt_ui/liberation_theme.py index 703d15ee..258a8683 100644 --- a/userdata/liberation_theme.py +++ b/qt_ui/liberation_theme.py @@ -1,7 +1,6 @@ import json import os - -import qt_ui.uiconstants as CONST +from typing import Dict global __theme_index @@ -10,6 +9,21 @@ THEME_PREFERENCES_FILE_PATH = "liberation_theme.json" DEFAULT_THEME_INDEX = 1 +# new themes can be added here +THEMES: Dict[int, Dict[str, str]] = { + 0: {'themeName': 'Vanilla', + 'themeFile': 'windows-style.css', + 'themeIcons': 'medium', + }, + + 1: {'themeName': 'DCS World', + 'themeFile': 'style-dcs.css', + 'themeIcons': 'light', + }, + +} + + def init(): global __theme_index @@ -49,19 +63,19 @@ def get_theme_index(): # get theme name based on current index def get_theme_name(): - theme_name = CONST.THEMES[get_theme_index()]['themeName'] + theme_name = THEMES[get_theme_index()]['themeName'] return theme_name # get theme icon sub-folder name based on current index def get_theme_icons(): - theme_icons = CONST.THEMES[get_theme_index()]['themeIcons'] + theme_icons = THEMES[get_theme_index()]['themeIcons'] return str(theme_icons) # get theme stylesheet css based on current index def get_theme_css_file(): - theme_file = CONST.THEMES[get_theme_index()]['themeFile'] + theme_file = THEMES[get_theme_index()]['themeFile'] return str(theme_file) diff --git a/userdata/logging_config.py b/qt_ui/logging_config.py similarity index 100% rename from userdata/logging_config.py rename to qt_ui/logging_config.py diff --git a/qt_ui/main.py b/qt_ui/main.py index e019d32c..9503fb58 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -1,5 +1,3 @@ -from userdata import logging_config - import logging import os import sys @@ -9,11 +7,17 @@ from PySide2 import QtWidgets from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QSplashScreen -from qt_ui import uiconstants +from game import persistency +from qt_ui import ( + liberation_install, + liberation_theme, + logging_config, + uiconstants, +) from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.QLiberationWindow import QLiberationWindow -from qt_ui.windows.preferences.QLiberationFirstStartWindow import QLiberationFirstStartWindow -from userdata import liberation_install, persistency, liberation_theme +from qt_ui.windows.preferences.QLiberationFirstStartWindow import \ + QLiberationFirstStartWindow # Logging setup logging_config.init_logging(uiconstants.VERSION_STRING) diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index f822b0fc..706ff3ab 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -1,12 +1,10 @@ -# URL for UI links import os from typing import Dict from PySide2.QtGui import QColor, QFont, QPixmap -from game.event import UnitsDeliveryEvent, FrontlineAttackEvent from theater.theatergroundobject import CATEGORY_MAP -from userdata.liberation_theme import get_theme_icons +from .liberation_theme import get_theme_icons VERSION_STRING = "2.1.4" @@ -28,20 +26,6 @@ FONT_PRIMARY_I = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=True) FONT_PRIMARY_B = QFont(FONT_NAME, FONT_SIZE, weight=75, italic=False) FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False) -# new themes can be added here -THEMES: Dict[int, Dict[str, str]] = { - 0: {'themeName': 'Vanilla', - 'themeFile': 'windows-style.css', - 'themeIcons': 'medium', - }, - - 1: {'themeName': 'DCS World', - 'themeFile': 'style-dcs.css', - 'themeIcons': 'light', - }, - -} - COLORS: Dict[str, QColor] = { "white": QColor(255, 255, 255), "white_transparent": QColor(255, 255, 255, 35), diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py index 752d19d5..e0ecce57 100644 --- a/qt_ui/windows/QDebriefingWindow.py +++ b/qt_ui/windows/QDebriefingWindow.py @@ -1,8 +1,15 @@ from PySide2.QtGui import QIcon, QPixmap -from PySide2.QtWidgets import QLabel, QDialog, QVBoxLayout, QGroupBox, QGridLayout, QPushButton +from PySide2.QtWidgets import ( + QDialog, + QGridLayout, + QGroupBox, + QLabel, + QPushButton, + QVBoxLayout, +) -from game.game import Event, db, Game -from userdata.debriefing import Debriefing +from game.debriefing import Debriefing +from game.game import Event, Game, db class QDebriefingWindow(QDialog): diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 50a4b120..3933083c 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -17,8 +17,7 @@ from PySide2.QtWidgets import ( ) import qt_ui.uiconstants as CONST -from game import Game -from game.inventory import GlobalAircraftInventory +from game import Game, persistency from qt_ui.dialogs import Dialog from qt_ui.models import GameModel from qt_ui.uiconstants import URLS @@ -31,7 +30,6 @@ from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard from qt_ui.windows.preferences.QLiberationPreferencesWindow import \ QLiberationPreferencesWindow -from userdata import persistency class QLiberationWindow(QMainWindow): diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index be56c99c..b93d3256 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -2,15 +2,24 @@ import json import os from PySide2 import QtCore -from PySide2.QtCore import QObject, Signal, Qt -from PySide2.QtGui import QMovie, QIcon, QPixmap -from PySide2.QtWidgets import QLabel, QDialog, QGroupBox, QGridLayout, QPushButton, QFileDialog, QMessageBox, QTextEdit, \ - QHBoxLayout +from PySide2.QtCore import QObject, Qt, Signal +from PySide2.QtGui import QIcon, QMovie, QPixmap +from PySide2.QtWidgets import ( + QDialog, + QFileDialog, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QTextEdit, +) +from game.debriefing import Debriefing, wait_for_debriefing from game.game import Event, Game, logging +from game.persistency import base_path from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from userdata.debriefing import wait_for_debriefing, Debriefing -from userdata.persistency import base_path class DebriefingFileWrittenSignal(QObject): diff --git a/qt_ui/windows/preferences/QLiberationPreferences.py b/qt_ui/windows/preferences/QLiberationPreferences.py index 074c1c17..bc392561 100644 --- a/qt_ui/windows/preferences/QLiberationPreferences.py +++ b/qt_ui/windows/preferences/QLiberationPreferences.py @@ -1,16 +1,21 @@ import os -from PySide2 import QtWidgets -from PySide2.QtCore import QFile from PySide2.QtGui import Qt -from PySide2.QtWidgets import QFrame, QLineEdit, QGridLayout, QVBoxLayout, QLabel, QPushButton, \ - QFileDialog, QMessageBox, QDialog, QComboBox, QApplication -import qt_ui.uiconstants as CONST -import sys +from PySide2.QtWidgets import ( + QComboBox, + QFileDialog, + QFrame, + QGridLayout, + QLabel, + QLineEdit, + QMessageBox, + QPushButton, + QVBoxLayout, +) -import userdata -from userdata import liberation_install, liberation_theme -from userdata.liberation_theme import get_theme_index, set_theme_index +import qt_ui.uiconstants as CONST +from qt_ui import liberation_install, liberation_theme +from qt_ui.liberation_theme import get_theme_index, set_theme_index class QLiberationPreferences(QFrame): diff --git a/userdata/__init__.py b/userdata/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/userdata/state.py b/userdata/state.py deleted file mode 100644 index 846f5893..00000000 --- a/userdata/state.py +++ /dev/null @@ -1,10 +0,0 @@ - - -class RunningMissionState: - - killed_aircrafts = [] - killed_ground_units = [] - weapons_fired = [] - - def __init__(self): - pass \ No newline at end of file From 1808e5bccf12d3254e008aeeeecec936ecdb0a40 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 3 Oct 2020 13:49:39 -0700 Subject: [PATCH 37/48] Add initial mypy.ini. --- mypy.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..1f9f6f7d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +namespace_packages = True + +[mypy-PIL.*] +ignore_missing_imports = True + +[mypy-winreg.*] +ignore_missing_imports = True \ No newline at end of file From db6b6602700abae7c8178c3a99211325a819c787 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 3 Oct 2020 14:58:44 -0700 Subject: [PATCH 38/48] Fix mypy issues in all modules except qt_ui. --- game/data/cap_capabilities_db.py | 20 +- game/data/radar_db.py | 23 +- game/db.py | 234 ++++++++++++++---- game/event/event.py | 44 ++-- game/event/frontlineattack.py | 20 +- game/factions/australia_2005.py | 28 ++- game/factions/bluefor_coldwar.py | 31 ++- game/factions/bluefor_coldwar_a4.py | 32 ++- game/factions/bluefor_coldwar_mods.py | 32 ++- game/factions/bluefor_modern.py | 46 +++- game/factions/canada_2005.py | 27 +- game/factions/china_2010.py | 39 ++- game/factions/france_1995.py | 29 ++- game/factions/france_2005.py | 32 ++- game/factions/france_modded.py | 33 ++- game/factions/germany_1944.py | 15 +- game/factions/germany_1944_easy.py | 15 +- game/factions/germany_1990.py | 29 ++- game/factions/india_2010.py | 33 ++- game/factions/insurgent.py | 15 +- game/factions/insurgent_modded.py | 21 +- game/factions/iran_2015.py | 36 ++- game/factions/israel_1948.py | 20 +- game/factions/israel_1973.py | 27 +- game/factions/israel_2000.py | 30 ++- game/factions/italy_1990.py | 29 ++- game/factions/italy_1990_mb339.py | 29 ++- game/factions/japan_2005.py | 25 +- game/factions/libya_2011.py | 25 +- game/factions/netherlands_1990.py | 26 +- game/factions/north_korea_2000.py | 34 ++- game/factions/pakistan_2015.py | 26 +- game/factions/private_miltary_companies.py | 26 +- game/factions/russia_1955.py | 11 +- game/factions/russia_1965.py | 21 +- game/factions/russia_1975.py | 33 ++- game/factions/russia_1990.py | 40 ++- game/factions/russia_2010.py | 43 +++- game/factions/spain_1990.py | 26 +- game/factions/sweden_1990.py | 22 +- game/factions/syria.py | 35 ++- game/factions/turkey_2005.py | 27 +- game/factions/uae_2005.py | 28 ++- game/factions/uk_1944.py | 19 +- game/factions/uk_1990.py | 30 ++- game/factions/ukraine_2010.py | 34 ++- game/factions/us_aggressors.py | 37 ++- game/factions/usa_1944.py | 20 +- game/factions/usa_1955.py | 23 +- game/factions/usa_1960.py | 26 +- game/factions/usa_1965.py | 27 +- game/factions/usa_1990.py | 35 ++- game/factions/usa_2005.py | 37 ++- game/game.py | 63 ++--- game/inventory.py | 11 +- game/models/game_stats.py | 6 +- game/operation/frontlineattack.py | 7 +- game/operation/operation.py | 62 +++-- game/persistency.py | 3 +- gen/__init__.py | 2 - gen/aaa.py | 51 ---- gen/aircraft.py | 145 +++++++---- gen/airfields.py | 6 +- gen/airsupportgen.py | 19 +- gen/armor.py | 40 ++- gen/conflictgen.py | 65 ++--- gen/environmentgen.py | 15 +- gen/fleet/ship_group_generator.py | 6 +- gen/flights/ai_flight_planner.py | 11 +- gen/flights/ai_flight_planner_db.py | 77 +++++- gen/flights/flight.py | 26 +- gen/flights/flightplan.py | 2 + gen/flights/waypointbuilder.py | 12 +- gen/ground_forces/ai_ground_planner.py | 43 ++-- gen/groundobjectsgen.py | 24 +- gen/kneeboard.py | 20 +- gen/missiles/missiles_group_generator.py | 6 +- gen/triggergen.py | 19 +- gen/visualgen.py | 30 +-- mypy.ini | 4 + .../windows/QWaitingForMissionResultWindow.py | 2 +- qt_ui/windows/basemenu/QBaseMenu2.py | 3 +- theater/base.py | 30 +-- theater/conflicttheater.py | 51 ++-- theater/controlpoint.py | 38 ++- theater/landmap.py | 10 +- theater/start_generator.py | 30 ++- theater/theatergroundobject.py | 7 +- 88 files changed, 1990 insertions(+), 661 deletions(-) delete mode 100644 gen/aaa.py diff --git a/game/data/cap_capabilities_db.py b/game/data/cap_capabilities_db.py index eb367238..438a0f80 100644 --- a/game/data/cap_capabilities_db.py +++ b/game/data/cap_capabilities_db.py @@ -1,4 +1,22 @@ -from dcs.planes import * +from dcs.planes import ( + Bf_109K_4, + C_101CC, + FW_190A8, + FW_190D9, + F_5E_3, + F_86F_Sabre, + I_16, + L_39ZA, + MiG_15bis, + MiG_19P, + MiG_21Bis, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) + from pydcs_extensions.a4ec.a4ec import A_4E_C """ diff --git a/game/data/radar_db.py b/game/data/radar_db.py index c3c9e25a..4e90d56c 100644 --- a/game/data/radar_db.py +++ b/game/data/radar_db.py @@ -1,5 +1,26 @@ +from dcs.ships import ( + CGN_1144_2_Pyotr_Velikiy, + CG_1164_Moskva, + CVN_70_Carl_Vinson, + CVN_71_Theodore_Roosevelt, + CVN_72_Abraham_Lincoln, + CVN_73_George_Washington, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + CV_1143_5_Admiral_Kuznetsov_2017, + FFG_11540_Neustrashimy, + FFL_1124_4_Grisha, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + Type_052B_Destroyer, + Type_052C_Destroyer, + Type_054A_Frigate, + USS_Arleigh_Burke_IIa, +) from dcs.vehicles import AirDefence -from dcs.ships import * UNITS_WITH_RADAR = [ diff --git a/game/db.py b/game/db.py index 45ddfdf7..6ffa6cd7 100644 --- a/game/db.py +++ b/game/db.py @@ -1,34 +1,176 @@ -import typing -import enum from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional, Tuple, Type, Union -from dcs.countries import get_by_id, country_dict -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * - -from dcs.task import * -from dcs.unit import * -from dcs.unittype import * -from dcs.unitgroup import * +from dcs.countries import country_dict +from dcs.helicopters import ( + AH_1W, + AH_64A, + AH_64D, + HelicopterType, + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, + OH_58D, + SA342L, + SA342M, + SA342Minigun, + SA342Mistral, + UH_1H, + UH_60A, + helicopter_map, +) +from dcs.mapping import Point +# mypy can't resolve these if they're wildcard imports for some reason. +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + A_20G, + A_50, + An_26B, + An_30M, + B_17G, + B_1B, + B_52H, + Bf_109K_4, + C_101CC, + C_130, + E_3A, + FA_18C_hornet, + FW_190A8, + FW_190D9, + F_14B, + F_15C, + F_15E, + F_16A, + F_16C_50, + F_4E, + F_5E_3, + F_86F_Sabre, + F_A_18C, + IL_76MD, + IL_78M, + JF_17, + J_11A, + Ju_88A4, + KC130, + KC_135, + KJ_2000, + L_39C, + L_39ZA, + MQ_9_Reaper, + M_2000C, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_27K, + MiG_29A, + MiG_29G, + MiG_29S, + MiG_31, + Mirage_2000_5, + P_47D_30, + P_47D_30bl1, + P_47D_40, + P_51D, + P_51D_30_NA, + PlaneType, + RQ_1A_Predator, + S_3B_Tanker, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_27, + Su_30, + Su_33, + Su_34, + Tornado_GR4, + Tornado_IDS, + WingLoong_I, + Yak_40, + plane_map, +) +from dcs.ships import ( + Armed_speedboat, + Bulk_cargo_ship_Yakushev, + CVN_71_Theodore_Roosevelt, + CVN_72_Abraham_Lincoln, + CVN_73_George_Washington, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + CV_1143_5_Admiral_Kuznetsov_2017, + Dry_cargo_ship_Ivanov, + LHA_1_Tarawa, + Tanker_Elnya_160, + ship_map, +) +from dcs.task import ( + AWACS, + AntishipStrike, + CAP, + CAS, + CargoTransportation, + Embarking, + GroundAttack, + Intercept, + MainTask, + Nothing, + PinpointStrike, + Reconnaissance, + Refueling, + SEAD, + Task, + Transport, +) +from dcs.terrain.terrain import Airport +from dcs.unit import Ship, Unit, Vehicle +from dcs.unitgroup import ShipGroup, StaticGroup +from dcs.unittype import FlyingType, ShipType, UnitType, VehicleType +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Carriage, + Infantry, + Unarmed, + vehicle_map, +) +import pydcs_extensions.frenchpack.frenchpack as frenchpack from game.factions.australia_2005 import Australia_2005 from game.factions.bluefor_coldwar import BLUEFOR_COLDWAR from game.factions.bluefor_coldwar_a4 import BLUEFOR_COLDWAR_A4 from game.factions.bluefor_coldwar_mods import BLUEFOR_COLDWAR_MODS +from game.factions.bluefor_modern import BLUEFOR_MODERN from game.factions.canada_2005 import Canada_2005 from game.factions.china_2010 import China_2010 from game.factions.france_1995 import France_1995 from game.factions.france_2005 import France_2005 from game.factions.france_modded import France_2005_Modded +from game.factions.germany_1944 import Germany_1944 from game.factions.germany_1944_easy import Germany_1944_Easy from game.factions.germany_1990 import Germany_1990 +from game.factions.india_2010 import India_2010 from game.factions.insurgent import Insurgent from game.factions.insurgent_modded import Insurgent_modded from game.factions.iran_2015 import Iran_2015 from game.factions.israel_1948 import Israel_1948 -from game.factions.israel_1973 import Israel_1973, Israel_1973_NO_WW2_UNITS, Israel_1982 +from game.factions.israel_1973 import ( + Israel_1973, + Israel_1973_NO_WW2_UNITS, + Israel_1982, +) from game.factions.israel_2000 import Israel_2000 from game.factions.italy_1990 import Italy_1990 from game.factions.italy_1990_mb339 import Italy_1990_MB339 @@ -37,35 +179,41 @@ from game.factions.libya_2011 import Libya_2011 from game.factions.netherlands_1990 import Netherlands_1990 from game.factions.north_korea_2000 import NorthKorea_2000 from game.factions.pakistan_2015 import Pakistan_2015 -from game.factions.private_miltary_companies import PMC_WESTERN_B, PMC_RUSSIAN, PMC_WESTERN_A -from game.factions.russia_1975 import Russia_1975 -from game.factions.germany_1944 import Germany_1944 -from game.factions.india_2010 import India_2010 +from game.factions.private_miltary_companies import ( + PMC_RUSSIAN, + PMC_WESTERN_A, + PMC_WESTERN_B, +) from game.factions.russia_1955 import Russia_1955 from game.factions.russia_1965 import Russia_1965 +from game.factions.russia_1975 import Russia_1975 from game.factions.russia_1990 import Russia_1990 from game.factions.russia_2010 import Russia_2010 from game.factions.spain_1990 import Spain_1990 from game.factions.sweden_1990 import Sweden_1990 -from game.factions.syria import Syria_2011, Syria_1967, Syria_1967_WW2_Weapons, Syria_1973, Arab_Armies_1948, Syria_1982 +from game.factions.syria import ( + Arab_Armies_1948, + Syria_1967, + Syria_1967_WW2_Weapons, + Syria_1973, + Syria_1982, + Syria_2011, +) from game.factions.turkey_2005 import Turkey_2005 from game.factions.uae_2005 import UAE_2005 from game.factions.uk_1944 import UK_1944 from game.factions.uk_1990 import UnitedKingdom_1990 from game.factions.ukraine_2010 import Ukraine_2010 from game.factions.us_aggressors import US_Aggressors -from game.factions.usa_1944 import USA_1944, ALLIES_1944 +from game.factions.usa_1944 import ALLIES_1944, USA_1944 from game.factions.usa_1955 import USA_1955 from game.factions.usa_1960 import USA_1960 from game.factions.usa_1965 import USA_1965 from game.factions.usa_1990 import USA_1990 from game.factions.usa_2005 import USA_2005 -from game.factions.bluefor_modern import BLUEFOR_MODERN - # PATCH pydcs data with MODS from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN -import pydcs_extensions.frenchpack.frenchpack as frenchpack from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M plane_map["A-4E-C"] = A_4E_C @@ -793,13 +941,13 @@ SAM_CONVERT = { """ Units that will always be spawned in the air """ -TAKEOFF_BAN = [ +TAKEOFF_BAN: List[Type[FlyingType]] = [ ] """ Units that will be always spawned in the air if launched from the carrier """ -CARRIER_TAKEOFF_BAN = [ +CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [ Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned ] @@ -807,7 +955,7 @@ CARRIER_TAKEOFF_BAN = [ Units separated by country. country : DCS Country name """ -FACTIONS: typing.Dict[str, typing.Dict[str, typing.Any]] = { +FACTIONS: Dict[str, Dict[str, Any]] = { "Bluefor Modern": BLUEFOR_MODERN, "Bluefor Cold War 1970s": BLUEFOR_COLDWAR, @@ -937,7 +1085,7 @@ COMMON_OVERRIDE = { GroundAttack: "STRIKE" } -PLANE_PAYLOAD_OVERRIDES = { +PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { FA_18C_hornet: { CAP: "CAP HEAVY", @@ -1134,17 +1282,17 @@ LHA_CAPABLE = [ ---------- END OF CONFIGURATION SECTION """ -UnitsDict = typing.Dict[UnitType, int] -PlaneDict = typing.Dict[FlyingType, int] -HeliDict = typing.Dict[HelicopterType, int] -ArmorDict = typing.Dict[VehicleType, int] -ShipDict = typing.Dict[ShipType, int] -AirDefenseDict = typing.Dict[AirDefence, int] +UnitsDict = Dict[UnitType, int] +PlaneDict = Dict[FlyingType, int] +HeliDict = Dict[HelicopterType, int] +ArmorDict = Dict[VehicleType, int] +ShipDict = Dict[ShipType, int] +AirDefenseDict = Dict[AirDefence, int] -AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]] -TaskForceDict = typing.Dict[typing.Type[MainTask], AssignedUnitsDict] +AssignedUnitsDict = Dict[Type[UnitType], Tuple[int, int]] +TaskForceDict = Dict[Type[MainTask], AssignedUnitsDict] -StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]] +StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point] def upgrade_to_supercarrier(unit, name: str): @@ -1162,7 +1310,7 @@ def upgrade_to_supercarrier(unit, name: str): else: return unit -def unit_task(unit: UnitType) -> Task: +def unit_task(unit: UnitType) -> Optional[Task]: for task, units in UNIT_BY_TASK.items(): if unit in units: return task @@ -1173,10 +1321,10 @@ def unit_task(unit: UnitType) -> Task: print(unit.name + " cause issue") return None -def find_unittype(for_task: Task, country_name: str) -> typing.List[UnitType]: +def find_unittype(for_task: Task, country_name: str) -> List[UnitType]: return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name]["units"]] -def find_infantry(country_name: str) -> typing.List[UnitType]: +def find_infantry(country_name: str) -> List[UnitType]: inf = [ Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Soldier_RPG, @@ -1199,7 +1347,7 @@ def unit_type_name(unit_type) -> str: def unit_type_name_2(unit_type) -> str: return unit_type.name and unit_type.name or unit_type.id -def unit_type_from_name(name: str) -> UnitType: +def unit_type_from_name(name: str) -> Optional[UnitType]: if name in vehicle_map: return vehicle_map[name] elif name in plane_map: @@ -1232,7 +1380,7 @@ def task_name(task) -> str: return task.name -def choose_units(for_task: Task, factor: float, count: int, country: str) -> typing.Collection[UnitType]: +def choose_units(for_task: Task, factor: float, count: int, country: str) -> List[UnitType]: suitable_unittypes = find_unittype(for_task, country) suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()] suitable_unittypes.sort(key=lambda x: PRICES[x]) @@ -1258,7 +1406,7 @@ def unitdict_merge(a: UnitsDict, b: UnitsDict) -> UnitsDict: def unitdict_split(unit_dict: UnitsDict, count: int): - buffer_dict = {} + buffer_dict: Dict[UnitType, int] = {} for unit_type, unit_count in unit_dict.items(): for _ in range(unit_count): unitdict_append(buffer_dict, unit_type, 1) @@ -1281,7 +1429,7 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict return {} -def assigned_units_split(fd: AssignedUnitsDict) -> typing.Tuple[PlaneDict, PlaneDict]: +def assigned_units_split(fd: AssignedUnitsDict) -> Tuple[PlaneDict, PlaneDict]: return {k: v1 for k, (v1, v2) in fd.items()}, {k: v2 for k, (v1, v2) in fd.items()}, @@ -1290,7 +1438,7 @@ def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict: def assignedunits_split_to_count(dict: AssignedUnitsDict, count: int): - buffer_dict = {} + buffer_dict: Dict[Type[UnitType], Tuple[int, int]] = {} for unit_type, (unit_count, client_count) in dict.items(): for _ in range(unit_count): new_count, new_client_count = buffer_dict.get(unit_type, (0, 0)) diff --git a/game/event/event.py b/game/event/event.py index ea5cbb4c..0af3852c 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -1,13 +1,25 @@ +from __future__ import annotations + +import logging +import math +from typing import Dict, List, Optional, Type, TYPE_CHECKING + +from dcs.mapping import Point +from dcs.task import Task from dcs.unittype import UnitType -from game import * -from game import persistency +from game import db, persistency from game.debriefing import Debriefing from game.infos.information import Information +from game.operation.operation import Operation from gen.environmentgen import EnvironmentSettings -from theater import * +from gen.ground_forces.combat_stance import CombatStance +from theater import ControlPoint from theater.start_generator import generate_airbase_defense_group +if TYPE_CHECKING: + from ..game import Game + DIFFICULTY_LOG_BASE = 1.1 EVENT_DEPARTURE_MAX_DISTANCE = 340000 @@ -26,7 +38,6 @@ class Event: game = None # type: Game location = None # type: Point from_cp = None # type: ControlPoint - departure_cp = None # type: ControlPoint to_cp = None # type: ControlPoint operation = None # type: Operation @@ -36,7 +47,7 @@ class Event: def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): self.game = game - self.departure_cp = None + self.departure_cp: Optional[ControlPoint] = None self.from_cp = from_cp self.to_cp = target_cp self.location = location @@ -48,14 +59,14 @@ class Event: return self.attacker_name == self.game.player_name @property - def enemy_cp(self) -> ControlPoint: + def enemy_cp(self) -> Optional[ControlPoint]: if self.attacker_name == self.game.player_name: return self.to_cp else: return self.departure_cp @property - def tasks(self) -> typing.Collection[typing.Type[Task]]: + def tasks(self) -> List[Type[Task]]: return [] @property @@ -80,18 +91,6 @@ class Event: def is_successfull(self, debriefing: Debriefing) -> bool: return self.operation.is_successfull(debriefing) - def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict): - if self.is_player_attacking: - self.departure_cp = cp - else: - self.to_cp = cp - - def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict): - if self.is_player_attacking: - self.departure_cp = cp - else: - self.to_cp = cp - def generate(self): self.operation.is_awacs_enabled = self.is_awacs_enabled self.operation.ca_slots = self.ca_slots @@ -242,7 +241,7 @@ class Event: for enemy_cp in enemy_cps: print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name) - delta = 0 + delta = 0.0 player_won = True ally_casualties = killed_unit_count_by_cp[cp.id] enemy_casualties = killed_unit_count_by_cp[enemy_cp.id] @@ -365,7 +364,6 @@ class Event: class UnitsDeliveryEvent(Event): informational = True - units = None # type: typing.Dict[UnitType, int] def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): super(UnitsDeliveryEvent, self).__init__(game=game, @@ -375,12 +373,12 @@ class UnitsDeliveryEvent(Event): attacker_name=attacker_name, defender_name=defender_name) - self.units = {} + self.units: Dict[UnitType, int] = {} def __str__(self): return "Pending delivery to {}".format(self.to_cp) - def deliver(self, units: typing.Dict[UnitType, int]): + def deliver(self, units: Dict[UnitType, int]): for k, v in units.items(): self.units[k] = self.units.get(k, 0) + v diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index bde6c62d..0046526d 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -1,12 +1,17 @@ -from game.event import * +from typing import List, Type + +from dcs.task import CAP, CAS, Task + +from game import db from game.operation.frontlineattack import FrontlineAttackOperation +from .event import Event from ..debriefing import Debriefing class FrontlineAttackEvent(Event): @property - def tasks(self) -> typing.Collection[typing.Type[Task]]: + def tasks(self) -> List[Type[Task]]: if self.is_player_attacking: return [CAS, CAP] else: @@ -34,6 +39,7 @@ class FrontlineAttackEvent(Event): self.to_cp.base.affect_strength(-0.1) def player_attacking(self, flights: db.TaskForceDict): + assert self.departure_cp is not None op = FrontlineAttackOperation(game=self.game, attacker_name=self.attacker_name, defender_name=self.defender_name, @@ -41,13 +47,3 @@ class FrontlineAttackEvent(Event): departure_cp=self.departure_cp, to_cp=self.to_cp) self.operation = op - - def player_defending(self, flights: db.TaskForceDict): - op = FrontlineAttackOperation(game=self.game, - attacker_name=self.attacker_name, - defender_name=self.defender_name, - from_cp=self.from_cp, - departure_cp=self.departure_cp, - to_cp=self.to_cp) - self.operation = op - diff --git a/game/factions/australia_2005.py b/game/factions/australia_2005.py index df4972e6..c9cfb320 100644 --- a/game/factions/australia_2005.py +++ b/game/factions/australia_2005.py @@ -1,7 +1,27 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Australia_2005 = { "country": "Australia", diff --git a/game/factions/bluefor_coldwar.py b/game/factions/bluefor_coldwar.py index 5db15d73..c241bbae 100644 --- a/game/factions/bluefor_coldwar.py +++ b/game/factions/bluefor_coldwar.py @@ -1,7 +1,30 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) BLUEFOR_COLDWAR = { "country": "Combined Joint Task Forces Blue", diff --git a/game/factions/bluefor_coldwar_a4.py b/game/factions/bluefor_coldwar_a4.py index 74983134..ce6cf016 100644 --- a/game/factions/bluefor_coldwar_a4.py +++ b/game/factions/bluefor_coldwar_a4.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C diff --git a/game/factions/bluefor_coldwar_mods.py b/game/factions/bluefor_coldwar_mods.py index aece4e46..a395fc48 100644 --- a/game/factions/bluefor_coldwar_mods.py +++ b/game/factions/bluefor_coldwar_mods.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + A_10A, + C_130, + E_3A, + F_14B, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/bluefor_modern.py b/game/factions/bluefor_modern.py index 8db0ad89..9f97827b 100644 --- a/game/factions/bluefor_modern.py +++ b/game/factions/bluefor_modern.py @@ -1,7 +1,45 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + Ka_50, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_16C_50, + F_5E_3, + JF_17, + KC130, + KC_135, + M_2000C, + Su_25T, + Su_27, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) BLUEFOR_MODERN = { "country": "Combined Joint Task Forces Blue", diff --git a/game/factions/canada_2005.py b/game/factions/canada_2005.py index ea4497ca..7a664709 100644 --- a/game/factions/canada_2005.py +++ b/game/factions/canada_2005.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Canada_2005 = { "country": "Canada", diff --git a/game/factions/china_2010.py b/game/factions/china_2010.py index 0d98d1b9..577fb33d 100644 --- a/game/factions/china_2010.py +++ b/game/factions/china_2010.py @@ -1,7 +1,38 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_28N, + Mi_8MT, +) +from dcs.planes import ( + An_26B, + An_30M, + IL_76MD, + IL_78M, + JF_17, + J_11A, + KJ_2000, + MiG_21Bis, + Su_30, + Su_33, + WingLoong_I, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, + Type_052B_Destroyer, + Type_052C_Destroyer, + Type_054A_Frigate, + Type_071_Amphibious_Transport_Dock, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) China_2010 = { "country": "China", diff --git a/game/factions/france_1995.py b/game/factions/france_1995.py index acf56495..a14f24e5 100644 --- a/game/factions/france_1995.py +++ b/game/factions/france_1995.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) France_1995 = { "country": "France", diff --git a/game/factions/france_2005.py b/game/factions/france_2005.py index b2f4b87a..28c00ae4 100644 --- a/game/factions/france_2005.py +++ b/game/factions/france_2005.py @@ -1,7 +1,31 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) France_2005 = { "country": "France", diff --git a/game/factions/france_modded.py b/game/factions/france_modded.py index ad0f7de5..8283d090 100644 --- a/game/factions/france_modded.py +++ b/game/factions/france_modded.py @@ -1,10 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + SA342Mistral, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) import pydcs_extensions.frenchpack.frenchpack as frenchpack -from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S +from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M France_2005_Modded = { "country": "France", diff --git a/game/factions/germany_1944.py b/game/factions/germany_1944.py index 9123b350..c63f88f4 100644 --- a/game/factions/germany_1944.py +++ b/game/factions/germany_1944.py @@ -1,5 +1,16 @@ -from dcs.planes import * -from dcs.vehicles import * +from dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_GERMANY_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/germany_1944_easy.py b/game/factions/germany_1944_easy.py index b79d45f0..8be93457 100644 --- a/game/factions/germany_1944_easy.py +++ b/game/factions/germany_1944_easy.py @@ -1,5 +1,16 @@ -from dcs.planes import * -from dcs.vehicles import * +from dcs.planes import ( + Bf_109K_4, + FW_190A8, + FW_190D9, + Ju_88A4, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_GERMANY_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/germany_1990.py b/game/factions/germany_1990.py index ae1f0668..54432ef4 100644 --- a/game/factions/germany_1990.py +++ b/game/factions/germany_1990.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_4E, + KC130, + KC_135, + MiG_29G, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Germany_1990 = { "country": "Germany", diff --git a/game/factions/india_2010.py b/game/factions/india_2010.py index 2dc756ec..756faa23 100644 --- a/game/factions/india_2010.py +++ b/game/factions/india_2010.py @@ -1,7 +1,32 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + Mi_8MT, +) +from dcs.planes import ( + C_130, + E_3A, + KC130, + KC_135, + M_2000C, + MiG_21Bis, + MiG_27K, + MiG_29S, + Mirage_2000_5, + Su_30, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + FSG_1241_1MP_Molniya, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) India_2010 = { "country": "India", diff --git a/game/factions/insurgent.py b/game/factions/insurgent.py index d94603c6..66ae3459 100644 --- a/game/factions/insurgent.py +++ b/game/factions/insurgent.py @@ -1,7 +1,14 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Insurgent = { "country": "Insurgents", diff --git a/game/factions/insurgent_modded.py b/game/factions/insurgent_modded.py index 39e1e174..b19b4344 100644 --- a/game/factions/insurgent_modded.py +++ b/game/factions/insurgent_modded.py @@ -1,8 +1,21 @@ -from dcs.ships import * -from dcs.vehicles import * +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) -from pydcs_extensions.frenchpack.frenchpack import DIM__TOYOTA_BLUE, DIM__TOYOTA_DESERT, DIM__TOYOTA_GREEN, \ - DIM__KAMIKAZE +from pydcs_extensions.frenchpack.frenchpack import ( + DIM__KAMIKAZE, + DIM__TOYOTA_BLUE, + DIM__TOYOTA_DESERT, + DIM__TOYOTA_GREEN, +) Insurgent_modded = { "country": "Insurgents", diff --git a/game/factions/iran_2015.py b/game/factions/iran_2015.py index 56751a2c..53f20dd6 100644 --- a/game/factions/iran_2015.py +++ b/game/factions/iran_2015.py @@ -1,7 +1,35 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_28N, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + F_14B, + F_4E, + F_5E_3, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_29A, + Su_17M4, + Su_24M, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Iran_2015 = { "country": "Iran", diff --git a/game/factions/israel_1948.py b/game/factions/israel_1948.py index ffa57fcb..bc3b615c 100644 --- a/game/factions/israel_1948.py +++ b/game/factions/israel_1948.py @@ -1,6 +1,20 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + B_17G, + Bf_109K_4, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Israel_1948 = { "country": "Israel", diff --git a/game/factions/israel_1973.py b/game/factions/israel_1973.py index 00624ec9..3bfa5d15 100644 --- a/game/factions/israel_1973.py +++ b/game/factions/israel_1973.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_16A, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.a4ec.a4ec import A_4E_C diff --git a/game/factions/israel_2000.py b/game/factions/israel_2000.py index 6c0db9c0..d87460f9 100644 --- a/game/factions/israel_2000.py +++ b/game/factions/israel_2000.py @@ -1,7 +1,29 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_15E, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Israel_2000 = { "country": "Israel", diff --git a/game/factions/italy_1990.py b/game/factions/italy_1990.py index 2c2175a8..267cd611 100644 --- a/game/factions/italy_1990.py +++ b/game/factions/italy_1990.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + KC_135, + S_3B_Tanker, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Italy_1990 = { "country": "Italy", diff --git a/game/factions/italy_1990_mb339.py b/game/factions/italy_1990_mb339.py index 92307749..9d594817 100644 --- a/game/factions/italy_1990_mb339.py +++ b/game/factions/italy_1990_mb339.py @@ -1,7 +1,28 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + KC_135, + S_3B_Tanker, + Tornado_IDS, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/japan_2005.py b/game/factions/japan_2005.py index c9657348..d33800ff 100644 --- a/game/factions/japan_2005.py +++ b/game/factions/japan_2005.py @@ -1,7 +1,24 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_15C, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import LHA_1_Tarawa, Ticonderoga_class, USS_Arleigh_Burke_IIa +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Japan_2005 = { "country": "Japan", diff --git a/game/factions/libya_2011.py b/game/factions/libya_2011.py index 688b4877..4de5b42c 100644 --- a/game/factions/libya_2011.py +++ b/game/factions/libya_2011.py @@ -1,6 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_23MLD, + Su_17M4, + Su_24M, + Yak_40, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Libya_2011 = { "country": "Libya", diff --git a/game/factions/netherlands_1990.py b/game/factions/netherlands_1990.py index b32fe7d0..48c916bd 100644 --- a/game/factions/netherlands_1990.py +++ b/game/factions/netherlands_1990.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Netherlands_1990 = { "country": "The Netherlands", diff --git a/game/factions/north_korea_2000.py b/game/factions/north_korea_2000.py index dd588f92..15d1f587 100644 --- a/game/factions/north_korea_2000.py +++ b/game/factions/north_korea_2000.py @@ -1,7 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_29A, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) NorthKorea_2000 = { "country": "North Korea", diff --git a/game/factions/pakistan_2015.py b/game/factions/pakistan_2015.py index 67bfa2aa..8d7c6190 100644 --- a/game/factions/pakistan_2015.py +++ b/game/factions/pakistan_2015.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + E_3A, + F_16C_50, + IL_78M, + JF_17, + MiG_19P, + MiG_21Bis, + WingLoong_I, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Pakistan_2015 = { "country": "Pakistan", diff --git a/game/factions/private_miltary_companies.py b/game/factions/private_miltary_companies.py index 4f2860ca..23fbcbdd 100644 --- a/game/factions/private_miltary_companies.py +++ b/game/factions/private_miltary_companies.py @@ -1,7 +1,25 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_8MT, + OH_58D, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_101CC, + L_39C, + L_39ZA, +) +from dcs.ships import ( + Armed_speedboat, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from pydcs_extensions.mb339.mb339 import MB_339PAN diff --git a/game/factions/russia_1955.py b/game/factions/russia_1955.py index dcaeec68..5730bd9d 100644 --- a/game/factions/russia_1955.py +++ b/game/factions/russia_1955.py @@ -1,6 +1,11 @@ -from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40 -from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160 -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.planes import An_26B, An_30M, IL_76MD, IL_78M, MiG_15bis, Yak_40 +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1955 = { "country": "Russia", diff --git a/game/factions/russia_1965.py b/game/factions/russia_1965.py index bc5762c6..9d88d251 100644 --- a/game/factions/russia_1965.py +++ b/game/factions/russia_1965.py @@ -1,7 +1,22 @@ from dcs.helicopters import Mi_8MT -from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50 -from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160 -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_15bis, + MiG_19P, + MiG_21Bis, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1965 = { "country": "Russia", diff --git a/game/factions/russia_1975.py b/game/factions/russia_1975.py index db6a50ae..b8a75437 100644 --- a/game/factions/russia_1975.py +++ b/game/factions/russia_1975.py @@ -1,8 +1,31 @@ -from dcs.helicopters import Mi_8MT, Mi_24V -from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \ - Yak_40, A_50 -from dcs.ships import * -from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29A, + Su_17M4, + Su_24M, + Su_25, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CGN_1144_2_Pyotr_Velikiy, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + Tanker_Elnya_160, +) +from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed Russia_1975 = { "country": "Russia", diff --git a/game/factions/russia_1990.py b/game/factions/russia_1990.py index ee2758a5..747024a8 100644 --- a/game/factions/russia_1990.py +++ b/game/factions/russia_1990.py @@ -1,7 +1,39 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + MiG_23MLD, + MiG_25PD, + MiG_29A, + MiG_29S, + MiG_31, + Su_24M, + Su_25, + Su_27, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Russia_1990 = { "country": "Russia", diff --git a/game/factions/russia_2010.py b/game/factions/russia_2010.py index cc45f062..13adefb6 100644 --- a/game/factions/russia_2010.py +++ b/game/factions/russia_2010.py @@ -1,7 +1,42 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_29S, + MiG_31, + Su_24M, + Su_25, + Su_25T, + Su_27, + Su_30, + Su_33, + Su_34, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + FF_1135M_Rezky, + FSG_1241_1MP_Molniya, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Russia_2010 = { "country": "Russia", diff --git a/game/factions/spain_1990.py b/game/factions/spain_1990.py index f016cb8a..246484a4 100644 --- a/game/factions/spain_1990.py +++ b/game/factions/spain_1990.py @@ -1,6 +1,26 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + AV8BNA, + C_101CC, + C_130, + E_3A, + FA_18C_hornet, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Spain_1990 = { "country": "Spain", diff --git a/game/factions/sweden_1990.py b/game/factions/sweden_1990.py index ebc754f9..058f1478 100644 --- a/game/factions/sweden_1990.py +++ b/game/factions/sweden_1990.py @@ -1,7 +1,21 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + AJS37, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Sweden_1990 = { "country": "Sweden", diff --git a/game/factions/syria.py b/game/factions/syria.py index de082320..f7017e86 100644 --- a/game/factions/syria.py +++ b/game/factions/syria.py @@ -1,6 +1,35 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, + SA342L, + SA342M, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29S, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Yak_40, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) Syria_2011 = { "country": "Syria", diff --git a/game/factions/turkey_2005.py b/game/factions/turkey_2005.py index 02b0500d..be68334c 100644 --- a/game/factions/turkey_2005.py +++ b/game/factions/turkey_2005.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_1W, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + F_4E, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Turkey_2005 = { "country": "Turkey", diff --git a/game/factions/uae_2005.py b/game/factions/uae_2005.py index cc412f06..d6240332 100644 --- a/game/factions/uae_2005.py +++ b/game/factions/uae_2005.py @@ -1,7 +1,27 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, +) +from dcs.planes import ( + C_130, + E_3A, + F_16C_50, + KC130, + KC_135, + M_2000C, + Mirage_2000_5, + WingLoong_I, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) UAE_2005 = { "country": "United Arab Emirates", diff --git a/game/factions/uk_1944.py b/game/factions/uk_1944.py index 7798d714..2620fc86 100644 --- a/game/factions/uk_1944.py +++ b/game/factions/uk_1944.py @@ -1,6 +1,19 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + A_20G, + B_17G, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) from game.data.building_data import WW2_ALLIES_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/uk_1990.py b/game/factions/uk_1990.py index f2bcc57a..4855059d 100644 --- a/game/factions/uk_1990.py +++ b/game/factions/uk_1990.py @@ -1,7 +1,29 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + SA342M, +) +from dcs.planes import ( + AV8BNA, + C_130, + E_3A, + F_4E, + KC130, + KC_135, + Tornado_GR4, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) UnitedKingdom_1990 = { "country": "UK", diff --git a/game/factions/ukraine_2010.py b/game/factions/ukraine_2010.py index cd5149b6..de030137 100644 --- a/game/factions/ukraine_2010.py +++ b/game/factions/ukraine_2010.py @@ -1,7 +1,33 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + Mi_24V, + Mi_8MT, +) +from dcs.planes import ( + A_50, + An_26B, + An_30M, + IL_76MD, + IL_78M, + L_39ZA, + MiG_29S, + Su_24M, + Su_25, + Su_25T, + Su_27, + Yak_40, +) +from dcs.ships import ( + Bulk_cargo_ship_Yakushev, + CV_1143_5_Admiral_Kuznetsov, + Dry_cargo_ship_Ivanov, + Tanker_Elnya_160, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) Ukraine_2010 = { "country": "Ukraine", diff --git a/game/factions/us_aggressors.py b/game/factions/us_aggressors.py index ab4e6ffd..650c09cd 100644 --- a/game/factions/us_aggressors.py +++ b/game/factions/us_aggressors.py @@ -1,7 +1,36 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + Ka_50, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + FA_18C_hornet, + F_15C, + F_16C_50, + F_5E_3, + KC130, + KC_135, + Su_27, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) US_Aggressors = { "country": "USAF Aggressors", diff --git a/game/factions/usa_1944.py b/game/factions/usa_1944.py index 29e07372..7b99bd42 100644 --- a/game/factions/usa_1944.py +++ b/game/factions/usa_1944.py @@ -1,6 +1,20 @@ -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.planes import ( + A_20G, + B_17G, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, +) +from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) from game.data.building_data import WW2_ALLIES_BUILDINGS from game.data.doctrine import WWII_DOCTRINE diff --git a/game/factions/usa_1955.py b/game/factions/usa_1955.py index efa0af47..1943544b 100644 --- a/game/factions/usa_1955.py +++ b/game/factions/usa_1955.py @@ -1,7 +1,22 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.planes import ( + C_130, + E_3A, + F_86F_Sabre, + KC130, + KC_135, + P_51D, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1955 = { "country": "USA", diff --git a/game/factions/usa_1960.py b/game/factions/usa_1960.py index e2b64bd1..ee162d04 100644 --- a/game/factions/usa_1960.py +++ b/game/factions/usa_1960.py @@ -1,7 +1,25 @@ -from dcs.vehicles import * -from dcs.ships import * -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + C_130, + E_3A, + F_86F_Sabre, + KC130, + KC_135, + P_51D, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1960 = { "country": "USA", diff --git a/game/factions/usa_1965.py b/game/factions/usa_1965.py index 59dc651a..cba61391 100644 --- a/game/factions/usa_1965.py +++ b/game/factions/usa_1965.py @@ -1,7 +1,26 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + UH_1H, +) +from dcs.planes import ( + B_52H, + C_130, + E_3A, + F_4E, + F_5E_3, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1965 = { "country": "USA", diff --git a/game/factions/usa_1990.py b/game/factions/usa_1990.py index df2b37df..4014237a 100644 --- a/game/factions/usa_1990.py +++ b/game/factions/usa_1990.py @@ -1,7 +1,34 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64A, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + A_10A, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_15E, + F_16C_50, + KC130, + KC_135, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Oliver_Hazzard_Perry_class, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Infantry, + Unarmed, +) USA_1990 = { "country": "USA", diff --git a/game/factions/usa_2005.py b/game/factions/usa_2005.py index d6b63a58..5b9e03ca 100644 --- a/game/factions/usa_2005.py +++ b/game/factions/usa_2005.py @@ -1,7 +1,36 @@ -from dcs.helicopters import * -from dcs.planes import * -from dcs.ships import * -from dcs.vehicles import * +from dcs.helicopters import ( + AH_64D, + UH_1H, +) +from dcs.planes import ( + AV8BNA, + A_10C, + A_10C_2, + C_130, + E_3A, + FA_18C_hornet, + F_14B, + F_15C, + F_15E, + F_16C_50, + KC130, + KC_135, + MQ_9_Reaper, +) +from dcs.ships import ( + Armed_speedboat, + CVN_74_John_C__Stennis, + LHA_1_Tarawa, + Ticonderoga_class, + USS_Arleigh_Burke_IIa, +) +from dcs.vehicles import ( + AirDefence, + Armor, + Artillery, + Infantry, + Unarmed, +) USA_2005 = { "country": "USA", diff --git a/game/game.py b/game/game.py index d1177f62..57fabfd6 100644 --- a/game/game.py +++ b/game/game.py @@ -1,6 +1,18 @@ +import logging +import math +import random +import sys from datetime import datetime, timedelta +from typing import Any, Dict, List -from game.db import REWARDS, PLAYER_BUDGET_BASE, sys +from dcs.action import Coalition +from dcs.mapping import Point +from dcs.task import CAP, CAS, PinpointStrike, Task +from dcs.unittype import UnitType +from dcs.vehicles import AirDefence + +from game import db +from game.db import PLAYER_BUDGET_BASE, REWARDS from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats from gen.ato import AirTaskingOrder @@ -8,10 +20,15 @@ from gen.conflictgen import Conflict from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner import GroundPlanner -from .event import * +from theater import ConflictTheater, ControlPoint +from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW +from . import persistency +from .debriefing import Debriefing +from .event.event import Event, UnitsDeliveryEvent +from .event.frontlineattack import FrontlineAttackEvent +from .infos.information import Information from .settings import Settings - COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 COMMISION_LIMITS_FACTORS = { @@ -50,20 +67,11 @@ PLAYER_BUDGET_IMPORTANCE_LOG = 2 class Game: - settings = None # type: Settings - budget = PLAYER_BUDGET_INITIAL - events = None # type: typing.List[Event] - pending_transfers = None # type: typing.Dict[] - ignored_cps = None # type: typing.Collection[ControlPoint] - turn = 0 - game_stats: GameStats = None - - current_unit_id = 0 - current_group_id = 0 - - def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, settings): + def __init__(self, player_name: str, enemy_name: str, + theater: ConflictTheater, start_date: datetime, + settings: Settings): self.settings = settings - self.events = [] + self.events: List[Event] = [] self.theater = theater self.player_name = player_name self.player_country = db.FACTIONS[player_name]["country"] @@ -73,14 +81,15 @@ class Game: self.date = datetime(start_date.year, start_date.month, start_date.day) self.game_stats = GameStats() self.game_stats.update(self) - self.ground_planners = {} + self.ground_planners: Dict[int, GroundPlanner] = {} self.informations = [] self.informations.append(Information("Game Start", "-" * 40, 0)) self.__culling_points = self.compute_conflicts_position() - self.__frontlineData = [] - self.__destroyed_units = [] - self.jtacs = [] + self.__destroyed_units: List[str] = [] self.savepath = "" + self.budget = PLAYER_BUDGET_INITIAL + self.current_unit_id = 0 + self.current_group_id = 0 self.blue_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder() @@ -128,7 +137,7 @@ class Game: for player_cp, enemy_cp in self.theater.conflicts(True): self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp) - def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]: + def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]: importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW) if for_task == AirDefence and not self.settings.sams: @@ -209,7 +218,7 @@ class Game: def on_load(self) -> None: ObjectiveDistanceCache.set_theater(self.theater) - def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None): + def pass_turn(self, no_action=False): logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.turn = self.turn + 1 @@ -233,11 +242,7 @@ class Game: if not cp.is_carrier and not cp.is_lha: cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY) - self.ignored_cps = [] - if ignored_cps: - self.ignored_cps = ignored_cps - - self.events = [] # type: typing.List[Event] + self.events = [] self._generate_events() # Update statistics @@ -424,10 +429,10 @@ class Game: return 1 def get_player_coalition(self): - return dcs.action.Coalition.Blue + return Coalition.Blue def get_enemy_coalition(self): - return dcs.action.Coalition.Red + return Coalition.Red def get_player_color(self): return "blue" diff --git a/game/inventory.py b/game/inventory.py index 1d05a473..5ef68b04 100644 --- a/game/inventory.py +++ b/game/inventory.py @@ -5,12 +5,13 @@ from typing import Dict, Iterable, Iterator, Set, Tuple from dcs.unittype import UnitType from gen.flights.flight import Flight +from theater import ControlPoint class ControlPointAircraftInventory: """Aircraft inventory for a single control point.""" - def __init__(self, control_point: "ControlPoint") -> None: + def __init__(self, control_point: ControlPoint) -> None: self.control_point = control_point self.inventory: Dict[UnitType, int] = defaultdict(int) @@ -81,8 +82,8 @@ class ControlPointAircraftInventory: class GlobalAircraftInventory: """Game-wide aircraft inventory.""" - def __init__(self, control_points: Iterable["ControlPoint"]) -> None: - self.inventories: Dict["ControlPoint", ControlPointAircraftInventory] = { + def __init__(self, control_points: Iterable[ControlPoint]) -> None: + self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = { cp: ControlPointAircraftInventory(cp) for cp in control_points } @@ -91,7 +92,7 @@ class GlobalAircraftInventory: for inventory in self.inventories.values(): inventory.clear() - def set_from_control_point(self, control_point: "ControlPoint") -> None: + def set_from_control_point(self, control_point: ControlPoint) -> None: """Set the control point's aircraft inventory. If the inventory for the given control point has already been set for @@ -103,7 +104,7 @@ class GlobalAircraftInventory: def for_control_point( self, - control_point: "ControlPoint") -> ControlPointAircraftInventory: + control_point: ControlPoint) -> ControlPointAircraftInventory: """Returns the inventory specific to the given control point.""" return self.inventories[control_point] diff --git a/game/models/game_stats.py b/game/models/game_stats.py index 2690d861..e6d628f4 100644 --- a/game/models/game_stats.py +++ b/game/models/game_stats.py @@ -1,3 +1,5 @@ +from typing import List + class FactionTurnMetadata: """ Store metadata about a faction @@ -31,10 +33,8 @@ class GameStats: Store statistics for the current game """ - data_per_turn: [GameTurnMetadata] = [] - def __init__(self): - self.data_per_turn = [] + self.data_per_turn: List[GameTurnMetadata] = [] def update(self, game): """ diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 48c5965c..19255247 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -1,7 +1,8 @@ -from game.db import assigned_units_split - -from .operation import * +from dcs.terrain.terrain import Terrain +from gen.conflictgen import Conflict +from .operation import Operation +from .. import db MAX_DISTANCE_BETWEEN_GROUPS = 12000 diff --git a/game/operation/operation.py b/game/operation/operation.py index 56c262e0..ecc82e51 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,13 +1,35 @@ -from typing import Set +import logging +import os +from pathlib import Path +from typing import List, Optional, Set -from gen import * -from gen.airfields import AIRFIELD_DATA -from gen.beacons import load_beacons_for_terrain -from gen.radios import RadioRegistry -from gen.tacan import TacanRegistry +from dcs import Mission +from dcs.action import DoScript, DoScriptFile +from dcs.coalition import Coalition from dcs.countries import country_dict from dcs.lua.parse import loads +from dcs.mapping import Point from dcs.terrain.terrain import Terrain +from dcs.translation import String +from dcs.triggers import TriggerStart +from dcs.unittype import UnitType + +from gen import Conflict, VisualGenerator +from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData +from gen.airfields import AIRFIELD_DATA +from gen.airsupportgen import AirSupport, AirSupportConflictGenerator +from gen.armor import GroundConflictGenerator, JtacInfo +from gen.beacons import load_beacons_for_terrain +from gen.briefinggen import BriefingGenerator +from gen.environmentgen import EnviromentGenerator +from gen.forcedoptionsgen import ForcedOptionsGenerator +from gen.groundobjectsgen import GroundObjectsGenerator +from gen.kneeboard import KneeboardGenerator +from gen.radios import RadioFrequency, RadioRegistry +from gen.tacan import TacanRegistry +from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator +from theater import ControlPoint +from .. import db from ..debriefing import Debriefing @@ -15,16 +37,15 @@ class Operation: attackers_starting_position = None # type: db.StartingPosition defenders_starting_position = None # type: db.StartingPosition - current_mission = None # type: dcs.Mission - regular_mission = None # type: dcs.Mission - quick_mission = None # type: dcs.Mission + current_mission = None # type: Mission + regular_mission = None # type: Mission + quick_mission = None # type: Mission conflict = None # type: Conflict - armorgen = None # type: ArmorConflictGenerator airgen = None # type: AircraftConflictGenerator triggersgen = None # type: TriggersGenerator airsupportgen = None # type: AirSupportConflictGenerator visualgen = None # type: VisualGenerator - envgen = None # type: EnvironmentGenerator + envgen = None # type: EnviromentGenerator groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator @@ -43,7 +64,7 @@ class Operation: defender_name: str, from_cp: ControlPoint, departure_cp: ControlPoint, - to_cp: ControlPoint = None): + to_cp: ControlPoint): self.game = game self.attacker_name = attacker_name self.attacker_country = db.FACTIONS[attacker_name]["country"] @@ -55,7 +76,7 @@ class Operation: self.to_cp = to_cp self.is_quick = False - def units_of(self, country_name: str) -> typing.Collection[UnitType]: + def units_of(self, country_name: str) -> List[UnitType]: return [] def is_successfull(self, debriefing: Debriefing) -> bool: @@ -75,7 +96,7 @@ class Operation: with open("resources/default_options.lua", "r") as f: options_dict = loads(f.read())["options"] - self.current_mission = dcs.Mission(terrain) + self.current_mission = Mission(terrain) print(self.game.player_country) print(country_dict[db.country_id_from_name(self.game.player_country)]) @@ -106,7 +127,11 @@ class Operation: self.defenders_starting_position = None else: self.attackers_starting_position = self.departure_cp.at - self.defenders_starting_position = self.to_cp.at + # TODO: Is this possible? + if self.to_cp is not None: + self.defenders_starting_position = self.to_cp.at + else: + self.defenders_starting_position = None def generate(self): radio_registry = RadioRegistry() @@ -374,6 +399,7 @@ class Operation: logging.warning(f"No aircraft data for {airframe.id}") return - aircraft_data.channel_allocator.assign_channels_for_flight( - flight, air_support - ) + if aircraft_data.channel_allocator is not None: + aircraft_data.channel_allocator.assign_channels_for_flight( + flight, air_support + ) diff --git a/game/persistency.py b/game/persistency.py index 9e55dda0..4e0b216f 100644 --- a/game/persistency.py +++ b/game/persistency.py @@ -2,8 +2,9 @@ import logging import os import pickle import shutil +from typing import Optional -_dcs_saved_game_folder = None # type: str +_dcs_saved_game_folder: Optional[str] = None _file_abs_path = None def setup(user_folder: str): diff --git a/gen/__init__.py b/gen/__init__.py index ad11614f..6fd6547c 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -1,4 +1,3 @@ -from .aaa import * from .aircraft import * from .armor import * from .airsupportgen import * @@ -12,4 +11,3 @@ from .forcedoptionsgen import * from .kneeboard import * from . import naming - diff --git a/gen/aaa.py b/gen/aaa.py deleted file mode 100644 index d9822202..00000000 --- a/gen/aaa.py +++ /dev/null @@ -1,51 +0,0 @@ -from .conflictgen import * -from .naming import * - -from dcs.mission import * -from dcs.mission import * - -from .conflictgen import * -from .naming import * - -DISTANCE_FACTOR = 0.5, 1 -EXTRA_AA_MIN_DISTANCE = 50000 -EXTRA_AA_MAX_DISTANCE = 150000 -EXTRA_AA_POSITION_FROM_CP = 550 - -class ExtraAAConflictGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game, player_country: Country, enemy_country: Country): - self.mission = mission - self.game = game - self.conflict = conflict - self.player_country = player_country - self.enemy_country = enemy_country - - def generate(self): - - for cp in self.game.theater.controlpoints: - if cp.is_global: - continue - - if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.to_cp.position) < EXTRA_AA_MIN_DISTANCE: - continue - - if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MAX_DISTANCE: - continue - - country_name = cp.captured and self.player_country or self.enemy_country - position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP) - - self.mission.vehicle_group( - country=self.mission.country(country_name), - name=namegen.next_basedefense_name(), - _type=db.EXTRA_AA[country_name], - position=position, - group_size=1 - ) - diff --git a/gen/aircraft.py b/gen/aircraft.py index 9eeb1836..f9cd63f1 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1,14 +1,67 @@ +import logging +import random from dataclasses import dataclass -from typing import Type +from typing import Dict, List, Optional, Tuple, Type, Union from dcs import helicopters -from dcs.action import ActivateGroup, AITaskPush, MessageToAll -from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone +from dcs.action import AITaskPush, ActivateGroup, MessageToAll +from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter +from dcs.country import Country from dcs.flyingunit import FlyingUnit -from dcs.helicopters import helicopter_map, UH_1H +from dcs.helicopters import UH_1H, helicopter_map +from dcs.mapping import Point +from dcs.mission import Mission, StartType +from dcs.planes import ( + AJS37, + B_17G, + Bf_109K_4, + FW_190A8, + FW_190D9, + F_14B, + I_16, + JF_17, + Ju_88A4, + P_47D_30, + P_51D, + P_51D_30_NA, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_33, +) +from dcs.point import PointAction +from dcs.task import ( + AntishipStrike, + AttackGroup, + Bombing, + CAP, + CAS, + ControlledTask, + EPLRS, + EngageTargets, + Escort, + GroundAttack, + MainTask, + NoTask, + OptROE, + OptRTBOnBingoFuel, + OptRTBOnOutOfAmmo, + OptReactOnThreat, + OptRestrictAfterburner, + OptRestrictJettison, + OrbitAction, + PinpointStrike, + SEAD, + StartCommand, + Targets, + Task, +) from dcs.terrain.terrain import Airport, NoParkingSlotError -from dcs.triggers import TriggerOnce, Event +from dcs.translation import String +from dcs.triggers import Event, TriggerOnce +from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup +from dcs.unittype import FlyingType, UnitType +from game import db from game.data.cap_capabilities_db import GUNFIGHTERS from game.settings import Settings from game.utils import nm_to_meter @@ -22,9 +75,10 @@ from gen.flights.flight import ( FlightWaypoint, FlightWaypointType, ) -from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry -from .conflictgen import * -from .naming import * +from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio +from theater.controlpoint import ControlPoint, ControlPointType +from .naming import namegen +from .conflictgen import Conflict WARM_START_HELI_AIRSPEED = 120 WARM_START_HELI_ALT = 500 @@ -264,8 +318,12 @@ class CommonRadioChannelAllocator(RadioChannelAllocator): def assign_channels_for_flight(self, flight: FlightData, air_support: AirSupport) -> None: - flight.assign_channel( - self.intra_flight_radio_index, 1, flight.intra_flight_channel) + if self.intra_flight_radio_index is not None: + flight.assign_channel( + self.intra_flight_radio_index, 1, flight.intra_flight_channel) + + if self.inter_flight_radio_index is None: + return # For cases where the inter-flight and intra-flight radios share presets # (the JF-17 only has one set of channels, even though it can use two @@ -335,8 +393,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator): # the guard channel. radio_id = 1 flight.assign_channel(radio_id, 1, flight.intra_flight_channel) - flight.assign_channel(radio_id, 4, flight.departure.atc) - flight.assign_channel(radio_id, 5, flight.arrival.atc) + if flight.departure.atc is not None: + flight.assign_channel(radio_id, 4, flight.departure.atc) + if flight.arrival.atc is not None: + flight.assign_channel(radio_id, 5, flight.arrival.atc) # TODO: Assign divert to 6 when we support divert airfields. @@ -348,8 +408,10 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator): air_support: AirSupport) -> None: radio_id = 1 flight.assign_channel(radio_id, 1, flight.intra_flight_channel) - flight.assign_channel(radio_id, 2, flight.departure.atc) - flight.assign_channel(radio_id, 3, flight.arrival.atc) + if flight.departure.atc is not None: + flight.assign_channel(radio_id, 2, flight.departure.atc) + if flight.arrival.atc is not None: + flight.assign_channel(radio_id, 3, flight.arrival.atc) # TODO : Some GCI on Channel 4 ? @@ -471,8 +533,6 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] class AircraftConflictGenerator: - escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]] - def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game, radio_registry: RadioRegistry): self.m = mission @@ -480,7 +540,7 @@ class AircraftConflictGenerator: self.settings = settings self.conflict = conflict self.radio_registry = radio_registry - self.escort_targets = [] + self.escort_targets: List[Tuple[FlyingGroup, int]] = [] self.flights: List[FlightData] = [] def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency: @@ -502,33 +562,23 @@ class AircraftConflictGenerator: def _start_type(self) -> StartType: return self.settings.cold_start and StartType.Cold or StartType.Warm - def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], + def _setup_group(self, group: FlyingGroup, for_task: Type[Task], flight: Flight, dynamic_runways: Dict[str, RunwayData]): did_load_loadout = False unit_type = group.units[0].unit_type if unit_type in db.PLANE_PAYLOAD_OVERRIDES: override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type] - if type(override_loadout) == dict: + # Clear pylons + for p in group.units: + p.pylons.clear() - # Clear pylons - for p in group.units: - p.pylons.clear() - - # Now load loadout - if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task] - group.load_loadout(payload_name) - did_load_loadout = True - logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) - elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: - payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"] - group.load_loadout(payload_name) - did_load_loadout = True - logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) - elif issubclass(override_loadout, MainTask): - group.load_task_default_loadout(override_loadout) + # Now load loadout + if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]: + payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task] + group.load_loadout(payload_name) did_load_loadout = True + logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task)) if not did_load_loadout: group.load_task_default_loadout(for_task) @@ -590,7 +640,7 @@ class AircraftConflictGenerator: # Special case so Su 33 carrier take off if unit_type is Su_33: - if task is not CAP: + if flight.flight_type is not CAP: for unit in group.units: unit.fuel = Su_33.fuel_max / 2.2 else: @@ -613,9 +663,12 @@ class AircraftConflictGenerator: # so just use the first runway. return runways[0] - def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup: + def _generate_at_airport(self, name: str, side: Country, + unit_type: FlyingType, count: int, + client_count: int, + airport: Optional[Airport] = None, + start_type=None) -> FlyingGroup: assert count > 0 - assert unit is not None if start_type is None: start_type = self._start_type() @@ -633,7 +686,6 @@ class AircraftConflictGenerator: def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup: assert count > 0 - assert unit is not None if unit_type in helicopters.helicopter_map.values(): alt = WARM_START_HELI_ALT @@ -660,9 +712,11 @@ class AircraftConflictGenerator: group.points[0].alt_type = "RADIO" return group - def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup], start_type=None) -> FlyingGroup: + def _generate_at_group(self, name: str, side: Country, + unit_type: FlyingType, count: int, client_count: int, + at: Union[ShipGroup, StaticGroup], + start_type=None) -> FlyingGroup: assert count > 0 - assert unit is not None if start_type is None: start_type = self._start_type() @@ -688,7 +742,7 @@ class AircraftConflictGenerator: return self._generate_at_group(name, side, unit_type, count, client_count, at) else: return self._generate_inflight(name, side, unit_type, count, client_count, at.position) - elif issubclass(at, Airport): + elif isinstance(at, Airport): takeoff_ban = unit_type in db.TAKEOFF_BAN ai_ban = client_count == 0 and self.settings.only_player_takeoff @@ -707,8 +761,9 @@ class AircraftConflictGenerator: point.alt_type = "RADIO" return point - def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None): - if not at: + def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, + at: Optional[db.StartingPosition] = None): + if at is None: at = cp.at position = at if isinstance(at, Point) else at.position diff --git a/gen/airfields.py b/gen/airfields.py index b7e08712..b3185158 100644 --- a/gen/airfields.py +++ b/gen/airfields.py @@ -195,10 +195,12 @@ AIRFIELD_DATA = { runway_length=8623, atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)), outer_ndb={ - "22": ("AP", MHz(443, 0)), "4": "443.00 (AN)" + "22": ("AP", MHz(443, 0)), + "04": ("AN", MHz(443)), }, inner_ndb={ - "22": ("P", MHz(215, 0)), "4": "215.00 (N)" + "22": ("P", MHz(215, 0)), + "04": ("N", MHz(215)), }, ), diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 791c80b6..ce00e9d5 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,8 +1,21 @@ from dataclasses import dataclass, field +from typing import List, Type +from dcs.mission import Mission, StartType +from dcs.planes import IL_78M +from dcs.task import ( + AWACS, + ActivateBeaconCommand, + MainTask, + Refueling, + SetImmortalCommand, + SetInvisibleCommand, +) + +from game import db +from .naming import namegen from .callsigns import callsign_for_support_unit -from .conflictgen import * -from .naming import * +from .conflictgen import Conflict from .radios import RadioFrequency, RadioRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry @@ -49,7 +62,7 @@ class AirSupportConflictGenerator: self.tacan_registry = tacan_registry @classmethod - def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]: + def support_tasks(cls) -> List[Type[MainTask]]: return [Refueling, AWACS] def generate(self, is_awacs_enabled): diff --git a/gen/armor.py b/gen/armor.py index 463d7571..426dc05a 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,13 +1,39 @@ +import logging +import random from dataclasses import dataclass +from typing import List +from dcs import Mission from dcs.action import AITaskPush -from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess -from dcs.triggers import TriggerOnce, Event +from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged +from dcs.country import Country +from dcs.mapping import Point +from dcs.planes import MQ_9_Reaper +from dcs.point import PointAction +from dcs.task import ( + AttackGroup, + ControlledTask, + EPLRS, + FireAtPoint, + GoToWaypoint, + Hold, + OrbitAction, + SetImmortalCommand, + SetInvisibleCommand, +) +from dcs.triggers import Event, TriggerOnce +from dcs.unit import Vehicle +from dcs.unittype import VehicleType -from gen import namegen -from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE +from game import db +from .naming import namegen +from gen.ground_forces.ai_ground_planner import ( + CombatGroupRole, + DISTANCE_FROM_FRONTLINE, +) from .callsigns import callsign_for_support_unit -from .conflictgen import * +from .conflictgen import Conflict +from .ground_forces.combat_stance import CombatStance SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -48,7 +74,7 @@ class GroundConflictGenerator: self.jtacs: List[JtacInfo] = [] def _group_point(self, point) -> Point: - distance = randint( + distance = random.randint( int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]), ) @@ -165,7 +191,7 @@ class GroundConflictGenerator: heading=forward_heading, move_formation=PointAction.OffRoad) - for i in range(randint(3, 10)): + for i in range(random.randint(3, 10)): u = random.choice(possible_infantry_units) position = infantry_position.random_point_within(55, 5) self.mission.vehicle_group( diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 9b83b51e..3c9eecfe 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -1,21 +1,11 @@ import logging -import typing -import pdb -import dcs +import random +from typing import Tuple -from random import randint -from dcs import Mission +from dcs.country import Country +from dcs.mapping import Point -from dcs.mission import * -from dcs.vehicles import * -from dcs.unitgroup import * -from dcs.unittype import * -from dcs.mapping import * -from dcs.point import * -from dcs.task import * -from dcs.country import * - -from theater import * +from theater import ConflictTheater, ControlPoint AIR_DISTANCE = 40000 @@ -65,24 +55,6 @@ def _heading_sum(h, a) -> int: class Conflict: - attackers_side = None # type: str - defenders_side = None # type: str - attackers_country = None # type: Country - defenders_country = None # type: Country - from_cp = None # type: ControlPoint - to_cp = None # type: ControlPoint - position = None # type: Point - size = None # type: int - radials = None # type: typing.List[int] - - heading = None # type: int - distance = None # type: int - - ground_attackers_location = None # type: Point - ground_defenders_location = None # type: Point - air_attackers_location = None # type: Point - air_defenders_location = None # type: Point - def __init__(self, theater: ConflictTheater, from_cp: ControlPoint, @@ -155,7 +127,7 @@ class Conflict: else: return self.position - def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> typing.Optional[Point]: + def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point: return Conflict._find_ground_position(at, max_distance, heading, self.theater) @classmethod @@ -163,7 +135,7 @@ class Conflict: return from_cp.has_frontline and to_cp.has_frontline @classmethod - def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[typing.Tuple[Point, int]]: + def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: attack_heading = from_cp.position.heading_between_point(to_cp.position) attack_distance = from_cp.position.distance_to_point(to_cp.position) middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2) @@ -174,9 +146,7 @@ class Conflict: @classmethod - def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]: - initial, heading = cls.frontline_position(theater, from_cp, to_cp) - + def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: """ probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH) probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ]) @@ -193,9 +163,6 @@ class Conflict: return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length """ frontline = cls.frontline_position(theater, from_cp, to_cp) - if not frontline: - return None - center_position, heading = frontline left_position, right_position = None, None @@ -243,7 +210,7 @@ class Conflict: """ @classmethod - def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]: + def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: pos = initial for _ in range(0, int(max_distance), 500): if theater.is_on_land(pos): @@ -302,10 +269,14 @@ class Conflict: distance = to_cp.size * GROUND_DISTANCE_FACTOR attackers_location = position.point_from_heading(attack_heading, distance) - attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater) + attackers_location = Conflict._find_ground_position( + attackers_location, int(distance * 2), + _heading_sum(attack_heading, 180), theater) defenders_location = position.point_from_heading(defense_heading, distance) - defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater) + defenders_location = Conflict._find_ground_position( + defenders_location, int(distance * 2), + _heading_sum(defense_heading, 180), theater) return cls( position=position, @@ -429,7 +400,7 @@ class Conflict: assert cls.has_frontline_between(from_cp, to_cp) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) - attack_position = position.point_from_heading(heading, randint(0, int(distance))) + attack_position = position.point_from_heading(heading, random.randint(0, int(distance))) attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE) defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE)) @@ -456,7 +427,9 @@ class Conflict: distance = to_cp.size * GROUND_DISTANCE_FACTOR defenders_location = position.point_from_heading(defense_heading, distance) - defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater) + defenders_location = Conflict._find_ground_position( + defenders_location, int(distance * 2), + _heading_sum(defense_heading, 180), theater) return cls( position=position, diff --git a/gen/environmentgen.py b/gen/environmentgen.py index 1d861842..57d70452 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -1,20 +1,11 @@ import logging -import typing import random -from datetime import datetime, timedelta, time +from datetime import timedelta from dcs.mission import Mission -from dcs.triggers import * -from dcs.condition import * -from dcs.action import * -from dcs.unit import Skill -from dcs.point import MovingPoint, PointProperties -from dcs.action import * -from dcs.weather import * +from dcs.weather import Weather, Wind -from game import db -from theater import * -from gen import * +from .conflictgen import Conflict WEATHER_CLOUD_BASE = 2000, 3000 WEATHER_CLOUD_DENSITY = 1, 8 diff --git a/gen/fleet/ship_group_generator.py b/gen/fleet/ship_group_generator.py index 455d9f27..db974d6c 100644 --- a/gen/fleet/ship_group_generator.py +++ b/gen/fleet/ship_group_generator.py @@ -28,13 +28,13 @@ SHIP_MAP = { } -def generate_ship_group(game, ground_object, faction:str): +def generate_ship_group(game, ground_object, faction_name: str): """ This generate a ship group :return: Nothing, but put the group reference inside the ground object """ - faction = db.FACTIONS[faction] - if "boat" in faction.keys(): + faction = db.FACTIONS[faction_name] + if "boat" in faction: generators = faction["boat"] if len(generators) > 0: gen = random.choice(generators) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 6b584e68..36b9d929 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import operator from dataclasses import dataclass -from typing import Dict, Iterator, List, Optional, Set, TYPE_CHECKING, Tuple +from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from dcs.unittype import UnitType @@ -406,17 +406,18 @@ class CoalitionMissionPlanner: self.game.aircraft_inventory, self.is_player ) - for flight in mission.flights: - if not builder.plan_flight(flight): + for proposed_flight in mission.flights: + if not builder.plan_flight(proposed_flight): builder.release_planned_aircraft() self.message("Insufficient aircraft", f"Not enough aircraft in range for {mission}") return package = builder.build() - builder = FlightPlanBuilder(self.game, package, self.is_player) + flight_plan_builder = FlightPlanBuilder(self.game, package, + self.is_player) for flight in package.flights: - builder.populate_flight_plan(flight) + flight_plan_builder.populate_flight_plan(flight) self.ato.add_package(package) def message(self, title, text) -> None: diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index e1393a1a..dd51fd26 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -1,5 +1,78 @@ -from dcs.planes import * -from dcs.helicopters import * +from dcs.helicopters import ( + AH_1W, + AH_64A, + AH_64D, + Ka_50, + Mi_24V, + Mi_28N, + Mi_8MT, + OH_58D, + SA342L, + SA342M, + UH_1H, +) +from dcs.planes import ( + AJS37, + AV8BNA, + A_10A, + A_10C, + A_10C_2, + A_20G, + B_17G, + Bf_109K_4, + C_101CC, + FA_18C_hornet, + FW_190A8, + FW_190D9, + F_14B, + F_15C, + F_15E, + F_16A, + F_16C_50, + F_4E, + F_5E_3, + F_86F_Sabre, + F_A_18C, + JF_17, + J_11A, + Ju_88A4, + L_39ZA, + MQ_9_Reaper, + M_2000C, + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_27K, + MiG_29A, + MiG_29G, + MiG_29K, + MiG_29S, + MiG_31, + Mirage_2000_5, + P_47D_30, + P_47D_30bl1, + P_47D_40, + P_51D, + P_51D_30_NA, + RQ_1A_Predator, + SpitfireLFMkIX, + SpitfireLFMkIXCW, + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_27, + Su_30, + Su_33, + Su_34, + Tornado_GR4, + Tornado_IDS, + WingLoong_I, +) # Interceptor are the aircraft prioritized for interception tasks # If none is available, the AI will use regular CAP-capable aircraft instead diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 676b6bd8..90b27ccf 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -1,10 +1,10 @@ from enum import Enum -from typing import List +from typing import Dict, Optional from game import db from dcs.unittype import UnitType from dcs.point import MovingPoint, PointAction -from theater.controlpoint import ControlPoint +from theater.controlpoint import ControlPoint, MissionTarget class FlightType(Enum): @@ -73,8 +73,8 @@ class FlightWaypoint: self.alt_type = "BARO" self.name = "" self.description = "" - self.targets = [] - self.targetGroup = None + self.targets: List[MissionTarget] = [] + self.targetGroup: Optional[MissionTarget] = None self.obj_name = "" self.pretty_name = "" self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED @@ -110,15 +110,9 @@ class FlightWaypoint: class Flight: - unit_type: UnitType = None - from_cp = None - points: List[FlightWaypoint] = [] - flight_type: FlightType = None count: int = 0 client_count: int = 0 - targets = [] use_custom_loadout = False - loadout = {} preset_loadout_name = "" start_type = "Runway" group = False # Contains DCS Mission group data after mission has been generated @@ -126,14 +120,14 @@ class Flight: # How long before this flight should take off scheduled_in = 0 - def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType): + def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType): self.unit_type = unit_type self.count = count self.from_cp = from_cp self.flight_type = flight_type - self.points = [] - self.targets = [] - self.loadout = {} + self.points: List[FlightWaypoint] = [] + self.targets: List[MissionTarget] = [] + self.loadout: Dict[str, str] = {} self.start_type = "Runway" def __repr__(self): @@ -143,10 +137,10 @@ class Flight: # Test if __name__ == '__main__': - from pydcs.dcs.planes import A_10C + from dcs.planes import A_10C from theater import ControlPoint, Point, List - from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0) + from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0) f = Flight(A_10C(), 4, from_cp, FlightType.CAS) f.scheduled_in = 50 print(f) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 2e595b31..5e5fefe2 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -388,12 +388,14 @@ class FlightPlanBuilder: def _join_point(self) -> Point: ingress_point = self.package.ingress_point + assert ingress_point is not None 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 + assert egress_point is not None heading = self._heading_to_package_airfield(egress_point) return egress_point.point_from_heading(heading, -self.doctrine.split_distance) diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 8481b6ba..6fc3fd85 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -153,14 +153,16 @@ class WaypointBuilder: self._target_point(target, name, f"STRIKE [{location.name}]: {name}", location) # TODO: Seems fishy. - self.ingress_point.targetGroup = location + if self.ingress_point is not None: + self.ingress_point.targetGroup = location def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str, location: MissionTarget) -> None: self._target_point(target, name, f"STRIKE [{location.name}]: {name}", location) # TODO: Seems fishy. - self.ingress_point.targetGroup = location + if self.ingress_point is not None: + self.ingress_point.targetGroup = location def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str, location: MissionTarget) -> None: @@ -191,12 +193,14 @@ class WaypointBuilder: def sead_area(self, target: MissionTarget) -> None: self._target_area(f"SEAD on {target.name}", target) # TODO: Seems fishy. - self.ingress_point.targetGroup = target + if self.ingress_point is not None: + self.ingress_point.targetGroup = target def dead_area(self, target: MissionTarget) -> None: self._target_area(f"DEAD on {target.name}", target) # TODO: Seems fishy. - self.ingress_point.targetGroup = target + if self.ingress_point is not None: + self.ingress_point.targetGroup = target def _target_area(self, name: str, location: MissionTarget) -> None: if self.ingress_point is None: diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index 877c9831..bda87407 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -1,13 +1,13 @@ import random from enum import Enum +from typing import Dict, List -from dcs.vehicles import * - -from gen import Conflict -from gen.ground_forces.combat_stance import CombatStance -from theater import ControlPoint +from dcs.vehicles import Armor, Artillery, Infantry, Unarmed +from dcs.unittype import VehicleType import pydcs_extensions.frenchpack.frenchpack as frenchpack +from gen.ground_forces.combat_stance import CombatStance +from theater import ControlPoint TYPE_TANKS = [ Armor.MBT_T_55, @@ -207,8 +207,8 @@ GROUP_SIZES_BY_COMBAT_STANCE = { class CombatGroup: - def __init__(self, role:CombatGroupRole): - self.units = [] + def __init__(self, role: CombatGroupRole): + self.units: List[VehicleType] = [] self.role = role self.assigned_enemy_cp = None self.start_position = None @@ -222,33 +222,22 @@ class CombatGroup: class GroundPlanner: - cp = None - combat_groups_dict = {} - connected_enemy_cp = [] - - tank_groups = [] - apc_group = [] - ifv_group = [] - art_group = [] - shorad_groups = [] - logi_groups = [] - def __init__(self, cp:ControlPoint, game): self.cp = cp self.game = game self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured] - self.tank_groups = [] - self.apc_group = [] - self.ifv_group = [] - self.art_group = [] - self.atgm_group = [] - self.logi_groups = [] - self.shorad_groups = [] + self.tank_groups: List[CombatGroup] = [] + self.apc_group: List[CombatGroup] = [] + self.ifv_group: List[CombatGroup] = [] + self.art_group: List[CombatGroup] = [] + self.atgm_group: List[CombatGroup] = [] + self.logi_groups: List[CombatGroup] = [] + self.shorad_groups: List[CombatGroup] = [] - self.units_per_cp = {} + self.units_per_cp: Dict[int, List[CombatGroup]] = {} for cp in self.connected_enemy_cp: self.units_per_cp[cp.id] = [] - self.reserve = [] + self.reserve: List[CombatGroup] = [] def plan_groundwar(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index ddf2706e..2d1894e8 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -1,11 +1,23 @@ -from dcs.statics import * -from dcs.unit import Ship, Vehicle +import logging +import random +from typing import Dict, Iterator -from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS +from dcs import Mission +from dcs.statics import fortification_map, warehouse_map +from dcs.task import ( + ActivateBeaconCommand, + ActivateICLSCommand, + EPLRS, + OptAlarmState, +) +from dcs.unit import Ship, Vehicle +from dcs.unitgroup import StaticGroup + +from game import db +from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name from .airfields import RunwayData -from .conflictgen import * -from .naming import * +from .conflictgen import Conflict from .radios import RadioRegistry from .tacan import TacanBand, TacanRegistry @@ -26,7 +38,7 @@ class GroundObjectsGenerator: self.icls_alloc = iter(range(1, 21)) self.runways: Dict[str, RunwayData] = {} - def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]: + def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]: if self.conflict.is_vector: center = self.conflict.center heading = self.conflict.heading - 90 diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 6843e395..e7a86bc5 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -82,6 +82,8 @@ class KneeboardPageWriter: def table(self, cells: List[List[str]], headers: Optional[List[str]] = None) -> None: + if headers is None: + headers = [] table = tabulate(cells, headers=headers, numalign="right") self.text(table, font=self.table_font) @@ -136,7 +138,7 @@ class FlightPlanBuilder: def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None: self.rows.append([ - waypoint.number, + str(waypoint.number), waypoint.waypoint.pretty_name, str(int(units.meters_to_feet(waypoint.waypoint.alt))) ]) @@ -194,7 +196,7 @@ class BriefingPage(KneeboardPage): tankers.append([ tanker.callsign, tanker.variant, - tanker.tacan, + str(tanker.tacan), self.format_frequency(tanker.freq), ]) writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"]) @@ -225,12 +227,22 @@ class BriefingPage(KneeboardPage): atc = "" if runway.atc is not None: atc = self.format_frequency(runway.atc) + if runway.tacan is None: + tacan = "" + else: + tacan = str(runway.tacan) + if runway.ils is not None: + ils = str(runway.ils) + elif runway.icls is not None: + ils = str(runway.icls) + else: + ils = "" return [ row_title, runway.airfield_name, atc, - runway.tacan or "", - runway.ils or runway.icls or "", + tacan, + ils, runway.runway_name, ] diff --git a/gen/missiles/missiles_group_generator.py b/gen/missiles/missiles_group_generator.py index c63fcca9..4e2e9d73 100644 --- a/gen/missiles/missiles_group_generator.py +++ b/gen/missiles/missiles_group_generator.py @@ -8,13 +8,13 @@ MISSILES_MAP = { } -def generate_missile_group(game, ground_object, faction:str): +def generate_missile_group(game, ground_object, faction_name: str): """ This generate a ship group :return: Nothing, but put the group reference inside the ground object """ - faction = db.FACTIONS[faction] - if "missiles" in faction.keys(): + faction = db.FACTIONS[faction_name] + if "missiles" in faction: generators = faction["missiles"] if len(generators) > 0: gen = random.choice(generators) diff --git a/gen/triggergen.py b/gen/triggergen.py index fbd8062e..ba87bb3e 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -1,19 +1,12 @@ -import typing -import random -from datetime import datetime, timedelta, time - +from dcs.action import MarkToAll +from dcs.condition import TimeAfter from dcs.mission import Mission -from dcs.triggers import * -from dcs.condition import * -from dcs.action import * +from dcs.task import Option +from dcs.translation import String +from dcs.triggers import Event, TriggerOnce from dcs.unit import Skill -from dcs.point import MovingPoint, PointProperties -from dcs.action import * -from game import db -from theater import * -from gen.airsupportgen import AirSupportConflictGenerator -from gen import * +from .conflictgen import Conflict PUSH_TRIGGER_SIZE = 3000 PUSH_TRIGGER_ACTIVATION_AGL = 25 diff --git a/gen/visualgen.py b/gen/visualgen.py index c3be7cad..5bc315e5 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -1,18 +1,20 @@ -import typing +from __future__ import annotations + import random -from datetime import datetime, timedelta +from typing import TYPE_CHECKING +from dcs.mapping import Point from dcs.mission import Mission -from dcs.statics import * from dcs.unit import Static +from dcs.unittype import StaticType -from theater import * -from .conflictgen import * -#from game.game import Game -from game import db +if TYPE_CHECKING: + from game import Game + +from .conflictgen import Conflict, FRONTLINE_LENGTH -class MarkerSmoke(unittype.StaticType): +class MarkerSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -20,7 +22,7 @@ class MarkerSmoke(unittype.StaticType): rate = 0.1 -class Smoke(unittype.StaticType): +class Smoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -28,7 +30,7 @@ class Smoke(unittype.StaticType): rate = 1 -class BigSmoke(unittype.StaticType): +class BigSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -36,7 +38,7 @@ class BigSmoke(unittype.StaticType): rate = 1 -class MassiveSmoke(unittype.StaticType): +class MassiveSmoke(StaticType): id = "big_smoke" category = "Effects" name = "big_smoke" @@ -44,7 +46,7 @@ class MassiveSmoke(unittype.StaticType): rate = 1 -class Outpost(unittype.StaticType): +class Outpost(StaticType): id = "outpost" name = "outpost" category = "Fortifications" @@ -90,9 +92,7 @@ def turn_heading(heading, fac): class VisualGenerator: - game = None # type: Game - - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game: Game): self.mission = mission self.conflict = conflict self.game = game diff --git a/mypy.ini b/mypy.ini index 1f9f6f7d..045a50e6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,10 @@ [mypy] namespace_packages = True +[mypy-dcs.*] +follow_imports=silent +ignore_missing_imports = True + [mypy-PIL.*] ignore_missing_imports = True diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py index b93d3256..c35a482e 100644 --- a/qt_ui/windows/QWaitingForMissionResultWindow.py +++ b/qt_ui/windows/QWaitingForMissionResultWindow.py @@ -172,7 +172,7 @@ class QWaitingForMissionResultWindow(QDialog): def process_debriefing(self): self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing) - self.game.pass_turn(ignored_cps=[self.gameEvent.to_cp, ]) + self.game.pass_turn() GameUpdateSignal.get_instance().sendDebriefing(self.game, self.gameEvent, self.debriefing) self.close() diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 597c2a32..cf5e1a34 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -2,13 +2,12 @@ from PySide2.QtCore import Qt from PySide2.QtGui import QCloseEvent, QPixmap from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget -from game.event import ControlPointType from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.basemenu.QBaseMenuTabs import QBaseMenuTabs from qt_ui.windows.basemenu.QRecruitBehaviour import QRecruitBehaviour -from theater import ControlPoint +from theater import ControlPoint, ControlPointType class QBaseMenu2(QDialog): diff --git a/theater/base.py b/theater/base.py index b294dd38..4ca5dec7 100644 --- a/theater/base.py +++ b/theater/base.py @@ -1,14 +1,15 @@ -import logging -import typing -import math import itertools +import logging +import math +import typing +from typing import Dict, Type -from dcs.planes import * -from dcs.vehicles import * -from dcs.task import * +from dcs.planes import PlaneType +from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task +from dcs.unittype import UnitType, VehicleType +from dcs.vehicles import AirDefence, Armor from game import db -from gen import aaa STRENGTH_AA_ASSEMBLE_MIN = 0.2 PLANES_SCRAMBLE_MIN_BASE = 2 @@ -21,16 +22,15 @@ BASE_MIN_STRENGTH = 0 class Base: aircraft = {} # type: typing.Dict[PlaneType, int] - armor = {} # type: typing.Dict[Armor, int] + armor = {} # type: typing.Dict[VehicleType, int] aa = {} # type: typing.Dict[AirDefence, int] strength = 1 # type: float - commision_points = {} def __init__(self): self.aircraft = {} self.armor = {} self.aa = {} - self.commision_points = {} + self.commision_points: Dict[Type, float] = {} self.strength = 1 @property @@ -55,17 +55,19 @@ class Base: def all_units(self): return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) - def _find_best_unit(self, dict, for_type: Task, count: int) -> typing.Dict: + def _find_best_unit(self, available_units: Dict[UnitType, int], + for_type: Task, count: int) -> Dict[UnitType, int]: if count <= 0: logging.warning("{}: no units for {}".format(self, for_type)) return {} - sorted_units = [key for key in dict.keys() if key in db.UNIT_BY_TASK[for_type]] + sorted_units = [key for key in available_units if + key in db.UNIT_BY_TASK[for_type]] sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) - result = {} + result: Dict[UnitType, int] = {} for unit_type in sorted_units: - existing_count = dict[unit_type] # type: int + existing_count = available_units[unit_type] # type: int if not existing_count: continue diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index e21a4e6d..d92def90 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,12 +1,19 @@ import json -import typing +from typing import Dict, Iterator, List, Optional, Tuple -import dcs from dcs.mapping import Point -from dcs.terrain import caucasus, persiangulf, nevada, normandy, thechannel, syria +from dcs.terrain import ( + caucasus, + nevada, + normandy, + persiangulf, + syria, + thechannel, +) +from dcs.terrain.terrain import Terrain from .controlpoint import ControlPoint -from .landmap import poly_contains, load_landmap +from .landmap import Landmap, load_landmap, poly_contains SIZE_TINY = 150 SIZE_SMALL = 600 @@ -47,26 +54,28 @@ COAST_DR_W = [135, 180, 225, 315] class ConflictTheater: - terrain = None # type: dcs.terrain.Terrain - controlpoints = None # type: typing.List[ControlPoint] + terrain: Terrain - reference_points = None # type: typing.Dict - overview_image = None # type: str - landmap = None # type: landmap.Landmap + reference_points: Dict[Tuple[float, float], Tuple[float, float]] + overview_image: str + landmap: Optional[Landmap] """ land_poly = None # type: Polygon """ - daytime_map = None # type: typing.Dict[str, typing.Tuple[int, int]] + daytime_map: Dict[str, Tuple[int, int]] def __init__(self): - self.controlpoints = [] + self.controlpoints: List[ControlPoint] = [] """ self.land_poly = geometry.Polygon(self.landmap[0][0]) for x in self.landmap[1]: self.land_poly = self.land_poly.difference(geometry.Polygon(x)) """ - def add_controlpoint(self, point: ControlPoint, connected_to: [ControlPoint] = []): + def add_controlpoint(self, point: ControlPoint, + connected_to: Optional[List[ControlPoint]] = None): + if connected_to is None: + connected_to = [] for connected_point in connected_to: point.connect(to=connected_point) @@ -108,15 +117,15 @@ class ConflictTheater: return True - def player_points(self) -> typing.Collection[ControlPoint]: + def player_points(self) -> List[ControlPoint]: return [point for point in self.controlpoints if point.captured] - def conflicts(self, from_player=True) -> typing.Collection[typing.Tuple[ControlPoint, ControlPoint]]: + def conflicts(self, from_player=True) -> Iterator[Tuple[ControlPoint, ControlPoint]]: for cp in [x for x in self.controlpoints if x.captured == from_player]: for connected_point in [x for x in cp.connected_points if x.captured != from_player]: - yield (cp, connected_point) + yield cp, connected_point - def enemy_points(self) -> typing.Collection[ControlPoint]: + def enemy_points(self) -> List[ControlPoint]: return [point for point in self.controlpoints if not point.captured] def add_json_cp(self, theater, p: dict) -> ControlPoint: @@ -205,7 +214,7 @@ class CaucasusTheater(ConflictTheater): class PersianGulfTheater(ConflictTheater): - terrain = dcs.terrain.PersianGulf() + terrain = persiangulf.PersianGulf() overview_image = "persiangulf.gif" reference_points = { (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( @@ -221,7 +230,7 @@ class PersianGulfTheater(ConflictTheater): class NevadaTheater(ConflictTheater): - terrain = dcs.terrain.Nevada() + terrain = nevada.Nevada() overview_image = "nevada.gif" reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } @@ -235,7 +244,7 @@ class NevadaTheater(ConflictTheater): class NormandyTheater(ConflictTheater): - terrain = dcs.terrain.Normandy() + terrain = normandy.Normandy() overview_image = "normandy.gif" reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} @@ -249,7 +258,7 @@ class NormandyTheater(ConflictTheater): class TheChannelTheater(ConflictTheater): - terrain = dcs.terrain.TheChannel() + terrain = thechannel.TheChannel() overview_image = "thechannel.gif" reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} @@ -263,7 +272,7 @@ class TheChannelTheater(ConflictTheater): class SyriaTheater(ConflictTheater): - terrain = dcs.terrain.Syria() + terrain = syria.Syria() overview_image = "syria.gif" reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} diff --git a/theater/controlpoint.py b/theater/controlpoint.py index fa211f47..6f520bd1 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -1,8 +1,8 @@ import re -import typing +from typing import Dict, List from enum import Enum -from dcs.mapping import * +from dcs.mapping import Point from dcs.ships import ( CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov, @@ -13,6 +13,7 @@ from dcs.terrain.terrain import Airport from game import db from gen.ground_forces.combat_stance import CombatStance +from .base import Base from .missiontarget import MissionTarget from .theatergroundobject import TheaterGroundObject @@ -27,35 +28,26 @@ class ControlPointType(Enum): class ControlPoint(MissionTarget): - id = 0 position = None # type: Point name = None # type: str - full_name = None # type: str - base = None # type: theater.base.Base - at = None # type: db.StartPosition allow_sea_units = True - connected_points = None # type: typing.List[ControlPoint] - ground_objects = None # type: typing.List[TheaterGroundObject] - captured = False has_frontline = True frontline_offset = 0.0 - cptype: ControlPointType = None alt = 0 - def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float, - has_frontline=True, cptype=ControlPointType.AIRBASE): - import theater.base - + def __init__(self, id: int, name: str, position: Point, + at: db.StartingPosition, radials: List[int], size: int, + importance: float, has_frontline=True, + cptype=ControlPointType.AIRBASE): self.id = id self.name = " ".join(re.split(r" |-", name)[:2]) self.full_name = name self.position: Point = position self.at = at - self.ground_objects = [] - self.ships = [] + self.ground_objects: List[TheaterGroundObject] = [] self.size = size self.importance = importance @@ -63,14 +55,14 @@ class ControlPoint(MissionTarget): self.captured_invert = False self.has_frontline = has_frontline self.radials = radials - self.connected_points = [] - self.base = theater.base.Base() + self.connected_points: List[ControlPoint] = [] + self.base: Base = Base() self.cptype = cptype - self.stances = {} + self.stances: Dict[int, CombatStance] = {} self.airport = None @classmethod - def from_airport(cls, airport: Airport, radials: typing.Collection[int], size: int, importance: float, has_frontline=True): + def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): assert airport obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE) obj.airport = airport() @@ -128,7 +120,7 @@ class ControlPoint(MissionTarget): return self.cptype in [ControlPointType.LHA_GROUP] @property - def sea_radials(self) -> typing.Collection[int]: + def sea_radials(self) -> List[int]: # TODO: fix imports all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ] result = [] @@ -195,11 +187,11 @@ class ControlPoint(MissionTarget): def is_connected(self, to) -> bool: return to in self.connected_points - def find_radial(self, heading: int, ignored_radial: int = None): + def find_radial(self, heading: int, ignored_radial: int = None) -> int: closest_radial = 0 closest_radial_delta = 360 for radial in [x for x in self.radials if x != ignored_radial]: - delta = math.fabs(radial - heading) + delta = abs(radial - heading) if delta < closest_radial_delta: closest_radial = radial closest_radial_delta = delta diff --git a/theater/landmap.py b/theater/landmap.py index c5384da7..6eaaf5fe 100644 --- a/theater/landmap.py +++ b/theater/landmap.py @@ -1,11 +1,11 @@ import pickle -import typing +from typing import Collection, Optional, Tuple -Zone = typing.Collection[typing.Tuple[float, float]] -Landmap = typing.Tuple[typing.Collection[Zone], typing.Collection[Zone]] +Zone = Collection[Tuple[float, float]] +Landmap = Tuple[Collection[Zone], Collection[Zone]] -def load_landmap(filename: str) -> Landmap: +def load_landmap(filename: str) -> Optional[Landmap]: try: with open(filename, "rb") as f: return pickle.load(f) @@ -30,7 +30,7 @@ def poly_contains(x, y, poly): p1x, p1y = p2x, p2y return inside -def poly_centroid(poly) -> typing.Tuple[float, float]: +def poly_centroid(poly) -> Tuple[float, float]: x_list = [vertex[0] for vertex in poly] y_list = [vertex[1] for vertex in poly] x = sum(x_list) / len(poly) diff --git a/theater/start_generator.py b/theater/start_generator.py index d459cbb1..b14f1b99 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -1,19 +1,35 @@ +import logging import math import pickle import random import typing -import logging +from dcs.mapping import Point +from dcs.task import CAP, CAS, PinpointStrike +from dcs.vehicles import AirDefence + +from game import db from game.data.building_data import DEFAULT_AVAILABLE_BUILDINGS from game.settings import Settings -from gen import namegen, TheaterGroundObject +from gen import namegen from gen.defenses.armor_group_generator import generate_armor_group -from gen.fleet.ship_group_generator import generate_carrier_group, generate_lha_group, generate_ship_group +from gen.fleet.ship_group_generator import ( + generate_carrier_group, + generate_lha_group, + generate_ship_group, +) from gen.missiles.missiles_group_generator import generate_missile_group -from gen.sam.sam_group_generator import generate_anti_air_group, generate_shorad_group -from theater import ControlPointType -from theater.base import * -from theater.conflicttheater import * +from gen.sam.sam_group_generator import ( + generate_anti_air_group, + generate_shorad_group, +) +from theater import ( + ConflictTheater, + ControlPoint, + ControlPointType, + TheaterGroundObject, +) +from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW UNIT_VARIETY = 3 UNIT_AMOUNT_FACTOR = 16 diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 3ffa86f2..659e366e 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,7 +1,12 @@ import uuid +from typing import List, TYPE_CHECKING from dcs.mapping import Point +from dcs.unitgroup import Group +if TYPE_CHECKING: + from .conflicttheater import ConflictTheater + from .controlpoint import ControlPoint from .missiontarget import MissionTarget NAME_BY_CATEGORY = { @@ -71,7 +76,7 @@ class TheaterGroundObject(MissionTarget): airbase_group = False heading = 0 position = None # type: Point - groups = [] + groups: List[Group] = [] obj_name = "" sea_object = False uuid = uuid.uuid1() From 9e96aee89f4182b30b806790ef5d9026cbd37710 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 00:50:46 -0700 Subject: [PATCH 39/48] Add default escort loadouts. --- game/db.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/game/db.py b/game/db.py index 6ffa6cd7..97c3c4fc 100644 --- a/game/db.py +++ b/game/db.py @@ -122,6 +122,7 @@ from dcs.task import ( CAS, CargoTransportation, Embarking, + Escort, GroundAttack, Intercept, MainTask, @@ -1082,7 +1083,8 @@ COMMON_OVERRIDE = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP", } PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { @@ -1094,7 +1096,8 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP HEAVY", }, F_A_18C: { CAP: "CAP HEAVY", @@ -1103,7 +1106,8 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = { PinpointStrike: "STRIKE", SEAD: "SEAD", AntishipStrike: "ANTISHIP", - GroundAttack: "STRIKE" + GroundAttack: "STRIKE", + Escort: "CAP HEAVY", }, A_10A: COMMON_OVERRIDE, A_10C: COMMON_OVERRIDE, From e0725ff139832da1d0de83e7987d151c68197a0a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 4 Oct 2020 14:42:24 -0700 Subject: [PATCH 40/48] Add escort mission planning to the UI. --- gen/ato.py | 1 + qt_ui/widgets/combos/QFlightTypeComboBox.py | 4 ++-- qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gen/ato.py b/gen/ato.py index a2e4a8a1..ff1bb5a9 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -89,6 +89,7 @@ class Package: FlightType.CAP, FlightType.BARCAP, FlightType.EWAR, + FlightType.ESCORT, ] for task in task_priorities: if flight_counts[task]: diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index 8da41217..9577b26c 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -18,11 +18,11 @@ class QFlightTypeComboBox(QComboBox): """Combo box for selecting a flight task type.""" COMMON_ENEMY_MISSIONS = [ + FlightType.ESCORT, FlightType.TARCAP, FlightType.SEAD, FlightType.DEAD, # TODO: FlightType.ELINT, - # TODO: FlightType.ESCORT, # TODO: FlightType.EWAR, # TODO: FlightType.RECON, ] @@ -42,9 +42,9 @@ class QFlightTypeComboBox(QComboBox): ] ENEMY_CARRIER_MISSIONS = [ + FlightType.ESCORT, FlightType.TARCAP, # TODO: FlightType.ANTISHIP - # TODO: FlightType.ESCORT, ] ENEMY_AIRBASE_MISSIONS = [ diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 2879c356..5e9e57a2 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -57,6 +57,7 @@ class QFlightWaypointTab(QFrame): recreate_types = [ FlightType.CAS, FlightType.CAP, + FlightType.ESCORT, FlightType.SEAD, FlightType.STRIKE ] From 93db1254ec1259173cfa4d7c05f8d8e467cf6cde Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 4 Oct 2020 14:35:10 -0700 Subject: [PATCH 41/48] Improve insufficient aircraft message. Specify only the tasks which were unfulfilled so the player knows what to buy. --- gen/flights/ai_flight_planner.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 36b9d929..d9621457 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -406,12 +406,21 @@ class CoalitionMissionPlanner: self.game.aircraft_inventory, self.is_player ) + + missing_types: Set[FlightType] = set() for proposed_flight in mission.flights: if not builder.plan_flight(proposed_flight): - builder.release_planned_aircraft() - self.message("Insufficient aircraft", - f"Not enough aircraft in range for {mission}") - return + missing_types.add(proposed_flight.task) + + if missing_types: + missing_types_str = ", ".join( + sorted([t.name for t in missing_types])) + builder.release_planned_aircraft() + self.message( + "Insufficient aircraft", + f"Not enough aircraft in range for {mission.location.name} " + f"capable of: {missing_types_str}") + return package = builder.build() flight_plan_builder = FlightPlanBuilder(self.game, package, From 023925d741ec88d67edbd29b493ddf5557cfc07a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 4 Oct 2020 14:23:49 -0700 Subject: [PATCH 42/48] Set preferred mission types for aircraft. --- gen/flights/ai_flight_planner.py | 80 +++++++++++++++------ gen/flights/ai_flight_planner_db.py | 108 ++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 21 deletions(-) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index d9621457..5deb0c38 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -3,9 +3,9 @@ from __future__ import annotations import logging import operator from dataclasses import dataclass -from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple +from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type -from dcs.unittype import UnitType +from dcs.unittype import FlyingType, UnitType from game import db from game.data.radar_db import UNITS_WITH_RADAR @@ -15,9 +15,13 @@ from gen import Conflict from gen.ato import Package from gen.flights.ai_flight_planner_db import ( CAP_CAPABLE, + CAP_PREFERRED, CAS_CAPABLE, + CAS_PREFERRED, SEAD_CAPABLE, + SEAD_PREFERRED, STRIKE_CAPABLE, + STRIKE_PREFERRED, ) from gen.flights.closestairfields import ( ClosestAirfields, @@ -102,30 +106,63 @@ class AircraftAllocator: maximum allowed range. If insufficient aircraft are available for the mission, None is returned. + Airfields are searched ordered nearest to farthest from the target and + searched twice. The first search looks for aircraft which prefer the + mission type, and the second search looks for any aircraft which are + capable of the mission type. For example, an F-14 from a nearby carrier + will be preferred for the CAP of an airfield that has only F-16s, but if + the carrier has only F/A-18s the F-16s will be used for CAP instead. + Note that aircraft *will* be removed from the global inventory on success. This is to ensure that the same aircraft are not matched twice on subsequent calls. If the found aircraft are not used, the caller is responsible for returning them to the inventory. """ - cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) - if flight.task in cap_missions: - types = CAP_CAPABLE - elif flight.task == FlightType.CAS: - types = CAS_CAPABLE - elif flight.task in (FlightType.DEAD, FlightType.SEAD): - types = SEAD_CAPABLE - elif flight.task == FlightType.STRIKE: - types = STRIKE_CAPABLE - elif flight.task == FlightType.ESCORT: - types = CAP_CAPABLE - else: - logging.error(f"Unplannable flight type: {flight.task}") - return None + result = self.find_aircraft_of_type( + flight, self.preferred_aircraft_for_task(flight.task) + ) + if result is not None: + return result + return self.find_aircraft_of_type( + flight, self.capable_aircraft_for_task(flight.task) + ) - # TODO: Implement mission type weighting for aircraft. - # We should avoid assigning F/A-18s to CAP missions when there are F-15s - # available, since the F/A-18 is capable of performing other tasks that - # the F-15 is not capable of. + @staticmethod + def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: + cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) + if task in cap_missions: + return CAP_PREFERRED + elif task == FlightType.CAS: + return CAS_PREFERRED + elif task in (FlightType.DEAD, FlightType.SEAD): + return SEAD_PREFERRED + elif task == FlightType.STRIKE: + return STRIKE_PREFERRED + elif task == FlightType.ESCORT: + return CAP_PREFERRED + else: + return [] + + @staticmethod + def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: + cap_missions = (FlightType.BARCAP, FlightType.CAP, FlightType.TARCAP) + if task in cap_missions: + return CAP_CAPABLE + elif task == FlightType.CAS: + return CAS_CAPABLE + elif task in (FlightType.DEAD, FlightType.SEAD): + return SEAD_CAPABLE + elif task == FlightType.STRIKE: + return STRIKE_CAPABLE + elif task == FlightType.ESCORT: + return CAP_CAPABLE + else: + logging.error(f"Unplannable flight type: {task}") + return [] + + def find_aircraft_of_type( + self, flight: ProposedFlight, types: List[Type[FlyingType]], + ) -> Optional[Tuple[ControlPoint, UnitType]]: airfields_in_range = self.closest_airfields.airfields_within( flight.max_distance ) @@ -312,7 +349,8 @@ class ObjectiveFinder: yield from cp.ground_objects yield from self.front_lines() - def closest_airfields_to(self, location: MissionTarget) -> ClosestAirfields: + @staticmethod + def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: """Returns the closest airfields to the given location.""" return ObjectiveDistanceCache.get_closest_airfields(location) diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index dd51fd26..715a7f66 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -80,6 +80,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M +# TODO: These lists really ought to be era (faction) dependent. +# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but +# factions that also have F-4s should not. + INTERCEPT_CAPABLE = [ MiG_21Bis, MiG_25PD, @@ -150,6 +154,42 @@ CAP_CAPABLE = [ Rafale_M, ] +CAP_PREFERRED = [ + MiG_15bis, + MiG_19P, + MiG_21Bis, + MiG_23MLD, + MiG_25PD, + MiG_29A, + MiG_29G, + MiG_29S, + MiG_31, + + Su_27, + J_11A, + Su_30, + Su_33, + + M_2000C, + Mirage_2000_5, + + F_86F_Sabre, + F_14B, + F_15C, + + P_51D_30_NA, + P_51D, + + SpitfireLFMkIXCW, + SpitfireLFMkIX, + + Bf_109K_4, + FW_190D9, + FW_190A8, + + Rafale_M, +] + # Used for CAS (Close air support) and BAI (Battlefield Interdiction) CAS_CAPABLE = [ @@ -228,6 +268,59 @@ CAS_CAPABLE = [ RQ_1A_Predator ] +CAS_PREFERRED = [ + Su_17M4, + Su_24M, + Su_24MR, + Su_25, + Su_25T, + Su_25TM, + Su_34, + + JF_17, + + A_10A, + A_10C, + A_10C_2, + AV8BNA, + + F_15E, + + Tornado_GR4, + + C_101CC, + MB_339PAN, + L_39ZA, + AJS37, + + SA342M, + SA342L, + OH_58D, + + AH_64A, + AH_64D, + AH_1W, + + UH_1H, + + Mi_8MT, + Mi_28N, + Mi_24V, + Ka_50, + + P_47D_30, + P_47D_30bl1, + P_47D_40, + A_20G, + + A_4E_C, + Rafale_A_S, + + WingLoong_I, + MQ_9_Reaper, + RQ_1A_Predator +] + # Aircraft used for SEAD / DEAD tasks SEAD_CAPABLE = [ F_4E, @@ -252,6 +345,12 @@ SEAD_CAPABLE = [ Rafale_A_S ] +SEAD_PREFERRED = [ + F_4E, + Su_25T, + Tornado_IDS, +] + # Aircraft used for Strike mission STRIKE_CAPABLE = [ MiG_15bis, @@ -309,6 +408,15 @@ STRIKE_CAPABLE = [ ] +STRIKE_PREFERRED = [ + AJS37, + F_15E, + Tornado_GR4, + + A_20G, + B_17G, +] + ANTISHIP_CAPABLE = [ Su_24M, Su_17M4, From 944748a0acbd3b5d64e9f8c916f02b2954db6e3d Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 21:09:25 -0700 Subject: [PATCH 43/48] Don't throw away exception info on save/load. Makes it much easier to determine what we did that broke save game compatibility. --- game/persistency.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/game/persistency.py b/game/persistency.py index 4e0b216f..617274ea 100644 --- a/game/persistency.py +++ b/game/persistency.py @@ -41,30 +41,33 @@ def restore_game(): try: save = pickle.load(f) return save - except: - logging.error("Invalid Save game") + except Exception: + logging.exception("Invalid Save game") return None + def load_game(path): with open(path, "rb") as f: try: save = pickle.load(f) save.savepath = path return save - except: - logging.error("Invalid Save game") + except Exception: + logging.exception("Invalid Save game") return None + def save_game(game) -> bool: try: with open(_temporary_save_file(), "wb") as f: pickle.dump(game, f) shutil.copy(_temporary_save_file(), game.savepath) return True - except Exception as e: - logging.error(e) + except Exception: + logging.exception("Could not save game") return False + def autosave(game) -> bool: """ Autosave to the autosave location @@ -75,7 +78,7 @@ def autosave(game) -> bool: with open(_autosave_path(), "wb") as f: pickle.dump(game, f) return True - except Exception as e: - logging.error(e) + except Exception: + logging.exception("Could not save game") return False From e537396fec52a82eff26de82ee2d2234c2241eac Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 21:48:13 -0700 Subject: [PATCH 44/48] Stagger package start times. Avoids crowding the taxiways, and adds some life to the end of the mission. Later on, this will happen more naturally because we can delay takeoffs to align with the package's DTOT. --- gen/ato.py | 2 ++ gen/flights/ai_flight_planner.py | 34 +++++++++++++++++++ .../windows/mission/flight/QFlightCreator.py | 2 ++ 3 files changed, 38 insertions(+) diff --git a/gen/ato.py b/gen/ato.py index ff1bb5a9..ee3a3c55 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -40,6 +40,8 @@ class Package: #: The set of flights in the package. flights: List[Flight] = field(default_factory=list) + delay: int = field(default=0) + 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) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 5deb0c38..7bc3fd26 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import random import operator from dataclasses import dataclass from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type @@ -384,6 +385,9 @@ class CoalitionMissionPlanner: MAX_SEAD_RANGE = nm_to_meter(150) MAX_STRIKE_RANGE = nm_to_meter(150) + NON_CAP_MIN_DELAY = 1 + NON_CAP_MAX_DELAY = 5 + def __init__(self, game: Game, is_player: bool) -> None: self.game = game self.is_player = is_player @@ -430,6 +434,8 @@ class CoalitionMissionPlanner: for proposed_mission in self.propose_missions(): self.plan_mission(proposed_mission) + self.stagger_missions() + for cp in self.objective_finder.friendly_control_points(): inventory = self.game.aircraft_inventory.for_control_point(cp) for aircraft, available in inventory.all_aircraft: @@ -467,6 +473,34 @@ class CoalitionMissionPlanner: flight_plan_builder.populate_flight_plan(flight) self.ato.add_package(package) + def stagger_missions(self) -> None: + def start_time_generator(count: int, earliest: int, latest: int, + margin: int) -> Iterator[int]: + interval = latest // count + for time in range(earliest, latest, interval): + error = random.randint(-margin, margin) + yield max(0, time + error) + + dca_types = ( + FlightType.BARCAP, + FlightType.CAP, + FlightType.INTERCEPTION, + ) + + non_dca_packages = [p for p in self.ato.packages if + p.primary_task not in dca_types] + + start_time = start_time_generator( + count=len(non_dca_packages), + earliest=5, + latest=90, + margin=5 + ) + for package in non_dca_packages: + package.delay = next(start_time) + for flight in package.flights: + flight.scheduled_in = package.delay + def message(self, title, text) -> None: """Emits a planning message to the player. diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index fc3f9415..2c8c7dfe 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -27,6 +27,7 @@ class QFlightCreator(QDialog): super().__init__() self.game = game + self.package = package self.setWindowTitle("Create flight") self.setWindowIcon(EVENT_ICONS["strike"]) @@ -90,6 +91,7 @@ class QFlightCreator(QDialog): size = self.flight_size_spinner.value() flight = Flight(aircraft, size, origin, task) + flight.scheduled_in = self.package.delay # noinspection PyUnresolvedReferences self.created.emit(flight) From 1c4aec83cbf6ab04f79dc7fc2db18d714e6a70fd Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 22:24:47 -0700 Subject: [PATCH 45/48] Clean up start-up logging. Most of this wasn't helpful. What was is now logging instead of print so it can be configured. --- qt_ui/liberation_theme.py | 11 ++++++----- qt_ui/uiconstants.py | 5 ----- qt_ui/widgets/map/QLiberationMap.py | 3 ++- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/qt_ui/liberation_theme.py b/qt_ui/liberation_theme.py index 258a8683..79714209 100644 --- a/qt_ui/liberation_theme.py +++ b/qt_ui/liberation_theme.py @@ -1,4 +1,5 @@ import json +import logging import os from typing import Dict @@ -28,25 +29,25 @@ def init(): global __theme_index __theme_index = DEFAULT_THEME_INDEX - print("init setting theme index to " + str(__theme_index)) if os.path.isfile(THEME_PREFERENCES_FILE_PATH): try: with(open(THEME_PREFERENCES_FILE_PATH)) as prefs: pref_data = json.loads(prefs.read()) __theme_index = pref_data["theme_index"] - print(__theme_index) set_theme_index(__theme_index) save_theme_config() - print("file setting theme index to " + str(__theme_index)) except: # is this necessary? set_theme_index(DEFAULT_THEME_INDEX) - print("except setting theme index to " + str(__theme_index)) + logging.exception("Unable to change theme") else: # is this necessary? set_theme_index(DEFAULT_THEME_INDEX) - print("else setting theme index to " + str(__theme_index)) + logging.error( + f"Using default theme because {THEME_PREFERENCES_FILE_PATH} " + "does not exist" + ) # set theme index then use save_theme_config to save to file diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 706ff3ab..5c97dc72 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -111,15 +111,12 @@ EVENT_ICONS: Dict[str, QPixmap] = {} def load_event_icons(): for image in os.listdir("./resources/ui/events/"): - print(image) if image.endswith(".PNG"): EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image)) def load_aircraft_icons(): for aircraft in os.listdir("./resources/ui/units/aircrafts/"): - print(aircraft) if aircraft.endswith(".jpg"): - print(aircraft[:-7] + " : " + os.path.join("./resources/ui/units/aircrafts/", aircraft) + " ") AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft)) AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"] AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"] @@ -128,7 +125,5 @@ def load_aircraft_icons(): def load_vehicle_icons(): for vehicle in os.listdir("./resources/ui/units/vehicles/"): - print(vehicle) if vehicle.endswith(".jpg"): - print(vehicle[:-7] + " : " + os.path.join("./resources/ui/units/vehicles/", vehicle) + " ") VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle)) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 8654a364..3d775367 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,3 +1,4 @@ +import logging from typing import Dict, List, Optional, Tuple from PySide2.QtCore import Qt @@ -74,7 +75,7 @@ class QLiberationMap(QGraphicsView): def setGame(self, game: Optional[Game]): self.game = game - print("Reloading Map Canvas") + logging.debug("Reloading Map Canvas") if self.game is not None: self.reload_scene() From 1d7f1082ea8d726d7089e7595d9843aa639bdb01 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 23:26:18 -0700 Subject: [PATCH 46/48] Fix logging by deferring campaign data loading. Logging before we've made it to the logging setup was causing the root logger to be permanently configured to the default (warning) log level, so we weren't getting any info or debug logs any more. Defer the campaign data load until it is needed rather than doing it at import time. I've also cleaned up a bit so we only load each campaign once, rather than re-loading the campaign to create the theater again after the wizard is finished. --- qt_ui/windows/newgame/QCampaignList.py | 78 +++++++++++++++---------- qt_ui/windows/newgame/QNewGameWizard.py | 29 +++++---- theater/conflicttheater.py | 58 +++++++++--------- 3 files changed, 93 insertions(+), 72 deletions(-) diff --git a/qt_ui/windows/newgame/QCampaignList.py b/qt_ui/windows/newgame/QCampaignList.py index 9329edfd..5d3de21c 100644 --- a/qt_ui/windows/newgame/QCampaignList.py +++ b/qt_ui/windows/newgame/QCampaignList.py @@ -1,46 +1,62 @@ +from __future__ import annotations + import json import logging -import os +from dataclasses import dataclass +from pathlib import Path +from typing import List from PySide2 import QtGui -from PySide2.QtCore import QSize, QItemSelectionModel -from PySide2.QtGui import QStandardItemModel, QStandardItem -from PySide2.QtWidgets import QListView, QAbstractItemView +from PySide2.QtCore import QItemSelectionModel +from PySide2.QtGui import QStandardItem, QStandardItemModel +from PySide2.QtWidgets import QAbstractItemView, QListView -from theater import caucasus, nevada, persiangulf, normandy, thechannel, syria, ConflictTheater import qt_ui.uiconstants as CONST +from theater import ConflictTheater -CAMPAIGN_DIR = ".\\resources\\campaigns" -CAMPAIGNS = [] -# Load the campaigns files from the directory -campaign_files = os.listdir(CAMPAIGN_DIR) -for f in campaign_files: - try: - ff = os.path.join(CAMPAIGN_DIR, f) - with open(ff, "r") as campaign_data: - data = json.load(campaign_data) - choice = (data["name"], ff, "Terrain_" + data["theater"].replace(" ", "")) - logging.info("Loaded campaign : " + data["name"]) - CAMPAIGNS.append(choice) - ConflictTheater.from_file(choice[1]) - logging.info("Loaded campaign :" + ff) - except Exception as e: - logging.info("Unable to load campaign :" + f) -CAMPAIGNS = sorted(CAMPAIGNS, key=lambda x: x[0]) +@dataclass(frozen=True) +class Campaign: + name: str + icon_name: str + theater: ConflictTheater + + @classmethod + def from_json(cls, path: Path) -> Campaign: + with path.open() as campaign_file: + data = json.load(campaign_file) + + sanitized_theater = data["theater"].replace(" ", "") + return cls(data["name"], f"Terrain_{sanitized_theater}", + ConflictTheater.from_json(data)) + + +def load_campaigns() -> List[Campaign]: + campaign_dir = Path("resources\\campaigns") + campaigns = [] + for path in campaign_dir.iterdir(): + try: + logging.debug(f"Loading campaign from {path}...") + campaign = Campaign.from_json(path) + campaigns.append(campaign) + except RuntimeError: + logging.exception(f"Unable to load campaign from {path}") + + return sorted(campaigns, key=lambda x: x.name) + class QCampaignItem(QStandardItem): - def __init__(self, text, filename, icon): + def __init__(self, campaign: Campaign) -> None: super(QCampaignItem, self).__init__() - self.filename = filename - self.setIcon(QtGui.QIcon(CONST.ICONS[icon])) + self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name])) self.setEditable(False) - self.setText(text) + self.setText(campaign.name) + class QCampaignList(QListView): - def __init__(self): + def __init__(self, campaigns: List[Campaign]) -> None: super(QCampaignList, self).__init__() self.model = QStandardItemModel(self) self.setModel(self.model) @@ -48,12 +64,12 @@ class QCampaignList(QListView): self.setMinimumHeight(350) self.campaigns = [] self.setSelectionBehavior(QAbstractItemView.SelectItems) - self.setup_content() + self.setup_content(campaigns) - def setup_content(self): - for i, campaign in enumerate(CAMPAIGNS): + def setup_content(self, campaigns: List[Campaign]) -> None: + for campaign in campaigns: self.campaigns.append(campaign) - item = QCampaignItem(*campaign) + item = QCampaignItem(campaign) self.model.appendRow(item) self.setSelectedCampaign(0) self.repaint() diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 9b82f14f..8a706ec7 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -2,27 +2,34 @@ from __future__ import unicode_literals import datetime import logging +from typing import List from PySide2 import QtGui, QtWidgets -from PySide2.QtCore import QPoint, QItemSelectionModel -from PySide2.QtWidgets import QHBoxLayout, QVBoxLayout +from PySide2.QtCore import QItemSelectionModel, QPoint +from PySide2.QtWidgets import QVBoxLayout from dcs.task import CAP, CAS import qt_ui.uiconstants as CONST -from game import db, Game +from game import Game, db from game.settings import Settings from gen import namegen -from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS -from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel +from qt_ui.windows.newgame.QCampaignList import ( + Campaign, + QCampaignList, + load_campaigns, +) +from theater import ConflictTheater, start_generator class NewGameWizard(QtWidgets.QWizard): def __init__(self, parent=None): super(NewGameWizard, self).__init__(parent) + self.campaigns = load_campaigns() + self.addPage(IntroPage()) self.addPage(FactionSelection()) - self.addPage(TheaterConfiguration()) + self.addPage(TheaterConfiguration(self.campaigns)) self.addPage(MiscOptions()) self.addPage(ConclusionPage()) @@ -43,9 +50,9 @@ class NewGameWizard(QtWidgets.QWizard): selectedCampaign = self.field("selectedCampaign") if selectedCampaign is None: - selectedCampaign = CAMPAIGNS[0] + selectedCampaign = self.campaigns[0] - conflictTheater = ConflictTheater.from_file(selectedCampaign[1]) + conflictTheater = selectedCampaign.theater timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]] midGame = self.field("midGame") @@ -232,8 +239,8 @@ class FactionSelection(QtWidgets.QWizardPage): class TheaterConfiguration(QtWidgets.QWizardPage): - def __init__(self, parent=None): - super(TheaterConfiguration, self).__init__(parent) + def __init__(self, campaigns: List[Campaign], parent=None) -> None: + super().__init__(parent) self.setTitle("Theater configuration") self.setSubTitle("\nChoose a terrain and time period for this game.") @@ -244,7 +251,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage): QtGui.QPixmap('./resources/ui/wizard/watermark3.png')) # List of campaigns - campaignList = QCampaignList() + campaignList = QCampaignList(campaigns) self.registerField("selectedCampaign", campaignList) def on_campaign_selected(): diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index d92def90..c9013062 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import json -from typing import Dict, Iterator, List, Optional, Tuple +from typing import Any, Dict, Iterator, List, Optional, Tuple from dcs.mapping import Point from dcs.terrain import ( @@ -163,39 +165,35 @@ class ConflictTheater: return cp @staticmethod - def from_file(filename): - with open(filename, "r") as content: - json_data = json.loads(content.read()) + def from_json(data: Dict[str, Any]) -> ConflictTheater: + theaters = { + "Caucasus": CaucasusTheater, + "Nevada": NevadaTheater, + "Persian Gulf": PersianGulfTheater, + "Normandy": NormandyTheater, + "The Channel": TheChannelTheater, + "Syria": SyriaTheater, + } + theater = theaters[data["theater"]] + t = theater() + cps = {} + for p in data["player_points"]: + cp = t.add_json_cp(theater, p) + cp.captured = True + cps[p["id"]] = cp + t.add_controlpoint(cp) - theaters = { - "Caucasus": CaucasusTheater, - "Nevada": NevadaTheater, - "Persian Gulf": PersianGulfTheater, - "Normandy": NormandyTheater, - "The Channel": TheChannelTheater, - "Syria": SyriaTheater, - } - theater = theaters[json_data["theater"]] - t = theater() - cps = {} + for p in data["enemy_points"]: + cp = t.add_json_cp(theater, p) + cps[p["id"]] = cp + t.add_controlpoint(cp) - for p in json_data["player_points"]: - cp = t.add_json_cp(theater, p) - cp.captured = True - cps[p["id"]] = cp - t.add_controlpoint(cp) + for l in data["links"]: + cps[l[0]].connect(cps[l[1]]) + cps[l[1]].connect(cps[l[0]]) - for p in json_data["enemy_points"]: - cp = t.add_json_cp(theater, p) - cps[p["id"]] = cp - t.add_controlpoint(cp) - - for l in json_data["links"]: - cps[l[0]].connect(cps[l[1]]) - cps[l[1]].connect(cps[l[0]]) - - return t + return t class CaucasusTheater(ConflictTheater): From 6bfb8cf2fde43880692c6b7b7a54147dd2d83a7b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 23:31:40 -0700 Subject: [PATCH 47/48] Fix double logging. Calling logging.basicConfig creates a stream handler for the root logger, and then we were adding our own with a different formatter. Pass the format string to basicConfig so we don't need to add our own duplicate stream handler. --- qt_ui/logging_config.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/qt_ui/logging_config.py b/qt_ui/logging_config.py index 5febea7d..739e8ac0 100644 --- a/qt_ui/logging_config.py +++ b/qt_ui/logging_config.py @@ -1,26 +1,22 @@ +"""Logging APIs.""" import logging import os from logging.handlers import RotatingFileHandler -def init_logging(version_string): +def init_logging(version: str) -> None: + """Initializes the logging configuration.""" if not os.path.isdir("./logs"): os.mkdir("logs") - logging.basicConfig(level="DEBUG") + fmt = "%(asctime)s :: %(levelname)s :: %(message)s" + logging.basicConfig(level=logging.DEBUG, format=fmt) logger = logging.getLogger() - formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') - handler = RotatingFileHandler('./logs/liberation.log', 'a', 5000000, 1) handler.setLevel(logging.INFO) - handler.setFormatter(formatter) + handler.setFormatter(logging.Formatter(fmt)) - stream_handler = logging.StreamHandler() - stream_handler.setLevel(logging.DEBUG) - stream_handler.setFormatter(formatter) - - logger.addHandler(stream_handler) logger.addHandler(handler) - logger.info("DCS Liberation {}".format(version_string)) + logger.info(f"DCS Liberation {version}") From 4abf80683773fa9d6668bb933754f02c5aded798 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 6 Oct 2020 23:44:11 -0700 Subject: [PATCH 48/48] Fix initial frequencies for support aircraft. Vaicom (a mod that adds voice control for the communications menus) isn't able to follow the waypoint frequency change that normally sets the radio channel for the AWACS/tanker flights. Set the group's frequency correctly to start so it works. --- gen/airsupportgen.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index ce00e9d5..8a98dba7 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -89,6 +89,7 @@ class AirSupportConflictGenerator: speed=574, tacanchannel=str(tacan), ) + tanker_group.set_frequency(freq.mhz) callsign = callsign_for_support_unit(tanker_group) tacan_callsign = { @@ -131,6 +132,8 @@ class AirSupportConflictGenerator: frequency=freq.mhz, start_type=StartType.Warm, ) + awacs_flight.set_frequency(freq.mhz) + awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True))