Finish implementation of add/remove squadrons from AirWingConfigurationDialog

Users can now add and remove squadrons with specific buttons. this also allows new aircraft types to be added as well.

- rebased existing PR to develop
- reverted observable and changed to signals
- changed the general concept so that changes only affect the ui data model and not the game directly. Game will only be updated on apply
- removed unused code
- adopt to review comments
- allow user to choose a predefined squadron preset (also alow none value to use the random generator)
- Reuse the squadron defs from the default assigner in the AirWing class
- allow user to re-roll the squadron nickname (also added new ui icons for the button)
This commit is contained in:
RndName 2021-11-16 23:16:19 +01:00
parent e4ba9a8b72
commit d2f7785f9f
No known key found for this signature in database
GPG Key ID: 5EF516FD9537F7C0
14 changed files with 342 additions and 382 deletions

View File

@ -5,10 +5,8 @@ from typing import Optional, TYPE_CHECKING
from game.squadrons import Squadron from game.squadrons import Squadron
from game.squadrons.squadrondef import SquadronDef from game.squadrons.squadrondef import SquadronDef
from game.squadrons.squadrondefloader import SquadronDefLoader
from ..ato.flighttype import FlightType from ..ato.flighttype import FlightType
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
from .squadrondefgenerator import SquadronDefGenerator
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
from ..theater import ControlPoint from ..theater import ControlPoint
@ -25,14 +23,6 @@ class DefaultSquadronAssigner:
self.game = game self.game = game
self.coalition = coalition self.coalition = coalition
self.air_wing = coalition.air_wing self.air_wing = coalition.air_wing
self.squadron_defs = SquadronDefLoader(game, coalition).load()
self.squadron_def_generator = SquadronDefGenerator(self.coalition)
def claim_squadron_def(self, squadron: SquadronDef) -> None:
try:
self.squadron_defs[squadron.aircraft].remove(squadron)
except ValueError:
pass
def assign(self) -> None: def assign(self) -> None:
for control_point in self.game.theater.control_points_for( for control_point in self.game.theater.control_points_for(
@ -47,7 +37,6 @@ class DefaultSquadronAssigner:
) )
continue continue
self.claim_squadron_def(squadron_def)
squadron = Squadron.create_from( squadron = Squadron.create_from(
squadron_def, control_point, self.coalition, self.game squadron_def, control_point, self.coalition, self.game
) )
@ -74,7 +63,7 @@ class DefaultSquadronAssigner:
# If we can't find any squadron matching the requirement, we should # If we can't find any squadron matching the requirement, we should
# create one. # create one.
return self.squadron_def_generator.generate_for_task( return self.air_wing.squadron_def_generator.generate_for_task(
config.primary, control_point config.primary, control_point
) )
@ -105,7 +94,7 @@ class DefaultSquadronAssigner:
# No premade squadron available for this aircraft that meets the requirements, # No premade squadron available for this aircraft that meets the requirements,
# so generate one if possible. # so generate one if possible.
return self.squadron_def_generator.generate_for_aircraft(aircraft) return self.air_wing.squadron_def_generator.generate_for_aircraft(aircraft)
@staticmethod @staticmethod
def squadron_compatible_with( def squadron_compatible_with(
@ -121,18 +110,24 @@ class DefaultSquadronAssigner:
def find_squadron_for_airframe( def find_squadron_for_airframe(
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
for squadron in self.squadron_defs[aircraft]: for squadron in self.air_wing.squadron_defs[aircraft]:
if self.squadron_compatible_with(squadron, task, control_point): if not squadron.claimed and self.squadron_compatible_with(
squadron, task, control_point
):
return squadron return squadron
return None return None
def find_squadron_by_name( def find_squadron_by_name(
self, name: str, task: FlightType, control_point: ControlPoint self, name: str, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
for squadrons in self.squadron_defs.values(): for squadrons in self.air_wing.squadron_defs.values():
for squadron in squadrons: for squadron in squadrons:
if squadron.name == name and self.squadron_compatible_with( if (
squadron, task, control_point, ignore_base_preference=True not squadron.claimed
and squadron.name == name
and self.squadron_compatible_with(
squadron, task, control_point, ignore_base_preference=True
)
): ):
return squadron return squadron
return None return None
@ -140,8 +135,10 @@ class DefaultSquadronAssigner:
def find_squadron_for_task( def find_squadron_for_task(
self, task: FlightType, control_point: ControlPoint self, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
for squadrons in self.squadron_defs.values(): for squadrons in self.air_wing.squadron_defs.values():
for squadron in squadrons: for squadron in squadrons:
if self.squadron_compatible_with(squadron, task, control_point): if not squadron.claimed and self.squadron_compatible_with(
squadron, task, control_point
):
return squadron return squadron
return None return None

View File

@ -12,12 +12,12 @@ from gen.flights.ai_flight_planner_db import aircraft_for_task, tasks_for_aircra
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
if TYPE_CHECKING: if TYPE_CHECKING:
from game.coalition import Coalition from game.factions.faction import Faction
class SquadronDefGenerator: class SquadronDefGenerator:
def __init__(self, coalition: Coalition) -> None: def __init__(self, faction: Faction) -> None:
self.coalition = coalition self.faction = faction
self.count = itertools.count(1) self.count = itertools.count(1)
self.used_nicknames: set[str] = set() self.used_nicknames: set[str] = set()
@ -26,7 +26,7 @@ class SquadronDefGenerator:
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
aircraft_choice: Optional[AircraftType] = None aircraft_choice: Optional[AircraftType] = None
for aircraft in aircraft_for_task(task): for aircraft in aircraft_for_task(task):
if aircraft not in self.coalition.faction.aircrafts: if aircraft not in self.faction.aircrafts:
continue continue
if not control_point.can_operate(aircraft): if not control_point.can_operate(aircraft):
continue continue
@ -44,7 +44,7 @@ class SquadronDefGenerator:
return SquadronDef( return SquadronDef(
name=f"Squadron {next(self.count):03}", name=f"Squadron {next(self.count):03}",
nickname=self.random_nickname(), nickname=self.random_nickname(),
country=self.coalition.country_name, country=self.faction.country,
role="Flying Squadron", role="Flying Squadron",
aircraft=aircraft, aircraft=aircraft,
livery=None, livery=None,

View File

@ -40,7 +40,7 @@ class Coalition:
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet() self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
self.bullseye = Bullseye(Point(0, 0)) self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales) self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(player) self.air_wing = AirWing(player, game, self.faction)
self.transfers = PendingTransfers(game, player) self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually # Late initialized because the two coalitions in the game are mutually

View File

@ -1,21 +0,0 @@
class Event(object):
pass
class Observable(object):
def __init__(self) -> None:
self.callbacks = []
def subscribe(self, callback) -> None:
self.callbacks.append(callback)
def unsubscribe(self, callback) -> None:
self.callbacks.remove(callback)
def fire(self, **attrs) -> None:
e = Event()
e.source = self
for k, v in attrs.items():
setattr(e, k, v)
for fn in self.callbacks:
fn(e)

View File

@ -7,41 +7,34 @@ from typing import Sequence, Iterator, TYPE_CHECKING, Optional
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from .squadrondef import SquadronDef
from .squadrondefloader import SquadronDefLoader
from ..campaignloader.squadrondefgenerator import SquadronDefGenerator
from ..factions.faction import Faction
from ..theater import ControlPoint, MissionTarget from ..theater import ControlPoint, MissionTarget
from ..observer import Observable
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game
from ..ato.flighttype import FlightType from ..ato.flighttype import FlightType
from .squadron import Squadron from .squadron import Squadron
class AirWing(Observable): class AirWing:
def __init__(self, player: bool) -> None: def __init__(self, player: bool, game: Game, faction: Faction) -> None:
super().__init__()
self.player = player self.player = player
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
self.squadron_defs = SquadronDefLoader(game, faction).load()
self.squadron_def_generator = SquadronDefGenerator(faction)
def unclaim_squadron_def(self, squadron: Squadron) -> None:
if squadron.aircraft in self.squadron_defs:
for squadron_def in self.squadron_defs[squadron.aircraft]:
if squadron_def.claimed and squadron_def.name == squadron.name:
squadron_def.claimed = False
def add_squadron(self, squadron: Squadron) -> None: def add_squadron(self, squadron: Squadron) -> None:
new_aircraft_type = squadron.aircraft not in self.squadrons
self.squadrons[squadron.aircraft].append(squadron) self.squadrons[squadron.aircraft].append(squadron)
if new_aircraft_type:
self.fire(type="add_aircraft_type", obj=squadron.aircraft)
self.fire(type="add_squadron", obj=squadron)
def remove_squadron(self, toRemove: Squadron) -> None:
if toRemove.aircraft in self.squadrons:
self.squadrons[toRemove.aircraft].remove(toRemove)
self.fire(type="remove_squadron", obj=toRemove)
if len(self.squadrons[toRemove.aircraft]) == 0:
self.remove_aircraft_type(toRemove.aircraft)
def remove_aircraft_type(self, toRemove: AircraftType) -> None:
self.squadrons.pop(toRemove)
self.fire(type="remove_aircraft_type", obj=toRemove)
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]: def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
return self.squadrons[aircraft] return self.squadrons[aircraft]

View File

@ -418,6 +418,7 @@ class Squadron:
coalition: Coalition, coalition: Coalition,
game: Game, game: Game,
) -> Squadron: ) -> Squadron:
squadron_def.claimed = True
return Squadron( return Squadron(
squadron_def.name, squadron_def.name,
squadron_def.nickname, squadron_def.nickname,

View File

@ -28,6 +28,7 @@ class SquadronDef:
mission_types: tuple[FlightType, ...] mission_types: tuple[FlightType, ...]
operating_bases: OperatingBases operating_bases: OperatingBases
pilot_pool: list[Pilot] pilot_pool: list[Pilot]
claimed: bool = False
auto_assignable_mission_types: set[FlightType] = field( auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False init=False, hash=False, compare=False

View File

@ -10,13 +10,13 @@ from .squadrondef import SquadronDef
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.coalition import Coalition from ..factions.faction import Faction
class SquadronDefLoader: class SquadronDefLoader:
def __init__(self, game: Game, coalition: Coalition) -> None: def __init__(self, game: Game, faction: Faction) -> None:
self.game = game self.game = game
self.coalition = coalition self.faction = faction
@staticmethod @staticmethod
def squadron_directories() -> Iterator[Path]: def squadron_directories() -> Iterator[Path]:
@ -27,8 +27,8 @@ class SquadronDefLoader:
def load(self) -> dict[AircraftType, list[SquadronDef]]: def load(self) -> dict[AircraftType, list[SquadronDef]]:
squadrons: dict[AircraftType, list[SquadronDef]] = defaultdict(list) squadrons: dict[AircraftType, list[SquadronDef]] = defaultdict(list)
country = self.coalition.country_name country = self.faction.country
faction = self.coalition.faction faction = self.faction
any_country = country.startswith("Combined Joint Task Forces ") any_country = country.startswith("Combined Joint Task Forces ")
for directory in self.squadron_directories(): for directory in self.squadron_directories():
for path, squadron_def in self.load_squadrons_from(directory): for path, squadron_def in self.load_squadrons_from(directory):

View File

@ -111,6 +111,9 @@ def load_icons():
"./resources/ui/misc/" + get_theme_icons() + "/pluginsoptions.png" "./resources/ui/misc/" + get_theme_icons() + "/pluginsoptions.png"
) )
ICONS["Notes"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/notes.png") ICONS["Notes"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/notes.png")
ICONS["Reload"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/reload.png"
)
ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png") ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png") ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")

View File

@ -26,6 +26,7 @@ from PySide2.QtWidgets import (
QCheckBox, QCheckBox,
QPushButton, QPushButton,
QGridLayout, QGridLayout,
QToolButton,
) )
from game import Game from game import Game
@ -33,10 +34,9 @@ from game.ato.flighttype import FlightType
from game.coalition import Coalition from game.coalition import Coalition
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron from game.squadrons import AirWing, Pilot, Squadron
from game.squadrons.squadrondef import SquadronDef
from game.theater import ConflictTheater, ControlPoint from game.theater import ConflictTheater, ControlPoint
from qt_ui.uiconstants import AIRCRAFT_ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS, ICONS
from qt_ui.windows.SquadronConfigPopup import SquadronConfigPopup
class AllowedMissionTypeControls(QVBoxLayout): class AllowedMissionTypeControls(QVBoxLayout):
@ -72,7 +72,6 @@ class AllowedMissionTypeControls(QVBoxLayout):
self.allowed_mission_types.add(task) self.allowed_mission_types.add(task)
else: else:
self.allowed_mission_types.remove(task) self.allowed_mission_types.remove(task)
self.squadron.set_allowed_mission_types(self.allowed_mission_types)
class SquadronBaseSelector(QComboBox): class SquadronBaseSelector(QComboBox):
@ -85,29 +84,39 @@ class SquadronBaseSelector(QComboBox):
def __init__( def __init__(
self, self,
bases: Iterable[ControlPoint], bases: Iterable[ControlPoint],
squadron: Squadron, selected_base: Optional[ControlPoint],
aircraft_type: Optional[AircraftType],
) -> None: ) -> None:
super().__init__() super().__init__()
self.bases = list(bases)
self.squadron = squadron
self.setSizeAdjustPolicy(self.AdjustToContents) self.setSizeAdjustPolicy(self.AdjustToContents)
self.bases = list(bases)
self.set_aircraft_type(aircraft_type)
for base in self.bases: if selected_base:
if not base.can_operate(self.squadron.aircraft): self.setCurrentText(selected_base.name)
continue # TODO can we get a prefered base if none is selected?
self.addItem(base.name, base)
self.model().sort(0) def set_aircraft_type(self, aircraft_type: Optional[AircraftType]):
self.setCurrentText(self.squadron.location.name) self.clear()
if aircraft_type:
for base in self.bases:
if not base.can_operate(aircraft_type):
continue
self.addItem(base.name, base)
self.model().sort(0)
self.setEnabled(True)
else:
self.addItem("Select aircraft type first", None)
self.setEnabled(False)
self.update()
class SquadronConfigurationBox(QGroupBox): class SquadronConfigurationBox(QGroupBox):
def __init__( remove_squadron_signal = Signal(Squadron)
self, squadron: Squadron, theater: ConflictTheater, air_wing: AirWing
) -> None: def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
super().__init__() super().__init__()
self.setCheckable(False)
self.squadron = squadron self.squadron = squadron
self.air_wing = air_wing
self.reset_title() self.reset_title()
columns = QHBoxLayout() columns = QHBoxLayout()
@ -121,14 +130,24 @@ class SquadronConfigurationBox(QGroupBox):
self.name_edit.textChanged.connect(self.on_name_changed) self.name_edit.textChanged.connect(self.on_name_changed)
left_column.addWidget(self.name_edit) left_column.addWidget(self.name_edit)
left_column.addWidget(QLabel("Nickname:")) nickname_edit_layout = QGridLayout()
left_column.addLayout(nickname_edit_layout)
nickname_edit_layout.addWidget(QLabel("Nickname:"), 0, 0, 1, 2)
self.nickname_edit = QLineEdit(squadron.nickname) self.nickname_edit = QLineEdit(squadron.nickname)
self.nickname_edit.textChanged.connect(self.on_nickname_changed) self.nickname_edit.textChanged.connect(self.on_nickname_changed)
left_column.addWidget(self.nickname_edit) nickname_edit_layout.addWidget(self.nickname_edit, 1, 0, Qt.AlignTop)
reroll_nickname_button = QToolButton()
reroll_nickname_button.setIcon(QIcon(ICONS["Reload"]))
reroll_nickname_button.setToolTip("Re-roll nickname")
reroll_nickname_button.clicked.connect(self.reroll_nickname)
nickname_edit_layout.addWidget(reroll_nickname_button, 1, 1, Qt.AlignTop)
left_column.addWidget(QLabel("Base:")) left_column.addWidget(QLabel("Base:"))
self.base_selector = SquadronBaseSelector( self.base_selector = SquadronBaseSelector(
theater.control_points_for(squadron.player), squadron theater.control_points_for(squadron.player),
squadron.location,
squadron.aircraft,
) )
self.base_selector.currentIndexChanged.connect(self.on_base_changed) self.base_selector.currentIndexChanged.connect(self.on_base_changed)
left_column.addWidget(self.base_selector) left_column.addWidget(self.base_selector)
@ -150,20 +169,18 @@ class SquadronConfigurationBox(QGroupBox):
self.player_list.setAcceptRichText(False) self.player_list.setAcceptRichText(False)
self.player_list.setEnabled(squadron.player) self.player_list.setEnabled(squadron.player)
left_column.addWidget(self.player_list) left_column.addWidget(self.player_list)
self.player_list.textChanged.connect(self.on_pilots_changed) delete_button = QPushButton("Remove Squadron")
delete_button.setMaximumWidth(140)
delete_button = QPushButton("Remove") delete_button.clicked.connect(self.remove_from_squadron_config)
delete_button.setMaximumWidth(80)
delete_button.clicked.connect(
lambda state: self.air_wing.remove_squadron(self.squadron)
)
left_column.addWidget(delete_button) left_column.addWidget(delete_button)
left_column.addStretch() left_column.addStretch()
self.allowed_missions = AllowedMissionTypeControls(squadron) self.allowed_missions = AllowedMissionTypeControls(squadron)
columns.addLayout(self.allowed_missions) columns.addLayout(self.allowed_missions)
def remove_from_squadron_config(self) -> None:
self.remove_squadron_signal.emit(self.squadron)
def on_name_changed(self, text: str) -> None: def on_name_changed(self, text: str) -> None:
self.squadron.name = text self.squadron.name = text
self.reset_title() self.reset_title()
@ -180,56 +197,66 @@ class SquadronConfigurationBox(QGroupBox):
def reset_title(self) -> None: def reset_title(self) -> None:
self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}") self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}")
def on_pilots_changed(self) -> None: def reroll_nickname(self) -> None:
self.nickname_edit.setText(
self.squadron.coalition.air_wing.squadron_def_generator.random_nickname()
)
def apply(self) -> Squadron:
player_names = self.player_list.toPlainText().splitlines() player_names = self.player_list.toPlainText().splitlines()
# Prepend player pilots so they get set active first. # Prepend player pilots so they get set active first.
self.squadron.pilot_pool = [ self.squadron.pilot_pool = [
Pilot(n, player=True) for n in player_names Pilot(n, player=True) for n in player_names
] + self.squadron.pilot_pool ] + self.squadron.pilot_pool
self.squadron.set_allowed_mission_types(
self.allowed_missions.allowed_mission_types
)
return self.squadron
class SquadronConfigurationLayout(QVBoxLayout): class SquadronConfigurationLayout(QVBoxLayout):
def __init__( config_changed = Signal(AircraftType)
self, squadrons: list[Squadron], theater: ConflictTheater, air_wing: AirWing
) -> None: def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None:
super().__init__() super().__init__()
self.squadron_configs = []
self.theater = theater self.theater = theater
self.air_wing = air_wing
self.squadron_configs: dict[Squadron, SquadronConfigurationBox] = {}
for squadron in squadrons: for squadron in squadrons:
squadron_config = SquadronConfigurationBox( self.add_squadron(squadron)
squadron, self.theater, self.air_wing
)
self.squadron_configs[squadron] = squadron_config
self.addWidget(squadron_config)
def addSquadron(self, squadron: Squadron) -> None: def apply(self) -> list[Squadron]:
if squadron not in self.squadron_configs: keep_squadrons = []
squadron_config = SquadronConfigurationBox( for squadron_config in self.squadron_configs:
squadron, self.theater, self.air_wing keep_squadrons.append(squadron_config.apply())
) return keep_squadrons
self.squadron_configs[squadron] = squadron_config
self.addWidget(squadron_config)
self.update()
def removeSquadron(self, squadron: Squadron) -> None: def remove_squadron(self, squadron: Squadron) -> None:
if squadron in self.squadron_configs: for squadron_config in self.squadron_configs:
self.removeWidget(self.squadron_configs[squadron]) if squadron_config.squadron == squadron:
self.squadron_configs.pop(squadron) squadron_config.deleteLater()
self.update() self.squadron_configs.remove(squadron_config)
squadron.coalition.air_wing.unclaim_squadron_def(squadron)
self.update()
self.config_changed.emit(squadron.aircraft)
return
def add_squadron(self, squadron: Squadron) -> None:
squadron_config = SquadronConfigurationBox(squadron, self.theater)
squadron_config.remove_squadron_signal.connect(self.remove_squadron)
self.squadron_configs.append(squadron_config)
self.addWidget(squadron_config)
class AircraftSquadronsPage(QWidget): class AircraftSquadronsPage(QWidget):
def __init__( remove_squadron_page = Signal(AircraftType)
self, squadrons: list[Squadron], theater: ConflictTheater, air_wing: AirWing
) -> None: def __init__(self, squadrons: list[Squadron], theater: ConflictTheater) -> None:
super().__init__() super().__init__()
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) self.setLayout(layout)
self.squadrons_config = SquadronConfigurationLayout( self.squadrons_config = SquadronConfigurationLayout(squadrons, theater)
squadrons, theater, air_wing self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
)
scrolling_widget = QWidget() scrolling_widget = QWidget()
scrolling_widget.setLayout(self.squadrons_config) scrolling_widget.setLayout(self.squadrons_config)
@ -242,51 +269,59 @@ class AircraftSquadronsPage(QWidget):
layout.addWidget(scrolling_area) layout.addWidget(scrolling_area)
def addSquadron(self, squadron: Squadron) -> None: def on_squadron_config_changed(self, aircraft_type: AircraftType):
self.squadrons_config.addSquadron(squadron) if len(self.squadrons_config.squadron_configs) == 0:
self.remove_squadron_page.emit(aircraft_type)
def removeSquadron(self, squadron: Squadron) -> None: def add_squadron_to_page(self, squadron: Squadron):
self.squadrons_config.removeSquadron(squadron) self.squadrons_config.add_squadron(squadron)
def apply(self) -> list[Squadron]:
return self.squadrons_config.apply()
class AircraftSquadronsPanel(QStackedLayout): class AircraftSquadronsPanel(QStackedLayout):
page_removed = Signal(AircraftType)
def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None: def __init__(self, air_wing: AirWing, theater: ConflictTheater) -> None:
super().__init__() super().__init__()
self.air_wing = air_wing self.air_wing = air_wing
self.theater = theater self.theater = theater
self.air_wing.subscribe(self.handleChanges)
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {} self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
for aircraft, squadrons in self.air_wing.squadrons.items(): for aircraft, squadrons in self.air_wing.squadrons.items():
page = AircraftSquadronsPage(squadrons, self.theater, self.air_wing) self.new_page_for_type(aircraft, squadrons)
self.addWidget(page)
self.squadrons_pages[aircraft] = page
def __del__(self) -> None: def remove_page_for_type(self, aircraft_type: AircraftType):
self.air_wing.unsubscribe(self.handleChanges) page = self.squadrons_pages[aircraft_type]
self.removeWidget(page)
def handleChanges(self, event) -> None: page.deleteLater()
if event.type == "add_aircraft_type": self.squadrons_pages.pop(aircraft_type)
aircraft_type = event.obj self.page_removed.emit(aircraft_type)
if aircraft_type not in self.squadrons_pages:
page = AircraftSquadronsPage(
self.air_wing.squadrons[aircraft_type], self.theater, self.air_wing
)
self.addWidget(page)
self.squadrons_pages[aircraft_type] = page
elif event.type == "remove_aircraft_type":
aircraft_type = event.obj
if aircraft_type in self.squadrons_pages:
self.removeWidget(self.squadrons_pages[aircraft_type])
self.squadrons_pages.pop(aircraft_type)
elif event.type == "add_squadron":
squadron = event.obj
self.squadrons_pages[squadron.aircraft].addSquadron(squadron)
elif event.type == "remove_squadron":
squadron = event.obj
self.squadrons_pages[squadron.aircraft].removeSquadron(squadron)
self.update() self.update()
def new_page_for_type(
self, aircraft_type: AircraftType, squadrons: list[Squadron]
) -> None:
page = AircraftSquadronsPage(squadrons, self.theater)
page.remove_squadron_page.connect(self.remove_page_for_type)
self.addWidget(page)
self.squadrons_pages[aircraft_type] = page
def add_squadron_to_panel(self, squadron: Squadron):
# Find existing page or add new one
if squadron.aircraft in self.squadrons_pages:
page = self.squadrons_pages[squadron.aircraft]
page.add_squadron_to_page(squadron)
else:
self.new_page_for_type(squadron.aircraft, [squadron])
self.update()
def apply(self) -> None:
self.air_wing.squadrons = {}
for aircraft, page in self.squadrons_pages.items():
self.air_wing.squadrons[aircraft] = page.apply()
class AircraftTypeList(QListView): class AircraftTypeList(QListView):
page_index_changed = Signal(int) page_index_changed = Signal(int)
@ -296,47 +331,29 @@ class AircraftTypeList(QListView):
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
self.setMinimumWidth(300) self.setMinimumWidth(300)
self.air_wing = air_wing
self.item_model = QStandardItemModel(self) self.item_model = QStandardItemModel(self)
self.setModel(self.item_model) self.setModel(self.item_model)
for aircraft in self.air_wing.squadrons:
aircraft_item = QStandardItem(aircraft.name)
icon = self.icon_for(aircraft)
if icon is not None:
aircraft_item.setIcon(icon)
aircraft_item.setEditable(False)
aircraft_item.setSelectable(True)
self.item_model.appendRow(aircraft_item)
self.selectionModel().setCurrentIndex( self.selectionModel().setCurrentIndex(
self.item_model.index(0, 0), QItemSelectionModel.Select self.item_model.index(0, 0), QItemSelectionModel.Select
) )
self.selectionModel().selectionChanged.connect(self.on_selection_changed) self.selectionModel().selectionChanged.connect(self.on_selection_changed)
for aircraft in air_wing.squadrons:
self.add_aircraft_type(aircraft)
self.air_wing.subscribe(self.handleChanges) def remove_aircraft_type(self, aircraft: AircraftType):
for item in self.item_model.findItems(aircraft.name):
self.item_model.removeRow(item.row())
self.page_index_changed.emit(self.selectionModel().currentIndex().row())
def __del__(self) -> None: def add_aircraft_type(self, aircraft: AircraftType):
self.air_wing.unsubscribe(self.handleChanges) aircraft_item = QStandardItem(aircraft.name)
icon = self.icon_for(aircraft)
def handleChanges(self, event) -> None: if icon is not None:
if event.type == "remove_aircraft_type": aircraft_item.setIcon(icon)
aircraft_type = event.obj aircraft_item.setEditable(False)
items = self.item_model.findItems(aircraft_type.name) aircraft_item.setSelectable(True)
if len(items) == 1: self.item_model.appendRow(aircraft_item)
for item in items:
self.item_model.takeRow(item.row())
elif event.type == "add_aircraft_type":
aircraft_type = event.obj
aircraft_item = QStandardItem(aircraft_type.name)
icon = self.icon_for(aircraft_type)
if icon is not None:
aircraft_item.setIcon(icon)
aircraft_item.setEditable(False)
aircraft_item.setSelectable(True)
self.item_model.appendRow(aircraft_item)
self.update()
def on_selection_changed( def on_selection_changed(
self, selected: QItemSelection, _deselected: QItemSelection self, selected: QItemSelection, _deselected: QItemSelection
@ -348,18 +365,6 @@ class AircraftTypeList(QListView):
return return
self.page_index_changed.emit(indexes[0].row()) self.page_index_changed.emit(indexes[0].row())
def deleteSelectedType(self) -> None:
if self.selectionModel().currentIndex().isValid():
aircraftName = str(self.selectionModel().currentIndex().data())
to_remove = None
for type in self.air_wing.squadrons:
if str(type) == aircraftName:
to_remove = type
if to_remove != None:
self.air_wing.remove_aircraft_type(to_remove)
else:
raise RuntimeError("No aircraft was selected for removal")
@staticmethod @staticmethod
def icon_for(aircraft: AircraftType) -> Optional[QIcon]: def icon_for(aircraft: AircraftType) -> Optional[QIcon]:
name = aircraft.dcs_id name = aircraft.dcs_id
@ -369,37 +374,69 @@ class AircraftTypeList(QListView):
class AirWingConfigurationTab(QWidget): class AirWingConfigurationTab(QWidget):
def __init__( def __init__(self, coalition: Coalition, game: Game) -> None:
self, coalition: Coalition, theater: ConflictTheater, game: Game
) -> None:
super().__init__() super().__init__()
self.game = game
self.theater = theater
self.coalition = coalition
self.air_wing = coalition.air_wing
layout = QGridLayout() layout = QGridLayout()
self.setLayout(layout) self.setLayout(layout)
self.game = game
self.coalition = coalition
self.type_list = AircraftTypeList(self.air_wing) self.type_list = AircraftTypeList(coalition.air_wing)
layout.addWidget(self.type_list, 1, 1, 1, 2) layout.addWidget(self.type_list, 1, 1, 1, 2)
add_button = QPushButton("Add Aircraft/Squadron") add_button = QPushButton("Add Squadron")
add_button.clicked.connect(lambda state: self.addAircraftType()) add_button.clicked.connect(lambda state: self.add_squadron())
layout.addWidget(add_button, 2, 1, 1, 1) layout.addWidget(add_button, 2, 1, 1, 1)
remove_button = QPushButton("Remove Aircraft") self.squadrons_panel = AircraftSquadronsPanel(coalition.air_wing, game.theater)
remove_button.clicked.connect(lambda state: self.type_list.deleteSelectedType()) self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
layout.addWidget(remove_button, 2, 2, 1, 1)
self.squadrons_panel = AircraftSquadronsPanel(self.air_wing, self.theater)
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1) layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)
self.type_list.page_index_changed.connect(self.squadrons_panel.setCurrentIndex) self.type_list.page_index_changed.connect(self.squadrons_panel.setCurrentIndex)
def addAircraftType(self) -> None: def add_squadron(self) -> None:
SquadronConfigPopup(self.coalition, self.theater, self.game).exec_() selected_aircraft = None
if self.type_list.selectionModel().currentIndex().row() >= 0:
selected_aircraft = self.type_list.item_model.item(
self.type_list.selectionModel().currentIndex().row()
).text()
popup = SquadronConfigPopup(
selected_aircraft,
self.coalition.faction.aircrafts,
list(self.game.theater.control_points_for(self.coalition.player)),
self.coalition.air_wing.squadron_defs,
)
if popup.exec_() != QDialog.Accepted:
return
selected_type = popup.aircraft_type_selector.currentData()
selected_base = popup.squadron_base_selector.currentData()
selected_def = popup.squadron_def_selector.currentData()
# Let user choose the preset or generate one
squadron_def = (
selected_def
or self.coalition.air_wing.squadron_def_generator.generate_for_aircraft(
selected_type
)
)
squadron = Squadron.create_from(
squadron_def, selected_base, self.coalition, self.game
)
# Add Squadron
if not self.type_list.item_model.findItems(selected_type.name):
self.type_list.add_aircraft_type(selected_type)
# TODO Select the newly added type
self.squadrons_panel.add_squadron_to_panel(squadron)
self.update()
def apply(self) -> None:
self.squadrons_panel.apply()
class AirWingConfigurationDialog(QDialog): class AirWingConfigurationDialog(QDialog):
@ -414,22 +451,13 @@ class AirWingConfigurationDialog(QDialog):
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) self.setLayout(layout)
doc_url = (
"https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots"
)
doc_label = QLabel( doc_label = QLabel(
"Use this opportunity to customize the squadrons available to your " "Use this opportunity to customize the squadrons available to your "
"coalition. <strong>This is your only opportunity to make changes.</strong>" "coalition. <strong>This is your only opportunity to make changes.</strong>"
"<br /><br />" "<br /><br />"
"To accept your changes and continue, close this window.<br />" "To accept your changes and continue, close this window."
"<br />"
"To remove a squadron from the game, uncheck the box in the title. New "
"squadrons cannot be added via the UI at this time. To add a custom "
"squadron,<br />"
f'see <a style="color:#ffffff" href="{doc_url}">the wiki</a>.'
) )
doc_label.setOpenExternalLinks(True)
layout.addWidget(doc_label) layout.addWidget(doc_label)
tab_widget = QTabWidget() tab_widget = QTabWidget()
@ -437,10 +465,113 @@ class AirWingConfigurationDialog(QDialog):
self.tabs = [] self.tabs = []
for coalition in game.coalitions: for coalition in game.coalitions:
coalition_tab = AirWingConfigurationTab(coalition, game.theater, game) coalition_tab = AirWingConfigurationTab(coalition, game)
name = "Blue" if coalition.player else "Red" name = "Blue" if coalition.player else "Red"
tab_widget.addTab(coalition_tab, name) tab_widget.addTab(coalition_tab, name)
self.tabs.append(coalition_tab) self.tabs.append(coalition_tab)
def reject(self) -> None: def reject(self) -> None:
for tab in self.tabs:
tab.apply()
super().reject() super().reject()
class SquadronAircraftTypeSelector(QComboBox):
def __init__(
self, types: list[AircraftType], selected_aircraft: Optional[str]
) -> None:
super().__init__()
self.setSizeAdjustPolicy(self.AdjustToContents)
for type in sorted(types, key=lambda type: type.name):
self.addItem(type.name, type)
if selected_aircraft:
self.setCurrentText(selected_aircraft)
class SquadronDefSelector(QComboBox):
def __init__(
self,
squadron_defs: dict[AircraftType, list[SquadronDef]],
aircraft: Optional[AircraftType],
) -> None:
super().__init__()
self.setSizeAdjustPolicy(self.AdjustToContents)
self.squadron_defs = squadron_defs
self.set_aircraft_type(aircraft)
def set_aircraft_type(self, aircraft: Optional[AircraftType]):
self.clear()
self.addItem("None (Random)", None)
if aircraft and aircraft in self.squadron_defs:
for squadron_def in sorted(
self.squadron_defs[aircraft], key=lambda squadron_def: squadron_def.name
):
if not squadron_def.claimed:
squadron_name = squadron_def.name
if squadron_def.nickname:
squadron_name += " (" + squadron_def.nickname + ")"
self.addItem(squadron_name, squadron_def)
self.setCurrentText("None (Random)")
class SquadronConfigPopup(QDialog):
def __init__(
self,
selected_aircraft: Optional[str],
types: list[AircraftType],
bases: list[ControlPoint],
squadron_defs: dict[AircraftType, list[SquadronDef]],
) -> None:
super().__init__()
self.setWindowTitle(f"Add new Squadron")
self.column = QVBoxLayout()
self.setLayout(self.column)
self.bases = bases
self.column.addWidget(QLabel("Aircraft:"))
self.aircraft_type_selector = SquadronAircraftTypeSelector(
types, selected_aircraft
)
self.aircraft_type_selector.currentIndexChanged.connect(
self.on_aircraft_selection
)
self.column.addWidget(self.aircraft_type_selector)
self.column.addWidget(QLabel("Base:"))
self.squadron_base_selector = SquadronBaseSelector(
bases, None, self.aircraft_type_selector.currentData()
)
self.column.addWidget(self.squadron_base_selector)
self.column.addWidget(QLabel("Preset:"))
self.squadron_def_selector = SquadronDefSelector(
squadron_defs, self.aircraft_type_selector.currentData()
)
self.column.addWidget(self.squadron_def_selector)
self.column.addStretch()
self.button_layout = QHBoxLayout()
self.column.addLayout(self.button_layout)
self.accept_button = QPushButton("Accept")
self.accept_button.clicked.connect(lambda state: self.accept())
self.button_layout.addWidget(self.accept_button)
self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(lambda state: self.reject())
self.button_layout.addWidget(self.cancel_button)
def on_aircraft_selection(self) -> None:
self.squadron_base_selector.set_aircraft_type(
self.aircraft_type_selector.currentData()
)
self.squadron_def_selector.set_aircraft_type(
self.aircraft_type_selector.currentData()
)
self.update()

View File

@ -1,145 +0,0 @@
from typing import Optional, Callable, Iterable
from PySide2.QtWidgets import (
QDialog,
QPushButton,
QVBoxLayout,
QLabel,
QLineEdit,
QTextEdit,
QCheckBox,
QHBoxLayout,
QComboBox,
)
from game.dcs.aircrafttype import AircraftType
from game.game import Game
from game.squadrons import Squadron
from game.theater import ConflictTheater, ControlPoint
from game.coalition import Coalition
from game.factions.faction import Faction
from game.campaignloader.squadrondefgenerator import SquadronDefGenerator
from gen.flights.flight import FlightType
class AircraftTypeSelector(QComboBox):
def __init__(self, faction: Faction) -> None:
super().__init__()
self.types = faction.aircrafts
self.setSizeAdjustPolicy(self.AdjustToContents)
self.addItem("Select aircraft type...", None)
self.setCurrentText("Select aircraft type...")
self.types.sort(key=str)
for type in self.types:
self.addItem(type.name, type)
class SquadronConfigPopup(QDialog):
def __init__(
self, coalition: Coalition, theater: ConflictTheater, game: Game
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.theater = theater
self.squadron = None
# self.setMinimumSize(500, 800)
self.setWindowTitle(f"Add new Squadron")
self.column = QVBoxLayout()
self.setLayout(self.column)
self.aircraft_type_selector = AircraftTypeSelector(coalition.faction)
self.column.addWidget(self.aircraft_type_selector)
self.column.addWidget(QLabel("Name:"))
self.name_edit = QLineEdit("---")
self.name_edit.setEnabled(False)
self.name_edit.textChanged.connect(self.on_name_changed)
self.column.addWidget(self.name_edit)
self.column.addWidget(QLabel("Nickname:"))
self.nickname_edit = QLineEdit("---")
self.nickname_edit.setEnabled(False)
self.nickname_edit.textChanged.connect(self.on_nickname_changed)
self.column.addWidget(self.nickname_edit)
self.column.addStretch()
self.aircraft_type_selector.currentIndexChanged.connect(
self.on_aircraft_selection
)
self.button_layout = QHBoxLayout()
self.column.addLayout(self.button_layout)
self.accept_button = QPushButton("Accept")
self.accept_button.clicked.connect(lambda state: self.accept())
self.accept_button.setEnabled(False)
self.button_layout.addWidget(self.accept_button)
self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(lambda state: self.cancel())
self.button_layout.addWidget(self.cancel_button)
def create_Squadron(
self, aircraft_type: AircraftType, base: ControlPoint
) -> Squadron:
squadron_def = SquadronDefGenerator(self.coalition).generate_for_aircraft(
aircraft_type
)
squadron = Squadron(
squadron_def.name,
squadron_def.nickname,
squadron_def.country,
squadron_def.role,
squadron_def.aircraft,
squadron_def.livery,
squadron_def.mission_types,
squadron_def.operating_bases,
squadron_def.pilot_pool,
self.coalition,
self.game.settings,
base,
)
return squadron
def on_aircraft_selection(self, index: int) -> None:
aircraft_type_name = self.aircraft_type_selector.currentText()
aircraft_type = None
for aircraft in self.coalition.faction.aircrafts:
if str(aircraft) == aircraft_type_name:
aircraft_type = aircraft
if aircraft != None:
self.squadron = self.create_Squadron(
aircraft_type,
next(self.theater.control_points_for(self.coalition.player)),
)
self.name_edit.setText(self.squadron.name)
self.name_edit.setEnabled(True)
self.nickname_edit.setText(self.squadron.nickname)
self.nickname_edit.setEnabled(True)
self.accept_button.setStyleSheet("background-color: green")
self.accept_button.setEnabled(True)
self.update()
def on_name_changed(self, text: str) -> None:
self.squadron.name = text
def on_nickname_changed(self, text: str) -> None:
self.squadron.nickname = text
def accept(self) -> None:
self.coalition.air_wing.add_squadron(self.squadron)
return super().accept()
def cancel(self) -> None:
return super().reject()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB