Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop

 Conflicts:
	qt_ui/widgets/map/QLiberationMap.py
This commit is contained in:
Khopa 2020-10-10 15:46:23 +02:00
commit a0ff78a810
13 changed files with 377 additions and 115 deletions

View File

@ -58,7 +58,7 @@ class Dialog:
flight: Flight) -> None: flight: Flight) -> None:
"""Opens the dialog to edit the given flight.""" """Opens the dialog to edit the given flight."""
cls.edit_flight_dialog = QEditFlightDialog( cls.edit_flight_dialog = QEditFlightDialog(
cls.game_model.game, cls.game_model,
package_model.package, package_model.package,
flight flight
) )

64
qt_ui/displayoptions.py Normal file
View File

@ -0,0 +1,64 @@
"""Visibility options for the game map."""
from dataclasses import dataclass
from typing import Iterator, Optional, Union
@dataclass
class DisplayRule:
name: str
_value: bool
@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
QLiberationMap.instance.reload_scene()
QLiberationMap.instance.update()
def __bool__(self) -> bool:
return self.value
class DisplayGroup:
def __init__(self, name: Optional[str]) -> None:
self.name = name
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", True)
self.only_selected = DisplayRule("Show Selected Flight Path", False)
self.all = DisplayRule("Show All Flight Paths", False)
class DisplayOptions:
ground_objects = DisplayRule("Ground Objects", True)
control_points = DisplayRule("Control Points", True)
lines = DisplayRule("Lines", True)
events = DisplayRule("Events", True)
sam_ranges = DisplayRule("SAM Ranges", True)
flight_paths = FlightPathOptions()
@classmethod
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
# Python 3.6 enforces that __dict__ is order preserving by default.
for value in cls.__dict__.values():
if isinstance(value, DisplayRule):
yield value
elif isinstance(value, DisplayGroup):
yield value

View File

@ -95,6 +95,8 @@ class NullListModel(QAbstractListModel):
class PackageModel(QAbstractListModel): class PackageModel(QAbstractListModel):
"""The model for an ATO package.""" """The model for an ATO package."""
FlightRole = Qt.UserRole
#: Emitted when this package is being deleted from the ATO. #: Emitted when this package is being deleted from the ATO.
deleted = Signal() deleted = Signal()
@ -113,6 +115,8 @@ class PackageModel(QAbstractListModel):
return self.text_for_flight(flight) return self.text_for_flight(flight)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon_for_flight(flight) return self.icon_for_flight(flight)
elif role == PackageModel.FlightRole:
return flight
return None return None
@staticmethod @staticmethod
@ -185,6 +189,8 @@ class AtoModel(QAbstractListModel):
PackageRole = Qt.UserRole PackageRole = Qt.UserRole
client_slots_changed = Signal()
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None: def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
super().__init__() super().__init__()
self.game = game self.game = game

View File

@ -1,4 +1,6 @@
"""A layout containing a widget with an associated label.""" """A layout containing a widget with an associated label."""
from typing import Optional
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
@ -10,8 +12,13 @@ class QLabeledWidget(QHBoxLayout):
label is used to name the input. label is used to name the input.
""" """
def __init__(self, text: str, widget: QWidget) -> None: def __init__(self, text: str, widget: QWidget,
tooltip: Optional[str]) -> None:
super().__init__() super().__init__()
self.addWidget(QLabel(text)) label = QLabel(text)
self.addWidget(label)
self.addStretch() self.addStretch()
self.addWidget(widget, alignment=Qt.AlignRight) self.addWidget(widget, alignment=Qt.AlignRight)
if tooltip is not None:
label.setToolTip(tooltip)
widget.setToolTip(tooltip)

View File

