diff --git a/changelog.md b/changelog.md index 88fc71ab..400e1303 100644 --- a/changelog.md +++ b/changelog.md @@ -23,7 +23,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings. * **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets. * **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows. -* **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map. +* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map. * **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present. * **[UI]** DCS loadouts are now selectable in the loadout setup menu. * **[UI]** Added global aircraft inventory view under Air Wing dialog. diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py deleted file mode 100644 index 2abf369c..00000000 --- a/qt_ui/displayoptions.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Visibility options for the game map.""" -from dataclasses import dataclass, field -from typing import Iterator, Optional, Union - - -@dataclass -class DisplayRule: - name: str - _value: bool - debug_only: bool = field(default=False) - - @property - def menu_text(self) -> str: - return self.name - - @property - def value(self) -> bool: - return self._value - - @value.setter - def value(self, value: bool) -> None: - from qt_ui.widgets.map.QLiberationMap import QLiberationMap - - self._value = value - if QLiberationMap.instance is not None: - QLiberationMap.instance.reload_scene() - QLiberationMap.instance.update() - - def __bool__(self) -> bool: - return self.value - - -class DisplayGroup: - def __init__(self, name: Optional[str], debug_only: bool = False) -> None: - self.name = name - self.debug_only = debug_only - - def __iter__(self) -> Iterator[DisplayRule]: - # Python 3.6 enforces that __dict__ is order preserving by default. - for value in self.__dict__.values(): - if isinstance(value, DisplayRule): - yield value - - -class FlightPathOptions(DisplayGroup): - def __init__(self) -> None: - super().__init__("Flight Paths") - self.hide = DisplayRule("Hide Flight Paths", False) - self.only_selected = DisplayRule("Show Selected Flight Path", False) - self.all = DisplayRule("Show All Flight Paths", True) - - -class ThreatZoneOptions(DisplayGroup): - def __init__(self, coalition_name: str) -> None: - super().__init__(f"{coalition_name} Threat Zones") - self.none = DisplayRule(f"Hide {coalition_name.lower()} threat zones", True) - self.all = DisplayRule( - f"Show full {coalition_name.lower()} threat zones", False - ) - self.aircraft = DisplayRule( - f"Show {coalition_name.lower()} aircraft threat tones", False - ) - self.air_defenses = DisplayRule( - f"Show {coalition_name.lower()} air defenses threat zones", False - ) - - -class NavMeshOptions(DisplayGroup): - def __init__(self) -> None: - super().__init__("Navmeshes", debug_only=True) - self.hide = DisplayRule("DEBUG Hide Navmeshes", True) - self.blue_navmesh = DisplayRule("DEBUG Show blue navmesh", False) - self.red_navmesh = DisplayRule("DEBUG Show red navmesh", False) - - -class PathDebugFactionOptions(DisplayGroup): - def __init__(self) -> None: - super().__init__("Faction for path debugging", debug_only=True) - self.blue = DisplayRule("Debug blue paths", True) - self.red = DisplayRule("Debug red paths", False) - - -class PathDebugOptions(DisplayGroup): - def __init__(self) -> None: - super().__init__("Shortest paths", debug_only=True) - self.hide = DisplayRule("DEBUG Hide paths", True) - self.shortest_path = DisplayRule("DEBUG Show shortest path", False) - self.barcap = DisplayRule("DEBUG Show BARCAP plan", False) - self.cas = DisplayRule("DEBUG Show CAS plan", False) - self.sweep = DisplayRule("DEBUG Show fighter sweep plan", False) - self.strike = DisplayRule("DEBUG Show strike plan", False) - self.tarcap = DisplayRule("DEBUG Show TARCAP plan", False) - - -class DisplayOptions: - ground_objects = DisplayRule("Ground Objects", True) - control_points = DisplayRule("Control Points", True) - lines = DisplayRule("Lines", True) - sam_ranges = DisplayRule("Ally SAM Threat Range", False) - enemy_sam_ranges = DisplayRule("Enemy SAM Threat Range", True) - detection_range = DisplayRule("SAM Detection Range", False) - map_poly = DisplayRule("Map Polygon Debug Mode", False) - waypoint_info = DisplayRule("Waypoint Information", True) - culling = DisplayRule("Display Culling Zones", False) - actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False) - patrol_engagement_range = DisplayRule( - "Display selected patrol engagement range", True - ) - flight_paths = FlightPathOptions() - blue_threat_zones = ThreatZoneOptions("Blue") - red_threat_zones = ThreatZoneOptions("Red") - navmeshes = NavMeshOptions() - path_debug_faction = PathDebugFactionOptions() - path_debug = PathDebugOptions() - - @classmethod - def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]: - debug = False # Set to True to enable debug options. - # Python 3.6 enforces that __dict__ is order preserving by default. - for value in cls.__dict__.values(): - if isinstance(value, DisplayRule): - if value.debug_only and not debug: - continue - yield value - elif isinstance(value, DisplayGroup): - if value.debug_only and not debug: - continue - yield value diff --git a/qt_ui/main.py b/qt_ui/main.py index cbc3f08c..46b7fc1c 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -57,7 +57,7 @@ def inject_custom_payloads(user_path: Path) -> None: PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads") -def run_ui(game: Optional[Game], new_map: bool) -> None: +def run_ui(game: Optional[Game]) -> None: os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens app = QApplication(sys.argv) @@ -111,7 +111,7 @@ def run_ui(game: Optional[Game], new_map: bool) -> None: GameUpdateSignal() # Start window - window = QLiberationWindow(game, new_map) + window = QLiberationWindow(game) window.showMaximized() splash.finish(window) qt_execution_code = app.exec_() @@ -139,16 +139,8 @@ def parse_args() -> argparse.Namespace: help="Emits a warning for weapons without date or fallback information.", ) - parser.add_argument( - "--new-map", - action="store_true", - default=True, - help="Use the new map. Functional but missing many display options.", - ) - - parser.add_argument( - "--old-map", dest="new_map", action="store_false", help="Use the old map." - ) + parser.add_argument("--new-map", help="Deprecated. Does nothing.") + parser.add_argument("--old-map", help="Deprecated. Does nothing.") new_game = subparsers.add_parser("new-game") @@ -267,7 +259,7 @@ def main(): args.cheats, ) - run_ui(game, args.new_map) + run_ui(game) if __name__ == "__main__": diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index b8d4f36c..19bc1945 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -1,7 +1,7 @@ import os from typing import Dict -from PySide2.QtGui import QColor, QFont, QPixmap +from PySide2.QtGui import QPixmap from game.theater.theatergroundobject import NAME_BY_CATEGORY from .liberation_theme import get_theme_icons @@ -16,51 +16,6 @@ URLS: Dict[str, str] = { LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Off"] SKILL_OPTIONS = ["Average", "Good", "High", "Excellent"] -FONT_SIZE = 8 -FONT_NAME = "Arial" -# FONT = QFont("Arial", 12, weight=5, italic=True) -FONT_PRIMARY = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=False) -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) - -COLORS: Dict[str, QColor] = { - "white": QColor(255, 255, 255), - "white_transparent": QColor(255, 255, 255, 35), - "light_red": QColor(231, 92, 83, 90), - "red": QColor(200, 80, 80), - "dark_red": QColor(140, 20, 20), - "red_transparent": QColor(227, 32, 0, 20), - "transparent": QColor(255, 255, 255, 0), - "light_blue": QColor(105, 182, 240, 90), - "blue": QColor(0, 132, 255), - "dark_blue": QColor(45, 62, 80), - "sea_blue": QColor(52, 68, 85), - "sea_blue_transparent": QColor(52, 68, 85, 150), - "blue_transparent": QColor(0, 132, 255, 20), - "purple": QColor(187, 137, 255), - "yellow": QColor(238, 225, 123), - "bright_red": QColor(150, 80, 80), - "super_red": QColor(227, 32, 0), - "green": QColor(128, 186, 128), - "light_green": QColor(223, 255, 173), - "light_green_transparent": QColor(180, 255, 140, 50), - "bright_green": QColor(64, 200, 64), - "black": QColor(0, 0, 0), - "black_transparent": QColor(0, 0, 0, 5), - "orange": QColor(254, 125, 10), - "night_overlay": QColor(12, 20, 69), - "dawn_dust_overlay": QColor(46, 38, 85), - "grey": QColor(150, 150, 150), - "grey_transparent": QColor(150, 150, 150, 150), - "dark_grey": QColor(75, 75, 75), - "dark_grey_transparent": QColor(75, 75, 75, 150), - "dark_dark_grey": QColor(48, 48, 48), - "dark_dark_grey_transparent": QColor(48, 48, 48, 150), -} - -CP_SIZE = 12 - AIRCRAFT_BANNERS: Dict[str, QPixmap] = {} AIRCRAFT_ICONS: Dict[str, QPixmap] = {} VEHICLE_BANNERS: Dict[str, QPixmap] = {} @@ -138,17 +93,6 @@ def load_icons(): "./resources/ui/misc/" + get_theme_icons() + "/ordnance_icon.png" ) - ICONS["target"] = QPixmap("./resources/ui/ground_assets/target.png") - ICONS["cleared"] = QPixmap("./resources/ui/ground_assets/cleared.png") - for category in NAME_BY_CATEGORY.keys(): - ICONS[category] = QPixmap("./resources/ui/ground_assets/" + category + ".png") - ICONS[category + "_blue"] = QPixmap( - "./resources/ui/ground_assets/" + category + "_blue.png" - ) - ICONS["destroyed"] = QPixmap("./resources/ui/ground_assets/destroyed.png") - ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png") - ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png") - ICONS["Generator"] = QPixmap( "./resources/ui/misc/" + get_theme_icons() + "/generator.png" ) diff --git a/qt_ui/widgets/QLiberationCalendar.py b/qt_ui/widgets/QLiberationCalendar.py index 43e0d469..c33d8810 100644 --- a/qt_ui/widgets/QLiberationCalendar.py +++ b/qt_ui/widgets/QLiberationCalendar.py @@ -1,8 +1,6 @@ -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2 import QtCore, QtGui from PySide2.QtWidgets import QCalendarWidget -from qt_ui.uiconstants import COLORS - class QLiberationCalendar(QCalendarWidget): def __init__(self, parent=None): @@ -29,7 +27,7 @@ class QLiberationCalendar(QCalendarWidget): painter.save() painter.fillRect(rect, QtGui.QColor("#D3D3D3")) painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(QtGui.QColor(COLORS["sea_blue"])) + painter.setBrush(QtGui.QColor(52, 68, 85)) r = QtCore.QRect( QtCore.QPoint(), min(rect.width(), rect.height()) * QtCore.QSize(1, 1) ) diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py deleted file mode 100644 index 0e886d5d..00000000 --- a/qt_ui/widgets/map/QFrontLine.py +++ /dev/null @@ -1,117 +0,0 @@ -"""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 game.theater import FrontLine -from qt_ui.dialogs import Dialog -from qt_ui.models import GameModel -from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog - - -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: FrontLine, - game_model: GameModel, - ) -> None: - super().__init__(x1, y1, x2, y2) - self.mission_target = mission_target - self.game_model = game_model - 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) - - if self.game_model.game.settings.enable_frontline_cheats: - cheat_forward = QAction(f"CHEAT: Advance Frontline") - cheat_forward.triggered.connect(self.cheat_forward) - menu.addAction(cheat_forward) - - cheat_backward = QAction(f"CHEAT: Retreat Frontline") - cheat_backward.triggered.connect(self.cheat_backward) - menu.addAction(cheat_backward) - - 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) - - def cheat_forward(self) -> None: - self.mission_target.blue_cp.base.affect_strength(0.1) - self.mission_target.red_cp.base.affect_strength(-0.1) - # Clear the ATO to replan missions affected by the front line. - self.game_model.game.reset_ato() - self.game_model.game.initialize_turn() - GameUpdateSignal.get_instance().updateGame(self.game_model.game) - - def cheat_backward(self) -> None: - self.mission_target.blue_cp.base.affect_strength(-0.1) - self.mission_target.red_cp.base.affect_strength(0.1) - # Clear the ATO to replan missions affected by the front line. - self.game_model.game.reset_ato() - self.game_model.game.initialize_turn() - GameUpdateSignal.get_instance().updateGame(self.game_model.game) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 94fe115d..5d7f2d7d 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -1,131 +1,21 @@ from __future__ import annotations -import datetime import logging -import math -from functools import singledispatchmethod from pathlib import Path from typing import ( - Iterable, - Iterator, - List, Optional, - Sequence, - Tuple, ) -from PySide2 import QtCore, QtWidgets -from PySide2.QtCore import QLineF, QPointF, QRectF, Qt, QUrl -from PySide2.QtGui import ( - QBrush, - QColor, - QFont, - QPen, - QPixmap, - QPolygonF, - QWheelEvent, -) +from PySide2.QtCore import QUrl from PySide2.QtWebChannel import QWebChannel from PySide2.QtWebEngineWidgets import ( QWebEnginePage, QWebEngineView, ) -from PySide2.QtWidgets import ( - QFrame, - QGraphicsItem, - QGraphicsOpacityEffect, - QGraphicsScene, - QGraphicsSceneMouseEvent, - QGraphicsView, -) -from dcs import Point -from dcs.mapping import point_from_heading -from dcs.unitgroup import Group -from shapely.geometry import ( - LineString, - MultiPolygon, - Point as ShapelyPoint, - Polygon, -) -import qt_ui.uiconstants as CONST from game import Game -from game.navmesh import NavMesh -from game.theater import ControlPoint, Enum -from game.theater.conflicttheater import ( - FrontLine, - ReferencePoint, -) -from game.theater.theatergroundobject import ( - TheaterGroundObject, -) -from game.transfers import Convoy -from game.utils import Distance, meters, nautical_miles, pairwise -from game.weather import TimeOfDay -from gen import Conflict, Package -from gen.flights.flight import ( - Flight, - FlightType, - FlightWaypoint, - FlightWaypointType, -) -from gen.flights.flightplan import ( - FlightPlan, - FlightPlanBuilder, - InvalidObjectiveLocation, - PatrollingFlightPlan, -) -from gen.flights.traveltime import TotEstimator -from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions from qt_ui.models import GameModel -from qt_ui.widgets.map.QFrontLine import QFrontLine -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.ShippingLaneSegment import ShippingLaneSegment -from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment from qt_ui.widgets.map.mapmodel import MapModel -from qt_ui.windows.GameUpdateSignal import GameUpdateSignal - -MAX_SHIP_DISTANCE = nautical_miles(80) - - -MapPoint = Tuple[float, float] - - -def binomial(i: int, n: int) -> float: - """Binomial coefficient""" - return math.factorial(n) / float(math.factorial(i) * math.factorial(n - i)) - - -def bernstein(t: float, i: int, n: int) -> float: - """Bernstein polynom""" - return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i)) - - -def bezier(t: float, points: Sequence[Tuple[float, float]]) -> Tuple[float, float]: - """Calculate coordinate of a point in the bezier curve""" - n = len(points) - 1 - x = y = 0 - for i, pos in enumerate(points): - bern = bernstein(t, i, n) - x += pos[0] * bern - y += pos[1] * bern - return x, y - - -def bezier_curve_range( - n: int, points: Sequence[Tuple[float, float]] -) -> Iterator[Tuple[float, float]]: - """Range of points in a curve bezier""" - for i in range(n): - t = i / float(n - 1) - yield bezier(t, points) - - -class QLiberationMapState(Enum): - NORMAL = 0 - MOVING_UNIT = 1 class LoggingWebPage(QWebEnginePage): @@ -144,12 +34,7 @@ class LoggingWebPage(QWebEnginePage): logging.info(message) -class LiberationMap: - def set_game(self, game: Optional[Game]) -> None: - raise NotImplementedError - - -class LeafletMap(QWebEngineView, LiberationMap): +class QLiberationMap(QWebEngineView): def __init__(self, game_model: GameModel, parent) -> None: super().__init__(parent) self.game_model = game_model @@ -171,1276 +56,3 @@ class LeafletMap(QWebEngineView, LiberationMap): self.map_model.clear() else: self.map_model.reset() - - -class QLiberationMap(QGraphicsView, LiberationMap): - - WAYPOINT_SIZE = 4 - reference_point_setup_mode = False - instance: Optional[QLiberationMap] = None - - def __init__(self, game_model: GameModel) -> None: - super().__init__() - QLiberationMap.instance = self - self.game_model = game_model - self.game: Optional[Game] = None # Setup by set_game below. - self.state = QLiberationMapState.NORMAL - - self.waypoint_info_font = QFont() - self.waypoint_info_font.setPointSize(12) - - self.flight_path_items: List[QGraphicsItem] = [] - # A tuple of (package index, flight index), or none. - self.selected_flight: Optional[Tuple[int, int]] = None - - self.setMinimumSize(800, 600) - self.setMaximumHeight(2160) - self._zoom = 0 - self.factor = 1 - self.factorized = 1 - self.init_scene() - self.set_game(game_model.game) - - # Object displayed when unit is selected - self.movement_line = QtWidgets.QGraphicsLineItem( - QtCore.QLineF(QPointF(0, 0), QPointF(0, 0)) - ) - self.movement_line.setPen(QPen(CONST.COLORS["orange"], width=10.0)) - self.selected_cp: Optional[QMapControlPoint] = None - - GameUpdateSignal.get_instance().flight_paths_changed.connect( - lambda: self.draw_flight_plans(self.scene()) - ) - - def update_package_selection(index: int) -> None: - # Optional[int] isn't a valid type for a Qt signal. None will be - # converted to zero automatically. We use -1 to indicate no - # selection. - if index == -1: - self.selected_flight = None - else: - self.selected_flight = index, 0 - self.draw_flight_plans(self.scene()) - - GameUpdateSignal.get_instance().package_selection_changed.connect( - update_package_selection - ) - - def update_flight_selection(index: int) -> None: - if self.selected_flight is None: - if index != -1: - # We don't know what order update_package_selection and - # update_flight_selection will be called in when the last - # package is removed. If no flight is selected, it's not a - # problem to also have no package selected. - logging.error("Flight was selected with no package selected") - return - - # Optional[int] isn't a valid type for a Qt signal. None will be - # converted to zero automatically. We use -1 to indicate no - # selection. - if index == -1: - self.selected_flight = self.selected_flight[0], None - self.selected_flight = self.selected_flight[0], index - self.draw_flight_plans(self.scene()) - - GameUpdateSignal.get_instance().flight_selection_changed.connect( - update_flight_selection - ) - - self.nm_to_pixel_ratio: int = 0 - - self.navmesh_highlight: Optional[QPolygonF] = None - self.shortest_path_segments: List[QLineF] = [] - - def init_scene(self): - scene = QLiberationScene(self) - self.setScene(scene) - self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) - self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) - self.setBackgroundBrush(QBrush(QColor(30, 30, 30))) - self.setFrameShape(QFrame.NoFrame) - self.setDragMode(QGraphicsView.ScrollHandDrag) - - def set_game(self, game: Optional[Game]): - should_recenter = self.game is None - self.game = game - if self.game is not None: - logging.debug("Reloading Map Canvas") - self.nm_to_pixel_ratio = self.distance_to_pixels(nautical_miles(1)) - self.reload_scene(should_recenter) - - """ - - Uncomment to set up theather reference points""" - - def keyPressEvent(self, event): - modifiers = QtWidgets.QApplication.keyboardModifiers() - if not self.reference_point_setup_mode: - if modifiers == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_R: - self.reference_point_setup_mode = True - self.reload_scene() - else: - super(QLiberationMap, self).keyPressEvent(event) - else: - if modifiers == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_R: - self.reference_point_setup_mode = False - self.reload_scene() - else: - distance = 1 - modifiers = int(event.modifiers()) - if modifiers & QtCore.Qt.ShiftModifier: - distance *= 10 - elif modifiers & QtCore.Qt.ControlModifier: - distance *= 100 - - if event.key() == QtCore.Qt.Key_Down: - self.update_reference_point( - self.game.theater.reference_points[0], Point(0, distance) - ) - if event.key() == QtCore.Qt.Key_Up: - self.update_reference_point( - self.game.theater.reference_points[0], Point(0, -distance) - ) - if event.key() == QtCore.Qt.Key_Left: - self.update_reference_point( - self.game.theater.reference_points[0], Point(-distance, 0) - ) - if event.key() == QtCore.Qt.Key_Right: - self.update_reference_point( - self.game.theater.reference_points[0], Point(distance, 0) - ) - - if event.key() == QtCore.Qt.Key_S: - self.update_reference_point( - self.game.theater.reference_points[1], Point(0, distance) - ) - if event.key() == QtCore.Qt.Key_W: - self.update_reference_point( - self.game.theater.reference_points[1], Point(0, -distance) - ) - if event.key() == QtCore.Qt.Key_A: - self.update_reference_point( - self.game.theater.reference_points[1], Point(-distance, 0) - ) - if event.key() == QtCore.Qt.Key_D: - self.update_reference_point( - self.game.theater.reference_points[1], Point(distance, 0) - ) - - logging.debug(f"Reference points: {self.game.theater.reference_points}") - self.reload_scene() - - @staticmethod - def update_reference_point(point: ReferencePoint, change: Point) -> None: - point.image_coordinates += change - - def display_culling(self, scene: QGraphicsScene) -> None: - """Draws the culling distance rings on the map""" - culling_zones = self.game_model.game.get_culling_zones() - culling_distance = self.game_model.game.settings.perf_culling_distance - for zone in culling_zones: - culling_distance_zone = Point( - zone.x + culling_distance * 1000, zone.y + culling_distance * 1000 - ) - distance_zone = self._transform_point(culling_distance_zone) - transformed = self._transform_point(zone) - radius = distance_zone[0] - transformed[0] - scene.addEllipse( - transformed[0] - radius, - transformed[1] - radius, - 2 * radius, - 2 * radius, - CONST.COLORS["transparent"], - CONST.COLORS["light_green_transparent"], - ) - - def draw_shapely_poly( - self, scene: QGraphicsScene, poly: Polygon, pen: QPen, brush: QBrush - ) -> Optional[QPolygonF]: - if poly.is_empty: - return None - points = [] - for x, y in poly.exterior.coords: - x, y = self._transform_point(Point(x, y)) - points.append(QPointF(x, y)) - return scene.addPolygon(QPolygonF(points), pen, brush) - - def draw_threat_zone( - self, scene: QGraphicsScene, poly: Polygon, player: bool - ) -> None: - if player: - brush = QColor(0, 132, 255, 100) - else: - brush = QColor(227, 32, 0, 100) - self.draw_shapely_poly(scene, poly, CONST.COLORS["transparent"], brush) - - def display_threat_zones( - self, scene: QGraphicsScene, options: ThreatZoneOptions, player: bool - ) -> None: - """Draws the threat zones on the map.""" - threat_zones = self.game.threat_zone_for(player) - if options.all: - threat_poly = threat_zones.all - elif options.aircraft: - threat_poly = threat_zones.airbases - elif options.air_defenses: - threat_poly = threat_zones.air_defenses - else: - return - - if isinstance(threat_poly, MultiPolygon): - polys = threat_poly.geoms - else: - polys = [threat_poly] - for poly in polys: - self.draw_threat_zone(scene, poly, player) - - def draw_navmesh_neighbor_line( - self, scene: QGraphicsScene, poly: Polygon, begin: ShapelyPoint - ) -> None: - vertex = Point(begin.x, begin.y) - centroid = poly.centroid - direction = Point(centroid.x, centroid.y) - end = vertex.point_from_heading( - vertex.heading_between_point(direction), nautical_miles(2).meters - ) - - scene.addLine( - QLineF( - QPointF(*self._transform_point(vertex)), - QPointF(*self._transform_point(end)), - ), - CONST.COLORS["yellow"], - ) - - @singledispatchmethod - def draw_navmesh_border( - self, intersection, scene: QGraphicsScene, poly: Polygon - ) -> None: - raise NotImplementedError( - "draw_navmesh_border not implemented for %s", - intersection.__class__.__name__, - ) - - @draw_navmesh_border.register - def draw_navmesh_point_border( - self, intersection: ShapelyPoint, scene: QGraphicsScene, poly: Polygon - ) -> None: - # Draw a line from the vertex toward the center of the polygon. - self.draw_navmesh_neighbor_line(scene, poly, intersection) - - @draw_navmesh_border.register - def draw_navmesh_edge_border( - self, intersection: LineString, scene: QGraphicsScene, poly: Polygon - ) -> None: - # Draw a line from the center of the edge toward the center of the - # polygon. - edge_center = intersection.interpolate(0.5, normalized=True) - self.draw_navmesh_neighbor_line(scene, poly, edge_center) - - def display_navmesh(self, scene: QGraphicsScene, player: bool) -> None: - for navpoly in self.game.navmesh_for(player).polys: - self.draw_shapely_poly( - scene, navpoly.poly, CONST.COLORS["black"], CONST.COLORS["transparent"] - ) - - position = self._transform_point( - Point(navpoly.poly.centroid.x, navpoly.poly.centroid.y) - ) - text = scene.addSimpleText( - f"Navmesh {navpoly.ident}", self.waypoint_info_font - ) - text.setBrush(QColor(255, 255, 255)) - text.setPen(QColor(255, 255, 255)) - text.moveBy(position[0] + 8, position[1]) - text.setZValue(2) - - for border in navpoly.neighbors.values(): - self.draw_navmesh_border(border, scene, navpoly.poly) - - def highlight_mouse_navmesh( - self, scene: QGraphicsScene, navmesh: NavMesh, mouse_position: Point - ) -> None: - if self.navmesh_highlight is not None: - try: - scene.removeItem(self.navmesh_highlight) - except RuntimeError: - pass - navpoly = navmesh.localize(mouse_position) - if navpoly is None: - return - self.navmesh_highlight = self.draw_shapely_poly( - scene, - navpoly.poly, - CONST.COLORS["transparent"], - CONST.COLORS["light_green_transparent"], - ) - - def draw_shortest_path( - self, scene: QGraphicsScene, navmesh: NavMesh, destination: Point, player: bool - ) -> None: - for line in self.shortest_path_segments: - try: - scene.removeItem(line) - except RuntimeError: - pass - - if player: - origin = self.game.theater.player_points()[0] - else: - origin = self.game.theater.enemy_points()[0] - - prev_pos = self._transform_point(origin.position) - try: - path = navmesh.shortest_path(origin.position, destination) - except ValueError: - return - for waypoint in path[1:]: - new_pos = self._transform_point(waypoint) - flight_path_pen = self.flight_path_pen(player, selected=True) - # Draw the line to the *middle* of the waypoint. - offset = self.WAYPOINT_SIZE // 2 - self.shortest_path_segments.append( - scene.addLine( - prev_pos[0] + offset, - prev_pos[1] + offset, - new_pos[0] + offset, - new_pos[1] + offset, - flight_path_pen, - ) - ) - - self.shortest_path_segments.append( - scene.addEllipse( - new_pos[0], - new_pos[1], - self.WAYPOINT_SIZE, - self.WAYPOINT_SIZE, - flight_path_pen, - flight_path_pen, - ) - ) - - prev_pos = new_pos - - def draw_test_flight_plan( - self, - scene: QGraphicsScene, - task: FlightType, - point_near_target: Point, - player: bool, - ) -> None: - for line in self.shortest_path_segments: - try: - scene.removeItem(line) - except RuntimeError: - pass - - self.clear_flight_paths(scene) - - target = self.game.theater.closest_target(point_near_target) - - if player: - origin = self.game.theater.player_points()[0] - else: - origin = self.game.theater.enemy_points()[0] - - package = Package(target) - for squadron_list in self.game.air_wing_for(player=True).squadrons.values(): - squadron = squadron_list[0] - break - else: - logging.error("Player has no squadrons?") - return - - flight = Flight( - package, - self.game.country_for(player), - squadron, - 2, - task, - start_type="Warm", - departure=origin, - arrival=origin, - divert=None, - ) - package.add_flight(flight) - planner = FlightPlanBuilder(self.game, package, is_player=player) - try: - planner.populate_flight_plan(flight) - except InvalidObjectiveLocation: - return - - package.time_over_target = TotEstimator(package).earliest_tot() - self.draw_flight_plan(scene, flight, selected=True) - - @staticmethod - def should_display_ground_objects_at(cp: ControlPoint) -> bool: - return (DisplayOptions.sam_ranges and cp.captured) or ( - DisplayOptions.enemy_sam_ranges and not cp.captured - ) - - def draw_threat_range( - self, - scene: QGraphicsScene, - group: Group, - ground_object: TheaterGroundObject, - cp: ControlPoint, - ) -> None: - go_pos = self._transform_point(ground_object.position) - detection_range = ground_object.detection_range(group) - threat_range = ground_object.threat_range(group) - if threat_range: - threat_pos = self._transform_point( - ground_object.position + Point(threat_range.meters, threat_range.meters) - ) - threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos)) - - # Add threat range circle - scene.addEllipse( - go_pos[0] - threat_radius / 2 + 7, - go_pos[1] - threat_radius / 2 + 6, - threat_radius, - threat_radius, - self.threat_pen(cp.captured), - ) - - if detection_range and DisplayOptions.detection_range: - # Add detection range circle - detection_pos = self._transform_point( - ground_object.position - + Point(detection_range.meters, detection_range.meters) - ) - detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos)) - scene.addEllipse( - go_pos[0] - detection_radius / 2 + 7, - go_pos[1] - detection_radius / 2 + 6, - detection_radius, - detection_radius, - self.detection_pen(cp.captured), - ) - - def draw_ground_objects(self, scene: QGraphicsScene, cp: ControlPoint) -> None: - added_objects = [] - for ground_object in cp.ground_objects: - if ground_object.obj_name in added_objects: - continue - - go_pos = self._transform_point(ground_object.position) - if not ground_object.airbase_group: - buildings = self.game.theater.find_ground_objects_by_obj_name( - ground_object.obj_name - ) - scene.addItem( - QMapGroundObject( - self, - go_pos[0], - go_pos[1], - 14, - 12, - cp, - ground_object, - self.game, - buildings, - ) - ) - - should_display = self.should_display_ground_objects_at(cp) - if ground_object.might_have_aa and should_display: - for group in ground_object.groups: - self.draw_threat_range(scene, group, ground_object, cp) - added_objects.append(ground_object.obj_name) - - def recenter(self) -> None: - center = self._transform_point( - self.game.theater.terrain.map_view_default.position - ) - self.centerOn(QPointF(center[0], center[1])) - - def reload_scene(self, recenter: bool = False) -> None: - scene = self.scene() - scene.clear() - - playerColor = self.game.get_player_color() - enemyColor = self.game.get_enemy_color() - - self.addBackground() - if recenter: - self.recenter() - - # Display Culling - if DisplayOptions.culling and self.game.settings.perf_culling: - self.display_culling(scene) - - self.display_threat_zones(scene, DisplayOptions.blue_threat_zones, player=True) - self.display_threat_zones(scene, DisplayOptions.red_threat_zones, player=False) - - if DisplayOptions.navmeshes.blue_navmesh: - self.display_navmesh(scene, player=True) - if DisplayOptions.navmeshes.red_navmesh: - self.display_navmesh(scene, player=False) - - for cp in self.game.theater.controlpoints: - - 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_model, - ) - ) - - if cp.captured: - pen = QPen(brush=CONST.COLORS[playerColor]) - brush = CONST.COLORS[playerColor + "_transparent"] - else: - pen = QPen(brush=CONST.COLORS[enemyColor]) - brush = CONST.COLORS[enemyColor + "_transparent"] - - self.draw_ground_objects(scene, cp) - - if cp.target_position is not None: - proj = self._transform_point(cp.target_position) - scene.addLine( - QLineF(QPointF(pos[0], pos[1]), QPointF(proj[0], proj[1])), - QPen(CONST.COLORS["green"], width=10, s=Qt.DashDotLine), - ) - - self.draw_supply_routes() - self.draw_flight_plans(scene) - - for cp in self.game.theater.controlpoints: - pos = self._transform_point(cp.position) - text = scene.addText(cp.name, font=CONST.FONT_MAP) - text.setPos(pos[0] + CONST.CP_SIZE, pos[1] - CONST.CP_SIZE / 2) - text = scene.addText(cp.name, font=CONST.FONT_MAP) - text.setDefaultTextColor(Qt.white) - text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1) - - def clear_flight_paths(self, scene: QGraphicsScene) -> 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() - - def draw_flight_plans(self, scene: QGraphicsScene) -> None: - self.clear_flight_paths(scene) - if DisplayOptions.flight_paths.hide: - return - packages = list(self.game_model.ato_model.packages) - if self.game.settings.show_red_ato: - packages.extend(self.game_model.red_ato_model.packages) - for p_idx, package_model in enumerate(packages): - for f_idx, flight in enumerate(package_model.flights): - if self.selected_flight is None: - selected = False - else: - selected = (p_idx, f_idx) == self.selected_flight - if DisplayOptions.flight_paths.only_selected and not selected: - continue - self.draw_flight_plan(scene, flight, selected) - - def draw_flight_plan( - self, scene: QGraphicsScene, flight: Flight, selected: bool - ) -> None: - is_player = flight.from_cp.captured - pos = self._transform_point(flight.from_cp.position) - - self.draw_waypoint(scene, pos, is_player, selected) - prev_pos = tuple(pos) - drew_target = False - target_types = ( - FlightWaypointType.TARGET_GROUP_LOC, - FlightWaypointType.TARGET_POINT, - FlightWaypointType.TARGET_SHIP, - ) - for idx, point in enumerate(flight.flight_plan.waypoints[1:]): - if point.waypoint_type == FlightWaypointType.DIVERT: - # Don't clutter the map showing divert points. - continue - - new_pos = self._transform_point(Point(point.x, point.y)) - self.draw_flight_path(scene, prev_pos, new_pos, is_player, selected) - self.draw_waypoint(scene, new_pos, is_player, selected) - if selected and DisplayOptions.waypoint_info: - if point.waypoint_type in target_types: - if drew_target: - # Don't draw dozens of targets over each other. - continue - drew_target = True - self.draw_waypoint_info( - scene, idx + 1, point, new_pos, flight.flight_plan - ) - prev_pos = tuple(new_pos) - - if selected and DisplayOptions.patrol_engagement_range: - self.draw_patrol_commit_range(scene, flight) - - def draw_patrol_commit_range(self, scene: QGraphicsScene, flight: Flight) -> None: - if not isinstance(flight.flight_plan, PatrollingFlightPlan): - return - start = flight.flight_plan.patrol_start - end = flight.flight_plan.patrol_end - line = LineString( - [ - ShapelyPoint(start.x, start.y), - ShapelyPoint(end.x, end.y), - ] - ) - doctrine = self.game.faction_for(flight.departure.captured).doctrine - bubble = line.buffer(doctrine.cap_engagement_range.meters) - self.flight_path_items.append( - self.draw_shapely_poly( - scene, bubble, CONST.COLORS["yellow"], CONST.COLORS["transparent"] - ) - ) - - def draw_waypoint( - self, - scene: QGraphicsScene, - position: Tuple[float, float], - player: bool, - selected: bool, - ) -> None: - waypoint_pen = self.waypoint_pen(player, selected) - waypoint_brush = self.waypoint_brush(player, selected) - self.flight_path_items.append( - scene.addEllipse( - position[0], - position[1], - self.WAYPOINT_SIZE, - self.WAYPOINT_SIZE, - waypoint_pen, - waypoint_brush, - ) - ) - - def draw_waypoint_info( - self, - scene: QGraphicsScene, - number: int, - waypoint: FlightWaypoint, - position: Tuple[float, float], - flight_plan: FlightPlan, - ) -> None: - - altitude = int(waypoint.alt.feet) - altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL" - - prefix = "TOT" - time = flight_plan.tot_for_waypoint(waypoint) - if time is None: - prefix = "Depart" - time = flight_plan.depart_time_for_waypoint(waypoint) - if time is None: - tot = "" - else: - time = datetime.timedelta(seconds=int(time.total_seconds())) - tot = f"{prefix} T+{time}" - - pen = QPen(QColor("black"), 0.3) - brush = QColor("white") - - text = "\n".join( - [ - f"{number} {waypoint.name}", - f"{altitude} ft {altitude_type}", - tot, - ] - ) - - item = scene.addSimpleText(text, self.waypoint_info_font) - item.setFlag(QGraphicsItem.ItemIgnoresTransformations) - item.setBrush(brush) - item.setPen(pen) - item.moveBy(position[0] + 8, position[1]) - item.setZValue(2) - self.flight_path_items.append(item) - - def draw_flight_path( - self, - scene: QGraphicsScene, - pos0: Tuple[float, float], - pos1: Tuple[float, float], - player: bool, - selected: bool, - ) -> None: - flight_path_pen = self.flight_path_pen(player, selected) - # Draw the line to the *middle* of the waypoint. - offset = self.WAYPOINT_SIZE // 2 - self.flight_path_items.append( - scene.addLine( - pos0[0] + offset, - pos0[1] + offset, - pos1[0] + offset, - pos1[1] + offset, - flight_path_pen, - ) - ) - - def bezier_points( - self, points: Iterable[Point] - ) -> Iterator[Tuple[MapPoint, MapPoint]]: - # Thanks to Alquimista for sharing a python implementation of the bezier - # algorithm this is adapted from. - # https://gist.github.com/Alquimista/1274149#file-bezdraw-py - bezier_fixed_points = [] - for a, b in pairwise(points): - bezier_fixed_points.append(self._transform_point(a)) - bezier_fixed_points.append(self._transform_point(b)) - - old_point = bezier_fixed_points[0] - for point in bezier_curve_range( - int(len(bezier_fixed_points) * 2), bezier_fixed_points - ): - yield old_point, point - old_point = point - - def draw_bezier_frontline( - self, - scene: QGraphicsScene, - frontline: FrontLine, - convoys: List[Convoy], - ) -> None: - for a, b in self.bezier_points(frontline.points): - scene.addItem( - SupplyRouteSegment( - a[0], - a[1], - b[0], - b[1], - frontline.blue_cp, - frontline.red_cp, - convoys, - ) - ) - - def draw_supply_routes(self) -> None: - if not DisplayOptions.lines: - return - - seen = set() - for cp in self.game.theater.controlpoints: - seen.add(cp) - for connected in cp.connected_points: - if connected in seen: - continue - self.draw_supply_route_between(cp, connected) - for destination, shipping_lane in cp.shipping_lanes.items(): - if destination in seen: - continue - if cp.is_friendly(destination.captured): - self.draw_shipping_lane_between(cp, destination) - - def draw_shipping_lane_between(self, a: ControlPoint, b: ControlPoint) -> None: - ship_map = self.game.transfers.cargo_ships - ships = [] - ship = ship_map.find_transport(a, b) - if ship is not None: - ships.append(ship) - ship = ship_map.find_transport(b, a) - if ship is not None: - ships.append(ship) - - scene = self.scene() - for pa, pb in self.bezier_points(a.shipping_lanes[b]): - scene.addItem(ShippingLaneSegment(pa[0], pa[1], pb[0], pb[1], a, b, ships)) - - def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None: - scene = self.scene() - - convoy_map = self.game.transfers.convoys - convoys = [] - convoy = convoy_map.find_transport(a, b) - if convoy is not None: - convoys.append(convoy) - convoy = convoy_map.find_transport(b, a) - if convoy is not None: - convoys.append(convoy) - - if a.captured: - frontline = FrontLine(a, b) - else: - frontline = FrontLine(b, a) - if a.front_is_active(b): - if DisplayOptions.actual_frontline_pos: - self.draw_actual_frontline(scene, frontline, convoys) - else: - self.draw_frontline_approximation(scene, frontline, convoys) - else: - self.draw_bezier_frontline(scene, frontline, convoys) - - def draw_frontline_approximation( - self, - scene: QGraphicsScene, - frontline: FrontLine, - convoys: List[Convoy], - ) -> None: - posx = frontline.position - h = frontline.attack_heading - pos2 = self._transform_point(posx) - self.draw_bezier_frontline(scene, frontline, convoys) - p1 = point_from_heading(pos2[0], pos2[1], h + 180, 25) - p2 = point_from_heading(pos2[0], pos2[1], h, 25) - scene.addItem( - QFrontLine(p1[0], p1[1], p2[0], p2[1], frontline, self.game_model) - ) - - def draw_actual_frontline( - self, - scene: QGraphicsScene, - frontline: FrontLine, - convoys: List[Convoy], - ) -> None: - self.draw_bezier_frontline(scene, frontline, convoys) - vector = Conflict.frontline_vector(frontline, self.game.theater) - left_pos = self._transform_point(vector[0]) - right_pos = self._transform_point( - vector[0].point_from_heading(vector[1], vector[2]) - ) - scene.addItem( - QFrontLine( - left_pos[0], - left_pos[1], - right_pos[0], - right_pos[1], - frontline, - self.game_model, - ) - ) - - def draw_scale(self, scale_distance_nm=20, number_of_points=4): - - PADDING = 14 - POS_X = 0 - POS_Y = 10 - BIG_LINE = 5 - SMALL_LINE = 2 - - dist = self.distance_to_pixels(nautical_miles(scale_distance_nm)) - l = self.scene().addLine( - POS_X + PADDING, - POS_Y + BIG_LINE * 2, - POS_X + PADDING + dist, - POS_Y + BIG_LINE * 2, - ) - l.setPen(CONST.COLORS["black"]) - - lw = self.scene().addLine( - POS_X + PADDING + 1, - POS_Y + BIG_LINE * 2 + 1, - POS_X + PADDING + dist + 1, - POS_Y + BIG_LINE * 2 + 1, - ) - lw.setPen(CONST.COLORS["white"]) - - text = self.scene().addText( - "0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False) - ) - text.setPos(POS_X, POS_Y + BIG_LINE * 2) - text.setDefaultTextColor(Qt.black) - - text_white = self.scene().addText( - "0nm", font=QFont("Trebuchet MS", 6, weight=5, italic=False) - ) - text_white.setPos(POS_X + 1, POS_Y + BIG_LINE * 2) - text_white.setDefaultTextColor(Qt.white) - - text2 = self.scene().addText( - str(scale_distance_nm) + "nm", - font=QFont("Trebuchet MS", 6, weight=5, italic=False), - ) - text2.setPos(POS_X + dist, POS_Y + BIG_LINE * 2) - text2.setDefaultTextColor(Qt.black) - - text2_white = self.scene().addText( - str(scale_distance_nm) + "nm", - font=QFont("Trebuchet MS", 6, weight=5, italic=False), - ) - text2_white.setPos(POS_X + dist + 1, POS_Y + BIG_LINE * 2) - text2_white.setDefaultTextColor(Qt.white) - - for i in range(number_of_points + 1): - d = float(i) / float(number_of_points) - if i == 0 or i == number_of_points: - h = BIG_LINE - else: - h = SMALL_LINE - - l = self.scene().addLine( - POS_X + PADDING + d * dist, - POS_Y + BIG_LINE * 2, - POS_X + PADDING + d * dist, - POS_Y + BIG_LINE - h, - ) - l.setPen(CONST.COLORS["black"]) - - lw = self.scene().addLine( - POS_X + PADDING + d * dist + 1, - POS_Y + BIG_LINE * 2, - POS_X + PADDING + d * dist + 1, - POS_Y + BIG_LINE - h, - ) - lw.setPen(CONST.COLORS["white"]) - - def wheelEvent(self, event: QWheelEvent): - if event.angleDelta().y() > 0: - factor = 1.25 - self._zoom += 1 - if self._zoom < 10: - self.scale(factor, factor) - self.factorized *= factor - else: - self._zoom = 9 - else: - factor = 0.8 - self._zoom -= 1 - if self._zoom > -5: - self.scale(factor, factor) - self.factorized *= factor - else: - self._zoom = -4 - - @staticmethod - def _transpose_point(p: Point) -> Point: - return Point(p.y, p.x) - - def _scaling_factor(self) -> Point: - point_a = self.game.theater.reference_points[0] - point_b = self.game.theater.reference_points[1] - - world_distance = self._transpose_point( - point_b.world_coordinates - point_a.world_coordinates - ) - image_distance = point_b.image_coordinates - point_a.image_coordinates - - x_scale = image_distance.x / world_distance.x - y_scale = image_distance.y / world_distance.y - return Point(x_scale, y_scale) - - # TODO: Move this and its inverse into ConflictTheater. - def _transform_point(self, world_point: Point) -> Tuple[float, float]: - """Transforms world coordinates to image coordinates. - - World coordinates are transposed. X increases toward the North, Y - increases toward the East. The origin point depends on the map. - - Image coordinates originate from the top left. X increases to the right, - Y increases toward the bottom. - - The two points should be as distant as possible in both latitude and - logitude, and tuning the reference points will be simpler if they are in - geographically recognizable locations. For example, the Caucasus map is - aligned using the first point on Gelendzhik and the second on Batumi. - - The distances between each point are computed and a scaling factor is - determined from that. The given point is then offset from the first - point using the scaling factor. - - X is latitude, increasing northward. - Y is longitude, increasing eastward. - """ - point_a = self.game.theater.reference_points[0] - scale = self._scaling_factor() - - offset = self._transpose_point(point_a.world_coordinates - world_point) - scaled = Point(offset.x * scale.x, offset.y * scale.y) - transformed = point_a.image_coordinates - scaled - return transformed.x, transformed.y - - def _scene_to_dcs_coords(self, scene_point: Point) -> Point: - point_a = self.game.theater.reference_points[0] - scale = self._scaling_factor() - - offset = point_a.image_coordinates - scene_point - scaled = self._transpose_point(Point(offset.x / scale.x, offset.y / scale.y)) - return point_a.world_coordinates - scaled - - def distance_to_pixels(self, distance: Distance) -> int: - p1 = Point(0, 0) - p2 = Point(0, distance.meters) - p1a = Point(*self._transform_point(p1)) - p2a = Point(*self._transform_point(p2)) - return int(p1a.distance_to_point(p2a)) - - def highlight_color(self, transparent: Optional[bool] = False) -> QColor: - return QColor(255, 255, 0, 20 if transparent else 255) - - 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, selected: bool) -> QColor: - if selected and DisplayOptions.flight_paths.all: - return self.highlight_color() - name = self.base_faction_color_name(player) - return CONST.COLORS[name] - - def waypoint_brush(self, player: bool, selected: bool) -> QColor: - if selected and DisplayOptions.flight_paths.all: - return self.highlight_color(transparent=True) - name = self.base_faction_color_name(player) - return CONST.COLORS[f"{name}_transparent"] - - def threat_pen(self, player: bool) -> QPen: - color = "blue" if player else "red" - return QPen(CONST.COLORS[color]) - - def detection_pen(self, player: bool) -> QPen: - color = "purple" if player else "yellow" - qpen = QPen(CONST.COLORS[color]) - qpen.setStyle(Qt.DotLine) - return qpen - - def flight_path_pen(self, player: bool, selected: bool) -> QPen: - if selected and DisplayOptions.flight_paths.all: - return self.highlight_color() - - 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() - - if not DisplayOptions.map_poly: - bg = QPixmap("./resources/" + self.game.theater.overview_image) - scene.addPixmap(bg) - - # Apply graphical effects to simulate current daytime - if self.game.current_turn_time_of_day == TimeOfDay.Day: - pass - elif self.game.current_turn_time_of_day == TimeOfDay.Night: - ov = QPixmap(bg.width(), bg.height()) - ov.fill(CONST.COLORS["night_overlay"]) - overlay = scene.addPixmap(ov) - effect = QGraphicsOpacityEffect() - effect.setOpacity(0.7) - overlay.setGraphicsEffect(effect) - else: - ov = QPixmap(bg.width(), bg.height()) - ov.fill(CONST.COLORS["dawn_dust_overlay"]) - overlay = scene.addPixmap(ov) - effect = QGraphicsOpacityEffect() - effect.setOpacity(0.3) - overlay.setGraphicsEffect(effect) - - if DisplayOptions.map_poly or self.reference_point_setup_mode: - # Polygon display mode - if self.game.theater.landmap is not None: - - for sea_zone in self.game.theater.landmap.sea_zones: - print(sea_zone) - poly = QPolygonF( - [ - QPointF(*self._transform_point(Point(point[0], point[1]))) - for point in sea_zone.exterior.coords - ] - ) - if self.reference_point_setup_mode: - color = "sea_blue_transparent" - else: - color = "sea_blue" - scene.addPolygon(poly, CONST.COLORS[color], CONST.COLORS[color]) - - for inclusion_zone in self.game.theater.landmap.inclusion_zones: - poly = QPolygonF( - [ - QPointF(*self._transform_point(Point(point[0], point[1]))) - for point in inclusion_zone.exterior.coords - ] - ) - if self.reference_point_setup_mode: - scene.addPolygon( - poly, - CONST.COLORS["grey_transparent"], - CONST.COLORS["dark_grey_transparent"], - ) - else: - scene.addPolygon( - poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"] - ) - - for exclusion_zone in self.game.theater.landmap.exclusion_zones: - poly = QPolygonF( - [ - QPointF(*self._transform_point(Point(point[0], point[1]))) - for point in exclusion_zone.exterior.coords - ] - ) - if self.reference_point_setup_mode: - scene.addPolygon( - poly, - CONST.COLORS["grey_transparent"], - CONST.COLORS["dark_dark_grey_transparent"], - ) - else: - scene.addPolygon( - poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"] - ) - - # Uncomment to display plan projection test - # self.projection_test() - self.draw_scale() - - if self.reference_point_setup_mode: - for i, point in enumerate(self.game.theater.reference_points): - self.scene().addRect( - QRectF( - point.image_coordinates.x, point.image_coordinates.y, 25, 25 - ), - pen=CONST.COLORS["red"], - brush=CONST.COLORS["red"], - ) - text = self.scene().addText( - f"P{i} = {point.image_coordinates}", - font=QFont("Trebuchet MS", 14, weight=8, italic=False), - ) - text.setDefaultTextColor(CONST.COLORS["red"]) - text.setPos(point.image_coordinates.x + 26, point.image_coordinates.y) - - # Set to True to visually debug _transform_point. - draw_transformed = False - if draw_transformed: - x, y = self._transform_point(point.world_coordinates) - self.scene().addRect( - QRectF(x, y, 25, 25), - pen=CONST.COLORS["red"], - brush=CONST.COLORS["red"], - ) - text = self.scene().addText( - f"P{i}' = {x}, {y}", - font=QFont("Trebuchet MS", 14, weight=8, italic=False), - ) - text.setDefaultTextColor(CONST.COLORS["red"]) - text.setPos(x + 26, y) - - def projection_test(self): - for i in range(100): - for j in range(100): - x = i * 100.0 - y = j * 100.0 - original = Point(x, y) - proj = self._scene_to_dcs_coords(original) - unproj = self._transform_point(proj) - converted = Point(*unproj) - assert math.isclose(original.x, converted.x, abs_tol=0.00000001) - assert math.isclose(original.y, converted.y, abs_tol=0.00000001) - - def setSelectedUnit(self, selected_cp: QMapControlPoint): - self.state = QLiberationMapState.MOVING_UNIT - self.selected_cp = selected_cp - position = self._transform_point(selected_cp.control_point.position) - self.movement_line = QtWidgets.QGraphicsLineItem( - QLineF(QPointF(*position), QPointF(*position)) - ) - self.scene().addItem(self.movement_line) - - def is_valid_ship_pos(self, scene_position: Point) -> bool: - world_destination = self._scene_to_dcs_coords(scene_position) - distance = self.selected_cp.control_point.position.distance_to_point( - world_destination - ) - if meters(distance) > MAX_SHIP_DISTANCE: - return False - return self.game.theater.is_in_sea(world_destination) - - def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent): - if self.game is None: - return - - mouse_position = Point(event.scenePos().x(), event.scenePos().y()) - if self.state == QLiberationMapState.MOVING_UNIT: - self.setCursor(Qt.PointingHandCursor) - self.movement_line.setLine( - QLineF(self.movement_line.line().p1(), event.scenePos()) - ) - - if self.is_valid_ship_pos(mouse_position): - self.movement_line.setPen(CONST.COLORS["green"]) - else: - self.movement_line.setPen(CONST.COLORS["red"]) - - mouse_world_pos = self._scene_to_dcs_coords(mouse_position) - if DisplayOptions.navmeshes.blue_navmesh: - self.highlight_mouse_navmesh( - self.scene(), - self.game.blue_navmesh, - self._scene_to_dcs_coords(mouse_position), - ) - if DisplayOptions.path_debug.shortest_path: - self.draw_shortest_path( - self.scene(), self.game.blue_navmesh, mouse_world_pos, player=True - ) - - if DisplayOptions.navmeshes.red_navmesh: - self.highlight_mouse_navmesh( - self.scene(), self.game.red_navmesh, mouse_world_pos - ) - - debug_blue = DisplayOptions.path_debug_faction.blue - if DisplayOptions.path_debug.shortest_path: - self.draw_shortest_path( - self.scene(), - self.game.navmesh_for(player=debug_blue), - mouse_world_pos, - player=False, - ) - elif not DisplayOptions.path_debug.hide: - if DisplayOptions.path_debug.barcap: - task = FlightType.BARCAP - elif DisplayOptions.path_debug.cas: - task = FlightType.CAS - elif DisplayOptions.path_debug.sweep: - task = FlightType.SWEEP - elif DisplayOptions.path_debug.strike: - task = FlightType.STRIKE - elif DisplayOptions.path_debug.tarcap: - task = FlightType.TARCAP - else: - raise ValueError("Unexpected value for DisplayOptions.path_debug") - self.draw_test_flight_plan( - self.scene(), task, mouse_world_pos, player=debug_blue - ) - - def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent): - if self.state == QLiberationMapState.MOVING_UNIT: - if event.buttons() == Qt.RightButton: - pass - elif event.buttons() == Qt.LeftButton: - if self.selected_cp is not None: - # Set movement position for the cp - pos = event.scenePos() - point = Point(int(pos.x()), int(pos.y())) - proj = self._scene_to_dcs_coords(point) - - if self.is_valid_ship_pos(point): - self.selected_cp.control_point.target_position = proj - else: - self.selected_cp.control_point.target_position = None - - GameUpdateSignal.get_instance().updateGame(self.game_model.game) - else: - return - self.state = QLiberationMapState.NORMAL - try: - self.scene().removeItem(self.movement_line) - except: - pass - self.selected_cp = None diff --git a/qt_ui/widgets/map/QLiberationScene.py b/qt_ui/widgets/map/QLiberationScene.py deleted file mode 100644 index fff8c379..00000000 --- a/qt_ui/widgets/map/QLiberationScene.py +++ /dev/null @@ -1,21 +0,0 @@ -from PySide2.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent - -import qt_ui.uiconstants as CONST - - -class QLiberationScene(QGraphicsScene): - def __init__(self, parent): - super().__init__(parent) - item = self.addText( - 'Go to "File/New Game" to setup a new campaign or go to "File/Open" to load an existing save game.', - CONST.FONT_PRIMARY, - ) - item.setDefaultTextColor(CONST.COLORS["white"]) - - def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): - super(QLiberationScene, self).mouseMoveEvent(event) - self.parent().sceneMouseMovedEvent(event) - - def mousePressEvent(self, event: QGraphicsSceneMouseEvent): - super(QLiberationScene, self).mousePressEvent(event) - self.parent().sceneMousePressEvent(event) diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py deleted file mode 100644 index b7016536..00000000 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import Optional - -from PySide2.QtGui import QColor, QPainter -from PySide2.QtWidgets import QAction, QMenu - -import qt_ui.uiconstants as const -from game.theater import ControlPoint, NavalControlPoint -from qt_ui.models import GameModel -from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 -from .QMapObject import QMapObject -from ...displayoptions import DisplayOptions -from ...windows.GameUpdateSignal import GameUpdateSignal - - -class QMapControlPoint(QMapObject): - def __init__( - self, - parent, - x: float, - y: float, - w: float, - h: float, - 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.control_point.name) - self.base_details_dialog: Optional[QBaseMenu2] = None - self.capture_action = QAction(f"CHEAT: Capture {self.control_point.name}") - self.capture_action.triggered.connect(self.cheat_capture) - - self.move_action = QAction("Move") - self.move_action.triggered.connect(self.move) - - self.cancel_move_action = QAction("Cancel Move") - self.cancel_move_action.triggered.connect(self.cancel_move) - - def paint(self, painter, option, widget=None) -> None: - if DisplayOptions.control_points: - painter.save() - painter.setRenderHint(QPainter.Antialiasing) - painter.setBrush(self.brush_color) - painter.setPen(self.pen_color) - - if not self.control_point.runway_is_operational(): - painter.setBrush(const.COLORS["black"]) - painter.setPen(self.brush_color) - - r = option.rect - painter.drawEllipse(r.x(), r.y(), r.width(), r.height()) - # TODO: Draw sunk carriers differently. - # Either don't draw them at all, or perhaps use a sunk ship icon. - painter.restore() - - @property - def brush_color(self) -> QColor: - if self.control_point.captured: - return const.COLORS["blue"] - else: - return const.COLORS["super_red"] - - @property - 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.control_point, self.game_model - ) - self.base_details_dialog.show() - - def add_context_menu_actions(self, menu: QMenu) -> None: - - if self.control_point.moveable and self.control_point.captured: - menu.addAction(self.move_action) - if self.control_point.target_position is not None: - menu.addAction(self.cancel_move_action) - - if self.control_point.is_fleet: - return - - if self.control_point.captured: - return - - for connected in self.control_point.connected_points: - if ( - connected.captured - and self.game_model.game.settings.enable_base_capture_cheat - ): - menu.addAction(self.capture_action) - break - - def cheat_capture(self) -> None: - self.control_point.capture(self.game_model.game, for_player=True) - # Reinitialized ground planners and the like. The ATO needs to be reset because - # missions planned against the flipped base are no longer valid. - self.game_model.game.reset_ato() - self.game_model.game.initialize_turn() - GameUpdateSignal.get_instance().updateGame(self.game_model.game) - - def move(self): - self.parent.setSelectedUnit(self) - - def cancel_move(self): - self.control_point.target_position = None - GameUpdateSignal.get_instance().updateGame(self.game_model.game) - - def open_new_package_dialog(self) -> None: - """Extends the default packagedialog to redirect to base menu for red air base.""" - is_navy = isinstance(self.control_point, NavalControlPoint) - if self.control_point.captured or is_navy: - super().open_new_package_dialog() - return - self.on_click() diff --git a/qt_ui/widgets/map/QMapGroundObject.py b/qt_ui/widgets/map/QMapGroundObject.py deleted file mode 100644 index a93a566c..00000000 --- a/qt_ui/widgets/map/QMapGroundObject.py +++ /dev/null @@ -1,167 +0,0 @@ -from typing import List, Optional - -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 game.db import REWARDS -from game.theater import ControlPoint, TheaterGroundObject -from game.theater.theatergroundobject import ( - MissileSiteGroundObject, - CoastalSiteGroundObject, -) -from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu -from .QMapObject import QMapObject -from ...displayoptions import DisplayOptions - - -class QMapGroundObject(QMapObject): - def __init__( - self, - parent, - x: float, - y: float, - w: float, - h: float, - control_point: ControlPoint, - ground_object: TheaterGroundObject, - game: Game, - buildings: Optional[List[TheaterGroundObject]] = None, - ) -> None: - 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) - self.buildings = buildings if buildings is not None else [] - self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False) - self.ground_object_dialog: Optional[QGroundObjectMenu] = None - self.setToolTip(self.tooltip) - - @property - def tooltip(self) -> str: - lines = [ - f"[{self.ground_object.obj_name}]", - f"${self.production_per_turn} per turn", - ] - if self.ground_object.groups: - units = {} - 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 - - for unit in units.keys(): - lines.append(f"{unit} x {units[unit]}") - else: - for building in self.buildings: - if not building.is_dead: - lines.append(f"{building.dcs_identifier}") - - return "\n".join(lines) - - @property - def production_per_turn(self) -> int: - production = 0 - for building in self.buildings: - if building.is_dead: - continue - if building.category in REWARDS.keys(): - production += REWARDS[building.category] - return production - - def paint(self, painter, option, widget=None) -> None: - player_icons = "_blue" - enemy_icons = "" - - if DisplayOptions.ground_objects: - painter.save() - - cat = self.ground_object.category - - rect = QRect( - option.rect.x() + 2, - option.rect.y(), - option.rect.width() - 2, - option.rect.height(), - ) - - is_dead = self.ground_object.is_dead - for building in self.buildings: - if not building.is_dead: - is_dead = False - break - - if cat == "aa": - has_threat = False - for group in self.ground_object.groups: - if self.ground_object.threat_range(group).distance_in_meters > 0: - has_threat = True - - if not is_dead and not self.control_point.captured: - if cat == "aa" and not has_threat: - painter.drawPixmap(rect, const.ICONS["nothreat" + enemy_icons]) - else: - painter.drawPixmap(rect, const.ICONS[cat + enemy_icons]) - elif not is_dead: - if cat == "aa" and not has_threat: - painter.drawPixmap(rect, const.ICONS["nothreat" + player_icons]) - else: - painter.drawPixmap(rect, const.ICONS[cat + player_icons]) - else: - painter.drawPixmap(rect, const.ICONS["destroyed"]) - - self.draw_health_gauge(painter, option) - painter.restore() - - def draw_health_gauge(self, painter, option) -> None: - units_alive = 0 - units_dead = 0 - - if len(self.ground_object.groups) == 0: - for building in self.buildings: - if building.dcs_identifier in FORTIFICATION_BUILDINGS: - continue - if building.is_dead: - units_dead += 1 - else: - units_alive += 1 - - for g in self.ground_object.groups: - units_alive += len(g.units) - if hasattr(g, "units_losts"): - units_dead += len(g.units_losts) - - 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 on_click(self) -> None: - self.ground_object_dialog = QGroundObjectMenu( - self.window(), - self.ground_object, - self.buildings, - 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 deleted file mode 100644 index f4b0bfb6..00000000 --- a/qt_ui/widgets/map/QMapObject.py +++ /dev/null @@ -1,84 +0,0 @@ -"""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 game.theater.missiontarget import MissionTarget - - -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, 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): - self.setCursor(Qt.PointingHandCursor) - - def mousePressEvent(self, event: QGraphicsSceneMouseEvent): - if event.button() == Qt.LeftButton: - self.on_click() - - def add_context_menu_actions(self, menu: QMenu) -> None: - pass - - 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) - - # Not all locations have valid objectives. Off-map spawns, for example, - # have no mission types. - if list(self.mission_target.mission_types(for_player=True)): - new_package_action = QAction(f"New package") - new_package_action.triggered.connect(self.open_new_package_dialog) - menu.addAction(new_package_action) - - self.add_context_menu_actions(menu) - - 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/ShippingLaneSegment.py b/qt_ui/widgets/map/ShippingLaneSegment.py deleted file mode 100644 index 02d445a4..00000000 --- a/qt_ui/widgets/map/ShippingLaneSegment.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import List, Optional - -from PySide2.QtCore import Qt -from PySide2.QtGui import QColor, QPen -from PySide2.QtWidgets import ( - QGraphicsItem, - QGraphicsLineItem, -) - -from game.theater import ControlPoint -from game.transfers import CargoShip -from qt_ui.uiconstants import COLORS - - -class ShippingLaneSegment(QGraphicsLineItem): - def __init__( - self, - x0: float, - y0: float, - x1: float, - y1: float, - control_point_a: ControlPoint, - control_point_b: ControlPoint, - ships: List[CargoShip], - parent: Optional[QGraphicsItem] = None, - ) -> None: - super().__init__(x0, y0, x1, y1, parent) - self.control_point_a = control_point_a - self.control_point_b = control_point_b - self.ships = ships - self.setPen(self.make_pen()) - self.setToolTip(self.make_tooltip()) - self.setAcceptHoverEvents(True) - - @property - def has_ships(self) -> bool: - return bool(self.ships) - - def make_tooltip(self) -> str: - if not self.has_ships: - return "No ships present in this shipping lane." - - ships = [] - for ship in self.ships: - units = "units" if ship.size > 1 else "unit" - ships.append( - f"{ship.size} {units} transferring from {ship.origin} to " - f"{ship.destination}." - ) - return "\n".join(ships) - - @property - def line_color(self) -> QColor: - if self.control_point_a.captured: - return COLORS["dark_blue"] - else: - return COLORS["dark_red"] - - @property - def line_style(self) -> Qt.PenStyle: - if self.has_ships: - return Qt.PenStyle.SolidLine - return Qt.PenStyle.DotLine - - def make_pen(self) -> QPen: - pen = QPen(brush=self.line_color) - pen.setColor(self.line_color) - pen.setStyle(self.line_style) - pen.setWidth(2) - return pen diff --git a/qt_ui/widgets/map/SupplyRouteSegment.py b/qt_ui/widgets/map/SupplyRouteSegment.py deleted file mode 100644 index 78401bc2..00000000 --- a/qt_ui/widgets/map/SupplyRouteSegment.py +++ /dev/null @@ -1,76 +0,0 @@ -from functools import cached_property -from typing import List, Optional - -from PySide2.QtCore import Qt -from PySide2.QtGui import QColor, QPen -from PySide2.QtWidgets import ( - QGraphicsItem, - QGraphicsLineItem, -) - -from game.theater import ControlPoint -from game.transfers import Convoy -from qt_ui.uiconstants import COLORS - - -class SupplyRouteSegment(QGraphicsLineItem): - def __init__( - self, - x0: float, - y0: float, - x1: float, - y1: float, - control_point_a: ControlPoint, - control_point_b: ControlPoint, - convoys: List[Convoy], - parent: Optional[QGraphicsItem] = None, - ) -> None: - super().__init__(x0, y0, x1, y1, parent) - self.control_point_a = control_point_a - self.control_point_b = control_point_b - self.convoys = convoys - self.setPen(self.make_pen()) - self.setToolTip(self.make_tooltip()) - self.setAcceptHoverEvents(True) - - @property - def has_convoys(self) -> bool: - return bool(self.convoys) - - def make_tooltip(self) -> str: - if not self.has_convoys: - return "No convoys present on this supply route." - - convoys = [] - for convoy in self.convoys: - units = "units" if convoy.size > 1 else "unit" - convoys.append( - f"{convoy.size} {units} transferring from {convoy.origin} to " - f"{convoy.destination}" - ) - return "\n".join(convoys) - - @property - def line_color(self) -> QColor: - if self.control_point_a.front_is_active(self.control_point_b): - return COLORS["red"] - elif self.control_point_a.captured: - return COLORS["dark_blue"] - else: - return COLORS["dark_red"] - - @property - def line_style(self) -> Qt.PenStyle: - if ( - self.control_point_a.front_is_active(self.control_point_b) - or self.has_convoys - ): - return Qt.PenStyle.SolidLine - return Qt.PenStyle.DotLine - - def make_pen(self) -> QPen: - pen = QPen(brush=self.line_color) - pen.setColor(self.line_color) - pen.setStyle(self.line_style) - pen.setWidth(6) - return pen diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 536e78bf..6ae67fdf 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -36,6 +36,8 @@ from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu LeafletLatLon = list[float] LeafletPoly = list[LeafletLatLon] +MAX_SHIP_DISTANCE = nautical_miles(80) + # **EVERY PROPERTY NEEDS A NOTIFY SIGNAL** # # https://bugreports.qt.io/browse/PYSIDE-1426 diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index bfad8870..23c82718 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -22,12 +22,11 @@ from game import Game, VERSION, persistency from game.debriefing import Debriefing from qt_ui import liberation_install from qt_ui.dialogs import Dialog -from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule 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 LeafletMap, QLiberationMap, LiberationMap +from qt_ui.widgets.map.QLiberationMap import QLiberationMap from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.QDebriefingWindow import QDebriefingWindow from qt_ui.windows.infos.QInfoPanel import QInfoPanel @@ -38,7 +37,7 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import ( class QLiberationWindow(QMainWindow): - def __init__(self, game: Optional[Game], new_map: bool) -> None: + def __init__(self, game: Optional[Game]) -> None: super(QLiberationWindow, self).__init__() self.game = game @@ -46,7 +45,7 @@ class QLiberationWindow(QMainWindow): Dialog.set_game(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.info_panel = QInfoPanel(self.game) - self.liberation_map: LiberationMap = self.create_map(new_map) + self.liberation_map = QLiberationMap(self.game_model, self) self.setGeometry(300, 100, 270, 100) self.setWindowTitle(f"DCS Liberation - v{VERSION}") @@ -174,30 +173,6 @@ class QLiberationWindow(QMainWindow): file_menu.addSeparator() file_menu.addAction("E&xit", self.close) - displayMenu = self.menu.addMenu("&Display") - - last_was_group = False - for item in DisplayOptions.menu_items(): - if isinstance(item, DisplayRule): - if last_was_group: - displayMenu.addSeparator() - self.display_bar.addSeparator() - action = self.make_display_rule_action(item) - displayMenu.addAction(action) - if action.icon(): - self.display_bar.addAction(action) - last_was_group = False - elif isinstance(item, DisplayGroup): - displayMenu.addSeparator() - self.display_bar.addSeparator() - group = QActionGroup(displayMenu) - for display_rule in item: - action = self.make_display_rule_action(display_rule, group) - displayMenu.addAction(action) - if action.icon(): - self.display_bar.addAction(action) - last_was_group = True - help_menu = self.menu.addMenu("&Help") help_menu.addAction(self.openDiscordAction) help_menu.addAction(self.openGithubAction) @@ -284,11 +259,6 @@ class QLiberationWindow(QMainWindow): self.game = game GameUpdateSignal.get_instance().game_loaded.emit(self.game) - def create_map(self, new_map: bool) -> LiberationMap: - if new_map: - return LeafletMap(self.game_model, self) - return QLiberationMap(self.game_model) - def setGame(self, game: Optional[Game]): try: self.game = game