Initial squadrons implementation.

Doesn't actually do anything yet, but squadrons are created for each
aircraft type and pilots will be created as needed to fill flights.

https://github.com/dcs-liberation/dcs_liberation/issues/276
This commit is contained in:
Dan Albert
2021-05-25 00:12:14 -07:00
parent 6b30f47588
commit 4147d2f684
22 changed files with 728 additions and 41 deletions

View File

@@ -14,6 +14,7 @@ from PySide2.QtGui import QIcon
from game import db
from game.game import Game
from game.squadrons import Squadron, Pilot
from game.theater.missiontarget import MissionTarget
from game.transfers import TransferOrder
from gen.ato import AirTaskingOrder, Package
@@ -366,6 +367,87 @@ class TransferModel(QAbstractListModel):
return self.game_model.game.transfers.transfer_at_index(index.row())
class AirWingModel(QAbstractListModel):
"""The model for an air wing."""
SquadronRole = Qt.UserRole
def __init__(self, game_model: GameModel, player: bool) -> None:
super().__init__()
self.game_model = game_model
self.player = player
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return self.game_model.game.air_wing_for(self.player).size
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
squadron = self.squadron_at_index(index)
if role == Qt.DisplayRole:
return self.text_for_squadron(squadron)
if role == Qt.DecorationRole:
return self.icon_for_squadron(squadron)
elif role == AirWingModel.SquadronRole:
return squadron
return None
@staticmethod
def text_for_squadron(squadron: Squadron) -> str:
"""Returns the text that should be displayed for the squadron."""
return squadron.name
@staticmethod
def icon_for_squadron(_squadron: Squadron) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the squadron."""
return None
def squadron_at_index(self, index: QModelIndex) -> Squadron:
"""Returns the squadron located at the given index."""
return self.game_model.game.air_wing_for(self.player).squadron_at_index(
index.row()
)
class SquadronModel(QAbstractListModel):
"""The model for a squadron."""
PilotRole = Qt.UserRole
def __init__(self, squadron: Squadron) -> None:
super().__init__()
self.squadron = squadron
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return self.squadron.size
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
pilot = self.pilot_at_index(index)
if role == Qt.DisplayRole:
return self.text_for_pilot(pilot)
if role == Qt.DecorationRole:
return self.icon_for_pilot(pilot)
elif role == SquadronModel.PilotRole:
return pilot
return None
@staticmethod
def text_for_pilot(pilot: Pilot) -> str:
"""Returns the text that should be displayed for the pilot."""
return pilot.name
@staticmethod
def icon_for_pilot(_pilot: Pilot) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the pilot."""
return None
def pilot_at_index(self, index: QModelIndex) -> Pilot:
"""Returns the pilot located at the given index."""
return self.squadron.pilot_at_index(index.row())
class GameModel:
"""A model for the Game object.
@@ -376,6 +458,7 @@ class GameModel:
def __init__(self, game: Optional[Game]) -> None:
self.game: Optional[Game] = game
self.transfer_model = TransferModel(self)
self.blue_air_wing_model = AirWingModel(self, player=True)
if self.game is None:
self.ato_model = AtoModel(self, AirTaskingOrder())
self.red_ato_model = AtoModel(self, AirTaskingOrder())

View File

@@ -21,6 +21,7 @@ from qt_ui.widgets.QConditionsWidget import QConditionsWidget
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QIntelBox import QIntelBox
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
@@ -63,6 +64,11 @@ class QTopPanel(QFrame):
self.factionsInfos = QFactionsInfos(self.game)
self.air_wing = QPushButton("Air Wing")
self.air_wing.setDisabled(True)
self.air_wing.setProperty("style", "btn-primary")
self.air_wing.clicked.connect(self.open_air_wing)
self.transfers = QPushButton("Transfers")
self.transfers.setDisabled(True)
self.transfers.setProperty("style", "btn-primary")
@@ -84,6 +90,7 @@ class QTopPanel(QFrame):
self.buttonBox = QGroupBox("Misc")
self.buttonBoxLayout = QHBoxLayout()
self.buttonBoxLayout.addWidget(self.air_wing)
self.buttonBoxLayout.addWidget(self.transfers)
self.buttonBoxLayout.addWidget(self.settings)
self.buttonBoxLayout.addWidget(self.statistics)
@@ -114,6 +121,7 @@ class QTopPanel(QFrame):
if game is None:
return
self.air_wing.setEnabled(True)
self.transfers.setEnabled(True)
self.settings.setEnabled(True)
self.statistics.setEnabled(True)
@@ -130,6 +138,10 @@ class QTopPanel(QFrame):
else:
self.proceedButton.setEnabled(True)
def open_air_wing(self):
self.dialog = AirWingDialog(self.game_model, self.window())
self.dialog.show()
def open_transfers(self):
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()

View File

@@ -562,10 +562,17 @@ class QLiberationMap(QGraphicsView, LiberationMap):
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.player_country if player else self.game.enemy_country,
F_16C_50,
self.game.country_for(player),
squadron,
2,
task,
start_type="Warm",

View File

@@ -0,0 +1,154 @@
from typing import Optional
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
QSize,
Qt,
)
from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter
from PySide2.QtWidgets import (
QAbstractItemView,
QDialog,
QListView,
QStyle,
QStyleOptionViewItem,
QStyledItemDelegate,
QVBoxLayout,
)
from game.squadrons import Squadron
from qt_ui.delegate_helpers import painter_context
from qt_ui.models import GameModel, AirWingModel, SquadronModel
from qt_ui.windows.SquadronDialog import SquadronDialog
class SquadronDelegate(QStyledItemDelegate):
FONT_SIZE = 10
HMARGIN = 4
VMARGIN = 4
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod
def squadron(index: QModelIndex) -> Squadron:
return index.data(AirWingModel.SquadronRole)
def first_row_text(self, index: QModelIndex) -> str:
return self.air_wing_model.data(index, Qt.DisplayRole)
def second_row_text(self, index: QModelIndex) -> str:
return self.squadron(index).nickname
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))
@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 SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.dialog: Optional[SquadronDialog] = None
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
self.setModel(self.air_wing_model)
self.selectionModel().setCurrentIndex(
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
)
self.dialog.show()
class AirWingDialog(QDialog):
"""Dialog window showing the player's air wing."""
def __init__(self, game_model: GameModel, parent) -> None:
super().__init__(parent)
self.air_wing_model = game_model.blue_air_wing_model
self.setMinimumSize(1000, 440)
self.setWindowTitle(f"Air Wing")
# TODO: self.setWindowIcon()
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(SquadronList(self.air_wing_model))