@ -11,9 +11,11 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game from game import Game
from game.event import CAP, CAS, FrontlineAttackEvent from game.event import CAP, CAS, FrontlineAttackEvent
from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QTurnCounter import QTurnCounter from qt_ui.widgets.QTurnCounter import QTurnCounter
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QWaitingForMissionResultWindow import \ from qt_ui.windows.QWaitingForMissionResultWindow import \
QWaitingForMissionResultWindow QWaitingForMissionResultWindow
@ -23,14 +25,18 @@ from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QTopPanel(QFrame): class QTopPanel(QFrame):
def __init__(self, game: Game): def __init__(self, game_model: GameModel):
super(QTopPanel, self).__init__() super(QTopPanel, self).__init__()
self.game = game self.game_model = game_model
self.setMaximumHeight(70) self.setMaximumHeight(70)
self.init_ui() self.init_ui()
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame) GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update) GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
@property
def game(self) -> Optional[Game]:
return self.game_model.game
def init_ui(self): def init_ui(self):
self.turnCounter = QTurnCounter() self.turnCounter = QTurnCounter()
@ -68,6 +74,8 @@ class QTopPanel(QFrame):
self.proceedBox = QGroupBox("Proceed") self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout() self.proceedBoxLayout = QHBoxLayout()
self.proceedBoxLayout.addLayout(
MaxPlayerCount(self.game_model.ato_model))
self.proceedBoxLayout.addWidget(self.passTurnButton) self.proceedBoxLayout.addWidget(self.passTurnButton)
self.proceedBoxLayout.addWidget(self.proceedButton) self.proceedBoxLayout.addWidget(self.proceedButton)
self.proceedBox.setLayout(self.proceedBoxLayout) self.proceedBox.setLayout(self.proceedBoxLayout)
@ -84,16 +92,17 @@ class QTopPanel(QFrame):
self.setLayout(self.layout) self.setLayout(self.layout)
def setGame(self, game: Optional[Game]): def setGame(self, game: Optional[Game]):
self.game = game if game is None:
if game is not None: return
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
self.budgetBox.setGame(self.game)
self.factionsInfos.setGame(self.game)
if self.game and self.game.turn == 0: self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
self.proceedButton.setEnabled(False) self.budgetBox.setGame(self.game)
else: self.factionsInfos.setGame(self.game)
self.proceedButton.setEnabled(True)
if self.game and self.game.turn == 0:
self.proceedButton.setEnabled(False)
else:
self.proceedButton.setEnabled(True)
def openSettings(self): def openSettings(self):
self.subwindow = QSettingsWindow(self.game) self.subwindow = QSettingsWindow(self.game)

View File

@ -10,7 +10,7 @@ from PySide2.QtCore import (
QSize, QSize,
Qt, Qt,
) )
from PySide2.QtGui import QFont, QFontMetrics, QPainter from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QGroupBox, QGroupBox,
@ -18,15 +18,109 @@ from PySide2.QtWidgets import (
QListView, QListView,
QPushButton, QPushButton,
QSplitter, QSplitter,
QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout, QStyle, QStyleOptionViewItem, QStyledItemDelegate, QVBoxLayout,
) )
from game import db
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from ..models import AtoModel, GameModel, NullListModel, PackageModel from ..models import AtoModel, GameModel, NullListModel, PackageModel
class FlightDelegate(QStyledItemDelegate):
FONT_SIZE = 10
HMARGIN = 4
VMARGIN = 4
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod
def flight(index: QModelIndex) -> Flight:
return index.data(PackageModel.FlightRole)
def first_row_text(self, index: QModelIndex) -> str:
flight = self.flight(index)
task = flight.flight_type.name
count = flight.count
name = db.unit_type_name(flight.unit_type)
delay = flight.scheduled_in
return f"[{task}] {count} x {name} in {delay} minutes"
def second_row_text(self, index: QModelIndex) -> str:
flight = self.flight(index)
origin = flight.from_cp.name
return f"From {origin}"
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
index: QModelIndex) -> None:
# Draw the list item with all the default selection styling, but with an
# invalid index so text formatting is left to us.
super().paint(painter, option, QModelIndex())
rect = option.rect.adjusted(self.HMARGIN, self.VMARGIN, -self.HMARGIN,
-self.VMARGIN)
with painter_context(painter):
painter.setFont(self.get_font(option))
icon: Optional[QIcon] = index.data(Qt.DecorationRole)
if icon is not None:
icon.paint(painter, rect, Qt.AlignLeft | Qt.AlignVCenter,
self.icon_mode(option),
self.icon_state(option))
rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN,
0, 0, 0)
painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index))
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index))
clients = self.num_clients(index)
if clients:
painter.drawText(rect, Qt.AlignRight,
f"Player Slots: {clients}")
def num_clients(self, index: QModelIndex) -> int:
flight = self.flight(index)
return flight.client_count
@staticmethod
def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
if not (option.state & QStyle.State_Enabled):
return QIcon.Disabled
elif option.state & QStyle.State_Selected:
return QIcon.Selected
elif option.state & QStyle.State_Active:
return QIcon.Active
return QIcon.Normal
@staticmethod
def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
@staticmethod
def icon_size(option: QStyleOptionViewItem) -> QSize:
icon_size: Optional[QSize] = option.decorationSize
if icon_size is None:
return QSize(0, 0)
else:
return icon_size
def sizeHint(self, option: QStyleOptionViewItem,
index: QModelIndex) -> QSize:
left = self.icon_size(option).width() + self.HMARGIN
metrics = QFontMetrics(self.get_font(option))
first = metrics.size(0, self.first_row_text(index))
second = metrics.size(0, self.second_row_text(index))
text_width = max(first.width(), second.width())
return QSize(left + text_width + 2 * self.HMARGIN,
first.height() + second.height() + 2 * self.VMARGIN)
class QFlightList(QListView): class QFlightList(QListView):
"""List view for displaying the flights of a package.""" """List view for displaying the flights of a package."""
@ -34,6 +128,7 @@ class QFlightList(QListView):
super().__init__() super().__init__()
self.package_model = model self.package_model = model
self.set_package(model) self.set_package(model)
self.setItemDelegate(FlightDelegate())
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
@ -109,6 +204,7 @@ class QFlightPanel(QGroupBox):
"""Sets the package model to display.""" """Sets the package model to display."""
self.package_model = model self.package_model = model
self.flight_list.set_package(model) self.flight_list.set_package(model)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed() self.on_selection_changed()
@property @property
@ -122,6 +218,15 @@ class QFlightPanel(QGroupBox):
enabled = index.isValid() enabled = index.isValid()
self.edit_button.setEnabled(enabled) self.edit_button.setEnabled(enabled)
self.delete_button.setEnabled(enabled) self.delete_button.setEnabled(enabled)
self.change_map_flight_selection(index)
@staticmethod
def change_map_flight_selection(index: QModelIndex) -> None:
if not index.isValid():
GameUpdateSignal.get_instance().select_flight(None)
return
GameUpdateSignal.get_instance().select_flight(index.row())
def on_edit(self) -> None: def on_edit(self) -> None:
"""Opens the flight edit dialog.""" """Opens the flight edit dialog."""
@ -196,6 +301,15 @@ class PackageDelegate(QStyledItemDelegate):
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2) line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
painter.drawText(line2, Qt.AlignLeft, self.right_text(index)) painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
clients = self.num_clients(index)
if clients:
painter.drawText(rect, Qt.AlignRight,
f"Player Slots: {clients}")
def num_clients(self, index: QModelIndex) -> int:
package = self.package(index)
return sum(f.client_count for f in package.flights)
def sizeHint(self, option: QStyleOptionViewItem, def sizeHint(self, option: QStyleOptionViewItem,
index: QModelIndex) -> QSize: index: QModelIndex) -> QSize:
metrics = QFontMetrics(self.get_font(option)) metrics = QFontMetrics(self.get_font(option))
@ -270,6 +384,18 @@ class QPackagePanel(QGroupBox):
enabled = index.isValid() enabled = index.isValid()
self.edit_button.setEnabled(enabled) self.edit_button.setEnabled(enabled)
self.delete_button.setEnabled(enabled) self.delete_button.setEnabled(enabled)
self.change_map_package_selection(index)
def change_map_package_selection(self, index: QModelIndex) -> None:
if not index.isValid():
GameUpdateSignal.get_instance().select_package(None)
return
package = self.ato_model.get_package_model(index)
if package.rowCount() == 0:
GameUpdateSignal.get_instance().select_package(None)
else:
GameUpdateSignal.get_instance().select_package(index.row())
def on_edit(self) -> None: def on_edit(self) -> None:
"""Opens the package edit dialog.""" """Opens the package edit dialog."""

View File