View File

@@ -190,7 +190,7 @@ class PendingTransfersDialog(QDialog):
def can_cancel(self, index: QModelIndex) -> bool:
if not index.isValid():
return False
return self.transfer_model.transfer_at_index(index).player
return self.transfer_model.pilot_at_index(index).player
def on_selection_changed(
self, selected: QItemSelection, _deselected: QItemSelection

View File

@@ -0,0 +1,142 @@
from typing import Optional
from PySide2.QtCore import (
QItemSelectionModel,
QModelIndex,
QSize,
Qt,
)
from PySide2.QtGui import QFont, QFontMetrics, QIcon, QPainter
from PySide2.QtWidgets import (
QAbstractItemView,
QDialog,
QListView,
QStyle,
QStyleOptionViewItem,
QStyledItemDelegate,
QVBoxLayout,
)
from game.squadrons import Pilot
from qt_ui.delegate_helpers import painter_context
from qt_ui.models import SquadronModel
class PilotDelegate(QStyledItemDelegate):
FONT_SIZE = 10
HMARGIN = 4
VMARGIN = 4
def __init__(self, squadron_model: SquadronModel) -> None:
super().__init__()
self.squadron_model = squadron_model
def get_font(self, option: QStyleOptionViewItem) -> QFont:
font = QFont(option.font)
font.setPointSize(self.FONT_SIZE)
return font
@staticmethod
def pilot(index: QModelIndex) -> Pilot:
return index.data(SquadronModel.PilotRole)
def first_row_text(self, index: QModelIndex) -> str:
return self.squadron_model.data(index, Qt.DisplayRole)
def second_row_text(self, index: QModelIndex) -> str:
return "Alive" if self.pilot(index).alive else "Dead"
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))
@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 PilotList(QListView):
"""List view for displaying a squadron's pilots."""
def __init__(self, squadron_model: SquadronModel) -> None:
super().__init__()
self.squadron_model = squadron_model
self.setItemDelegate(PilotDelegate(self.squadron_model))
self.setModel(self.squadron_model)
self.selectionModel().setCurrentIndex(
self.squadron_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
class SquadronDialog(QDialog):
"""Dialog window showing a squadron."""
def __init__(self, squadron_model: SquadronModel, parent) -> None:
super().__init__(parent)
self.setMinimumSize(1000, 440)
self.setWindowTitle(squadron_model.squadron.name)
# TODO: self.setWindowIcon()
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(PilotList(squadron_model))

View File

@@ -175,7 +175,7 @@ class QFlightCreator(QDialog):
flight = Flight(
self.package,
self.country,
aircraft,
self.game.air_wing_for(player=True).squadron_for(aircraft),
size,
task,
self.start_type.currentText(),

View File

@@ -1,17 +1,75 @@
import logging
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout
from PySide2.QtCore import Signal, QModelIndex
from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout, QComboBox
from game import Game
from gen.flights.flight import Flight
from qt_ui.models import PackageModel
class PilotSelector(QComboBox):
available_pilots_changed = Signal()
def __init__(self, flight: Flight, idx: int) -> None:
super().__init__()
self.flight = flight
self.pilot_index = idx
self.rebuild(initial_build=True)
def _do_rebuild(self) -> None:
self.clear()
if self.pilot_index >= self.flight.count:
self.addItem("No aircraft", None)
self.setDisabled(True)
return
self.setEnabled(True)
self.addItem("Unassigned", None)
choices = list(self.flight.squadron.available_pilots)
current_pilot = self.flight.pilots[self.pilot_index]
if current_pilot is not None:
choices.append(current_pilot)
for pilot in sorted(choices, key=lambda p: p.name):
self.addItem(pilot.name, pilot)
if current_pilot is None:
self.setCurrentText("Unassigned")
return
self.setCurrentText(current_pilot.name)
self.currentIndexChanged.connect(self.replace_pilot)
def rebuild(self, initial_build: bool = False) -> None:
current_selection = self.currentData()
# The contents of the selector depend on the selection of the other selectors
# for the flight, so changing the selection of one causes each selector to
# rebuild. A rebuild causes a selection change, so if we don't block signals
# during a rebuild we'll never stop rebuilding. Block signals during the rebuild
# and emit signals if anything actually changes afterwards.
self.blockSignals(True)
try:
self._do_rebuild()
finally:
self.blockSignals(False)
new_selection = self.currentData()
if not initial_build and current_selection != new_selection:
self.currentIndexChanged.emit(self.currentIndex())
self.currentTextChanged.emit(self.currentText())
def replace_pilot(self, index: QModelIndex) -> None:
if self.itemText(index) == "No aircraft":
# The roster resize is handled separately, so we have no pilots to remove.
return
pilot = self.itemData(index)
if pilot == self.flight.pilots[self.pilot_index]:
return
self.flight.set_pilot(self.pilot_index, pilot)
self.available_pilots_changed.emit()
class QFlightSlotEditor(QGroupBox):
changed = Signal()
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
super().__init__("Slots")
self.package_model = package_model
@@ -49,33 +107,49 @@ class QFlightSlotEditor(QGroupBox):
layout.addWidget(self.client_count, 1, 0)
layout.addWidget(self.client_count_spinner, 1, 1)
layout.addWidget(QLabel("Squadron:"), 2, 0)
layout.addWidget(QLabel(self.flight.squadron.name), 2, 1)
layout.addWidget(QLabel("Assigned pilots:"), 3, 0)
self.pilot_selectors = []
for pilot_idx, row in enumerate(range(3, 7)):
selector = PilotSelector(self.flight, pilot_idx)
selector.available_pilots_changed.connect(self.reset_pilot_selectors)
self.pilot_selectors.append(selector)
layout.addWidget(selector, row, 1)
self.setLayout(layout)
def reset_pilot_selectors(self) -> None:
for selector in self.pilot_selectors:
selector.rebuild()
def _changed_aircraft_count(self):
self.game.aircraft_inventory.return_from_flight(self.flight)
old_count = self.flight.count
self.flight.count = int(self.aircraft_count_spinner.value())
new_count = int(self.aircraft_count_spinner.value())
try:
self.game.aircraft_inventory.claim_for_flight(self.flight)
except ValueError:
# The UI should have prevented this, but if we ran out of aircraft
# then roll back the inventory change.
difference = self.flight.count - old_count
difference = new_count - self.flight.count
available = self.inventory.available(self.flight.unit_type)
logging.error(
f"Could not add {difference} additional aircraft to "
f"{self.flight} because {self.flight.from_cp} has only "
f"{self.flight} because {self.flight.departure} has only "
f"{available} {self.flight.unit_type} remaining"
)
self.flight.count = old_count
self.game.aircraft_inventory.claim_for_flight(self.flight)
self.changed.emit()
return
self.flight.resize(new_count)
self._cap_client_count()
self.reset_pilot_selectors()
def _changed_client_count(self):
self.flight.client_count = int(self.client_count_spinner.value())
self._cap_client_count()
self.package_model.update_tot()
self.changed.emit()
def _cap_client_count(self):
if self.flight.client_count > self.flight.count:

View File

@@ -21,9 +21,6 @@ class QFlightWaypointList(QTableView):
header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
if len(self.flight.points) > 0:
self.selectedPoint = self.flight.points[0]
self.update_list()
self.selectionModel().setCurrentIndex(