@ -0,0 +1,28 @@
"""Widgets for displaying client slots."""
from PySide2.QtWidgets import QLabel
from qt_ui.models import AtoModel
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
class MaxPlayerCount(QLabeledWidget):
def __init__(self, ato_model: AtoModel) -> None:
self.ato_model = ato_model
self.slots_label = QLabel(str(self.count_client_slots))
self.ato_model.client_slots_changed.connect(self.update_count)
super().__init__(
"Max Players:", self.slots_label,
("Total number of client slots. To add client slots, edit a flight "
"using the panel on the left.")
)
@property
def count_client_slots(self) -> int:
slots = 0
for package in self.ato_model.packages:
for flight in package.flights:
slots += flight.client_count
return slots
def update_count(self) -> None:
self.slots_label.setText(str(self.count_client_slots))

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import logging import logging
from typing import Dict, List, Optional, Tuple from typing import List, Optional, Tuple
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent from PySide2.QtGui import QBrush, QColor, QPen, QPixmap, QWheelEvent
@ -19,11 +21,12 @@ from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR from game.data.radar_db import UNITS_WITH_RADAR
from gen import Conflict from gen import Conflict
from gen.flights.flight import Flight from gen.flights.flight import Flight
from qt_ui.displayoptions import DisplayOptions
from qt_ui.models import GameModel 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.QLiberationScene import QLiberationScene
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.widgets.map.QFrontLine import QFrontLine
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from theater import ControlPoint, FrontLine from theater import ControlPoint, FrontLine
@ -31,15 +34,7 @@ from theater import ControlPoint, FrontLine
class QLiberationMap(QGraphicsView): class QLiberationMap(QGraphicsView):
WAYPOINT_SIZE = 4 WAYPOINT_SIZE = 4
instance = None instance: Optional[QLiberationMap] = None
display_rules: Dict[str, bool] = {
"cp": True,
"go": True,
"lines": True,
"events": True,
"sam": True,
"flight_paths": False
}
def __init__(self, game_model: GameModel): def __init__(self, game_model: GameModel):
super(QLiberationMap, self).__init__() super(QLiberationMap, self).__init__()
@ -48,6 +43,8 @@ class QLiberationMap(QGraphicsView):
self.game: Optional[Game] = game_model.game self.game: Optional[Game] = game_model.game
self.flight_path_items: List[QGraphicsItem] = [] 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.setMinimumSize(800,600)
self.setMaximumHeight(2160) self.setMaximumHeight(2160)
@ -62,6 +59,25 @@ class QLiberationMap(QGraphicsView):
lambda: self.draw_flight_plans(self.scene()) lambda: self.draw_flight_plans(self.scene())
) )
def update_package_selection(index: Optional[int]) -> None:
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: Optional[int]) -> None:
if self.selected_flight is None:
logging.error("Flight was selected with no package selected")
return
self.selected_flight = self.selected_flight[0], index
self.draw_flight_plans(self.scene())
GameUpdateSignal.get_instance().flight_selection_changed.connect(
update_flight_selection
)
def init_scene(self): def init_scene(self):
scene = QLiberationScene(self) scene = QLiberationScene(self)
self.setScene(scene) self.setScene(scene)
@ -162,7 +178,8 @@ class QLiberationMap(QGraphicsView):
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name) 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)) scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
if ground_object.category == "aa" and self.get_display_rule("sam"): is_aa = ground_object.category == "aa"
if is_aa and DisplayOptions.sam_ranges:
threat_range = 0 threat_range = 0
detection_range = 0 detection_range = 0
can_fire = False can_fire = False
@ -194,11 +211,11 @@ class QLiberationMap(QGraphicsView):
added_objects.append(ground_object.obj_name) added_objects.append(ground_object.obj_name)
for cp in self.game.theater.enemy_points(): for cp in self.game.theater.enemy_points():
if self.get_display_rule("lines"): if DisplayOptions.lines:
self.scene_create_lines_for_cp(cp, playerColor, enemyColor) self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
for cp in self.game.theater.player_points(): for cp in self.game.theater.player_points():
if self.get_display_rule("lines"): if DisplayOptions.lines:
self.scene_create_lines_for_cp(cp, playerColor, enemyColor) self.scene_create_lines_for_cp(cp, playerColor, enemyColor)
self.draw_flight_plans(scene) self.draw_flight_plans(scene)
@ -219,37 +236,44 @@ class QLiberationMap(QGraphicsView):
# Something may have caused those items to already be removed. # Something may have caused those items to already be removed.
pass pass
self.flight_path_items.clear() self.flight_path_items.clear()
if not self.get_display_rule("flight_paths"): if DisplayOptions.flight_paths.hide:
return return
for package in self.game_model.ato_model.packages: for p_idx, package in enumerate(self.game_model.ato_model.packages):
for flight in package.flights: for f_idx, flight in enumerate(package.flights):
self.draw_flight_plan(scene, flight) selected = (p_idx, f_idx) == self.selected_flight
if DisplayOptions.flight_paths.only_selected and not selected:
continue
highlight = selected and DisplayOptions.flight_paths.all
self.draw_flight_plan(scene, flight, highlight)
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight) -> None: def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
highlight: bool) -> None:
is_player = flight.from_cp.captured is_player = flight.from_cp.captured
pos = self._transform_point(flight.from_cp.position) pos = self._transform_point(flight.from_cp.position)
self.draw_waypoint(scene, pos, is_player) self.draw_waypoint(scene, pos, is_player, highlight)
prev_pos = tuple(pos) prev_pos = tuple(pos)
for point in flight.points: for point in flight.points:
new_pos = self._transform_point(Point(point.x, point.y)) new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player) self.draw_flight_path(scene, prev_pos, new_pos, is_player,
self.draw_waypoint(scene, new_pos, is_player) highlight)
self.draw_waypoint(scene, new_pos, is_player, highlight)
prev_pos = tuple(new_pos) prev_pos = tuple(new_pos)
self.draw_flight_path(scene, prev_pos, pos, is_player) self.draw_flight_path(scene, prev_pos, pos, is_player, highlight)
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int], def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
player: bool) -> None: player: bool, highlight: bool) -> None:
waypoint_pen = self.waypoint_pen(player) waypoint_pen = self.waypoint_pen(player, highlight)
waypoint_brush = self.waypoint_brush(player) waypoint_brush = self.waypoint_brush(player, highlight)
self.flight_path_items.append(scene.addEllipse( self.flight_path_items.append(scene.addEllipse(
position[0], position[1], self.WAYPOINT_SIZE, position[0], position[1], self.WAYPOINT_SIZE,
self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush self.WAYPOINT_SIZE, waypoint_pen, waypoint_brush
)) ))
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int], def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
pos1: Tuple[int, int], player: bool): pos1: Tuple[int, int], player: bool,
flight_path_pen = self.flight_path_pen(player) highlight: bool) -> None:
flight_path_pen = self.flight_path_pen(player, highlight)
# Draw the line to the *middle* of the waypoint. # Draw the line to the *middle* of the waypoint.
offset = self.WAYPOINT_SIZE // 2 offset = self.WAYPOINT_SIZE // 2
self.flight_path_items.append(scene.addLine( self.flight_path_items.append(scene.addLine(
@ -338,17 +362,24 @@ class QLiberationMap(QGraphicsView):
return X > treshold and X or treshold, Y > treshold and Y or treshold return X > treshold and X or treshold, Y > treshold and Y or treshold
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: def base_faction_color_name(self, player: bool) -> str:
if player: if player:
return self.game.get_player_color() return self.game.get_player_color()
else: else:
return self.game.get_enemy_color() return self.game.get_enemy_color()
def waypoint_pen(self, player: bool) -> QPen: def waypoint_pen(self, player: bool, highlight: bool) -> QColor:
if highlight:
return self.highlight_color()
name = self.base_faction_color_name(player) name = self.base_faction_color_name(player)
return QPen(brush=CONST.COLORS[name]) return CONST.COLORS[name]
def waypoint_brush(self, player: bool) -> QColor: def waypoint_brush(self, player: bool, highlight: bool) -> QColor:
if highlight:
return self.highlight_color(transparent=True)
name = self.base_faction_color_name(player) name = self.base_faction_color_name(player)
return CONST.COLORS[f"{name}_transparent"] return CONST.COLORS[f"{name}_transparent"]
@ -369,7 +400,10 @@ class QLiberationMap(QGraphicsView):
qpen.setStyle(Qt.DotLine) qpen.setStyle(Qt.DotLine)
return qpen return qpen
def flight_path_pen(self, player: bool) -> QPen: def flight_path_pen(self, player: bool, highlight: bool) -> QPen:
if highlight:
return self.highlight_color()
name = self.base_faction_color_name(player) name = self.base_faction_color_name(player)
color = CONST.COLORS[name] color = CONST.COLORS[name]
pen = QPen(brush=color) pen = QPen(brush=color)
@ -401,18 +435,3 @@ class QLiberationMap(QGraphicsView):
effect = QGraphicsOpacityEffect() effect = QGraphicsOpacityEffect()
effect.setOpacity(0.3) effect.setOpacity(0.3)
overlay.setGraphicsEffect(effect) overlay.setGraphicsEffect(effect)
@staticmethod
def set_display_rule(rule: str, value: bool):
QLiberationMap.display_rules[rule] = value
QLiberationMap.instance.reload_scene()
QLiberationMap.instance.update()
@staticmethod
def get_display_rules() -> Dict[str, bool]:
return QLiberationMap.display_rules
@staticmethod
def get_display_rule(rule) -> bool:
return QLiberationMap.display_rules[rule]

View File

@ -7,6 +7,7 @@ from qt_ui.models import GameModel
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from theater import ControlPoint from theater import ControlPoint
from .QMapObject import QMapObject from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
class QMapControlPoint(QMapObject): class QMapControlPoint(QMapObject):
@ -21,7 +22,7 @@ class QMapControlPoint(QMapObject):
self.base_details_dialog: Optional[QBaseMenu2] = None self.base_details_dialog: Optional[QBaseMenu2] = None
def paint(self, painter, option, widget=None) -> None: def paint(self, painter, option, widget=None) -> None:
if self.parent.get_display_rule("cp"): if DisplayOptions.control_points:
painter.save() painter.save()
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(self.brush_color) painter.setBrush(self.brush_color)

View File

@ -8,8 +8,9 @@ import qt_ui.uiconstants as const
from game import Game from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS from game.data.building_data import FORTIFICATION_BUILDINGS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import TheaterGroundObject, ControlPoint from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject from .QMapObject import QMapObject
from ...displayoptions import DisplayOptions
class QMapGroundObject(QMapObject): class QMapGroundObject(QMapObject):
@ -50,7 +51,7 @@ class QMapGroundObject(QMapObject):
player_icons = "_blue" player_icons = "_blue"
enemy_icons = "" enemy_icons = ""
if self.parent.get_display_rule("go"): if DisplayOptions.ground_objects:
painter.save() painter.save()
cat = self.ground_object.category cat = self.ground_object.category

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional, Tuple
from PySide2.QtCore import QObject, Signal from PySide2.QtCore import QObject, Signal
@ -24,11 +24,21 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(DebriefingSignal) debriefingReceived = Signal(DebriefingSignal)
flight_paths_changed = Signal() flight_paths_changed = Signal()
package_selection_changed = Signal(int) # Optional[int]
flight_selection_changed = Signal(int) # Optional[int]
def __init__(self): def __init__(self):
super(GameUpdateSignal, self).__init__() super(GameUpdateSignal, self).__init__()
GameUpdateSignal.instance = self GameUpdateSignal.instance = self
def select_package(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.package_selection_changed.emit(index)
def select_flight(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.flight_selection_changed.emit(index)
def redraw_flight_paths(self) -> None: def redraw_flight_paths(self) -> None:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.flight_paths_changed.emit() self.flight_paths_changed.emit()

View File

@ -1,16 +1,16 @@
import logging import logging
import sys import sys
import webbrowser import webbrowser
from typing import Optional from typing import Optional, Union
from PySide2.QtCore import Qt from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon from PySide2.QtGui import QIcon
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAction, QAction,
QDesktopWidget, QActionGroup, QDesktopWidget,
QFileDialog, QFileDialog,
QMainWindow, QMainWindow,
QMessageBox, QMenu, QMessageBox,
QSplitter, QSplitter,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@ -19,6 +19,7 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game, persistency from game import Game, persistency
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel from qt_ui.widgets.QTopPanel import QTopPanel
@ -76,7 +77,7 @@ class QLiberationWindow(QMainWindow):
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.setMargin(0) vbox.setMargin(0)
vbox.addWidget(QTopPanel(self.game)) vbox.addWidget(QTopPanel(self.game_model))
vbox.addWidget(hbox) vbox.addWidget(hbox)
central_widget = QWidget() central_widget = QWidget()
@ -134,48 +135,23 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction) file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator() file_menu.addSeparator()
#file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
file_menu.addAction("E&xit" , lambda: self.exit()) file_menu.addAction("E&xit" , lambda: self.exit())
displayMenu = self.menu.addMenu("&Display") displayMenu = self.menu.addMenu("&Display")
tg_cp_visibility = QAction('&Control Point', displayMenu) last_was_group = True
tg_cp_visibility.setCheckable(True) for item in DisplayOptions.menu_items():
tg_cp_visibility.setChecked(True) if isinstance(item, DisplayRule):
tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked())) displayMenu.addAction(self.make_display_rule_action(item))
last_was_group = False
tg_go_visibility = QAction('&Ground Objects', displayMenu) elif isinstance(item, DisplayGroup):
tg_go_visibility.setCheckable(True) if not last_was_group:
tg_go_visibility.setChecked(True) displayMenu.addSeparator()
tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked())) group = QActionGroup(displayMenu)
for display_rule in item:
tg_line_visibility = QAction('&Lines', displayMenu) displayMenu.addAction(
tg_line_visibility.setCheckable(True) self.make_display_rule_action(display_rule, group))
tg_line_visibility.setChecked(True) last_was_group = True
tg_line_visibility.toggled.connect(
lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked()))
tg_event_visibility = QAction('&Events', displayMenu)
tg_event_visibility.setCheckable(True)
tg_event_visibility.setChecked(True)
tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked()))
tg_sam_visibility = QAction('&SAM Range', displayMenu)
tg_sam_visibility.setCheckable(True)
tg_sam_visibility.setChecked(True)
tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked()))
tg_flight_path_visibility = QAction('&Flight Paths', displayMenu)
tg_flight_path_visibility.setCheckable(True)
tg_flight_path_visibility.setChecked(False)
tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked()))
displayMenu.addAction(tg_go_visibility)
displayMenu.addAction(tg_cp_visibility)
displayMenu.addAction(tg_line_visibility)
displayMenu.addAction(tg_event_visibility)
displayMenu.addAction(tg_sam_visibility)
displayMenu.addAction(tg_flight_path_visibility)
help_menu = self.menu.addMenu("&Help") help_menu = self.menu.addMenu("&Help")
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ")) help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
@ -188,6 +164,21 @@ class QLiberationWindow(QMainWindow):
help_menu.addSeparator() help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction) help_menu.addAction(self.showAboutDialogAction)
@staticmethod
def make_display_rule_action(
display_rule, group: Optional[QActionGroup] = None) -> QAction:
def make_check_closure():
def closure():
display_rule.value = action.isChecked()
return closure
action = QAction(f"&{display_rule.menu_text}", group)
action.setCheckable(True)
action.setChecked(display_rule.value)
action.toggled.connect(make_check_closure())
return action
def newGame(self): def newGame(self):
wizard = NewGameWizard(self) wizard = NewGameWizard(self)
wizard.show() wizard.show()

View File

@ -4,9 +4,9 @@ from PySide2.QtWidgets import (
QVBoxLayout, QVBoxLayout,
) )
from game import Game
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
@ -15,22 +15,22 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
class QEditFlightDialog(QDialog): class QEditFlightDialog(QDialog):
"""Dialog window for editing flight plans and loadouts.""" """Dialog window for editing flight plans and loadouts."""
def __init__(self, game: Game, package: Package, flight: Flight) -> None: def __init__(self, game_model: GameModel, package: Package, flight: Flight) -> None:
super().__init__() super().__init__()
self.game = game self.game_model = game_model
self.setWindowTitle("Create flight") self.setWindowTitle("Create flight")
self.setWindowIcon(EVENT_ICONS["strike"]) self.setWindowIcon(EVENT_ICONS["strike"])
layout = QVBoxLayout() layout = QVBoxLayout()
self.flight_planner = QFlightPlanner(package, flight, game) self.flight_planner = QFlightPlanner(package, flight, game_model.game)
layout.addWidget(self.flight_planner) layout.addWidget(self.flight_planner)
self.setLayout(layout) self.setLayout(layout)
self.finished.connect(self.on_close) self.finished.connect(self.on_close)
@staticmethod def on_close(self, _result) -> None:
def on_close(_result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths() GameUpdateSignal.get_instance().redraw_flight_paths()
self.game_model.ato_model.client_slots_changed.emit()