mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
- Add the new airassault mission type and special flightplans for it - Add the mission type to airbase and FOB - Add Layout for the UH-1H - Add mission type to capable squadrons - Allow the auto planner to task air assault missions when preconditions are met - Improve Airlift mission type and improve the flightplan (Stopover and Helo landing) - Allow Slingload and spawnable crates for airlift - Rework airsupport to a general missiondata class - Added Carrier Information to mission data - Allow to define CTLD specific capabilities in the unit yaml - Allow inflight preload and fixed wing support for air assault
248 lines
9.6 KiB
Python
248 lines
9.6 KiB
Python
from typing import Optional, Type
|
|
|
|
from PySide2.QtCore import Qt, Signal
|
|
from PySide2.QtWidgets import (
|
|
QComboBox,
|
|
QDialog,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QVBoxLayout,
|
|
QLineEdit,
|
|
QHBoxLayout,
|
|
)
|
|
from dcs.unittype import FlyingType
|
|
|
|
from game import Game
|
|
from game.ato.starttype import StartType
|
|
from game.squadrons.squadron import Squadron
|
|
from game.theater import ControlPoint, OffMapSpawn
|
|
from game.ato.package import Package
|
|
from game.ato.flightroster import FlightRoster
|
|
from game.ato.flight import Flight
|
|
from qt_ui.uiconstants import EVENT_ICONS
|
|
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
|
|
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
|
from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
|
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
|
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
|
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
|
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
|
|
|
|
|
|
class QFlightCreator(QDialog):
|
|
created = Signal(Flight)
|
|
|
|
def __init__(self, game: Game, package: Package, parent=None) -> None:
|
|
super().__init__(parent=parent)
|
|
self.setMinimumWidth(400)
|
|
|
|
self.game = game
|
|
self.package = package
|
|
self.custom_name_text = None
|
|
self.country = self.game.blue.country_name
|
|
|
|
# Make dialog modal to prevent background windows to close unexpectedly.
|
|
self.setModal(True)
|
|
|
|
self.setWindowTitle("Create flight")
|
|
self.setWindowIcon(EVENT_ICONS["strike"])
|
|
|
|
layout = QVBoxLayout()
|
|
|
|
self.task_selector = QFlightTypeComboBox(
|
|
self.game.theater, package.target, self.game.settings
|
|
)
|
|
self.task_selector.setCurrentIndex(0)
|
|
self.task_selector.currentIndexChanged.connect(self.on_task_changed)
|
|
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
|
|
|
self.aircraft_selector = QAircraftTypeSelector(
|
|
self.game.blue.air_wing.best_available_aircrafts_for(
|
|
self.task_selector.currentData()
|
|
)
|
|
)
|
|
self.aircraft_selector.setCurrentIndex(0)
|
|
self.aircraft_selector.currentIndexChanged.connect(self.on_aircraft_changed)
|
|
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
|
|
|
|
self.squadron_selector = SquadronSelector(
|
|
self.game.air_wing_for(player=True),
|
|
self.task_selector.currentData(),
|
|
self.aircraft_selector.currentData(),
|
|
)
|
|
self.squadron_selector.setCurrentIndex(0)
|
|
layout.addLayout(QLabeledWidget("Squadron:", self.squadron_selector))
|
|
|
|
self.divert = QArrivalAirfieldSelector(
|
|
[cp for cp in game.theater.controlpoints if cp.captured],
|
|
self.aircraft_selector.currentData(),
|
|
"None",
|
|
)
|
|
layout.addLayout(QLabeledWidget("Divert:", self.divert))
|
|
|
|
self.flight_size_spinner = QFlightSizeSpinner()
|
|
self.update_max_size(self.squadron_selector.aircraft_available)
|
|
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
|
|
|
squadron = self.squadron_selector.currentData()
|
|
if squadron is None:
|
|
roster = None
|
|
else:
|
|
roster = FlightRoster(
|
|
squadron, initial_size=self.flight_size_spinner.value()
|
|
)
|
|
self.roster_editor = FlightRosterEditor(roster)
|
|
self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize)
|
|
self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
|
|
roster_layout = QHBoxLayout()
|
|
layout.addLayout(roster_layout)
|
|
roster_layout.addWidget(QLabel("Assigned pilots:"))
|
|
roster_layout.addLayout(self.roster_editor)
|
|
|
|
# When an off-map spawn overrides the start type to in-flight, we save
|
|
# the selected type into this value. If a non-off-map spawn is selected
|
|
# we restore the previous choice.
|
|
self.restore_start_type: Optional[str] = None
|
|
self.start_type = QComboBox()
|
|
for start_type in StartType:
|
|
self.start_type.addItem(start_type.value, start_type)
|
|
self.start_type.setCurrentText(self.game.settings.default_start_type.value)
|
|
layout.addLayout(
|
|
QLabeledWidget(
|
|
"Start type:",
|
|
self.start_type,
|
|
tooltip="Selects the start type for this flight.",
|
|
)
|
|
)
|
|
layout.addWidget(
|
|
QLabel(
|
|
"Any option other than Cold will make this flight "
|
|
+ "non-targetable<br />by OCA/Aircraft missions. This will affect "
|
|
+ "game balance."
|
|
)
|
|
)
|
|
|
|
self.custom_name = QLineEdit()
|
|
self.custom_name.textChanged.connect(self.set_custom_name_text)
|
|
layout.addLayout(
|
|
QLabeledWidget("Custom Flight Name (Optional)", self.custom_name)
|
|
)
|
|
|
|
layout.addStretch()
|
|
|
|
self.create_button = QPushButton("Create")
|
|
self.create_button.clicked.connect(self.create_flight)
|
|
layout.addWidget(self.create_button, alignment=Qt.AlignRight)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def reject(self) -> None:
|
|
super().reject()
|
|
# Clear the roster to return pilots to the pool.
|
|
self.roster_editor.replace(None)
|
|
|
|
def set_custom_name_text(self, text: str):
|
|
self.custom_name_text = text
|
|
|
|
def verify_form(self) -> Optional[str]:
|
|
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
|
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
|
divert: Optional[ControlPoint] = self.divert.currentData()
|
|
size: int = self.flight_size_spinner.value()
|
|
if aircraft is None:
|
|
return "You must select an aircraft type."
|
|
if squadron is None:
|
|
return "You must select a squadron."
|
|
if divert is not None and not divert.captured:
|
|
return f"{divert.name} is not owned by your coalition."
|
|
available = squadron.untasked_aircraft
|
|
if not available:
|
|
return f"{squadron} has no aircraft available."
|
|
if size > available:
|
|
return f"{squadron} has only {available} aircraft available."
|
|
if size <= 0:
|
|
return f"Flight must have at least one aircraft."
|
|
if self.custom_name_text and "|" in self.custom_name_text:
|
|
return f"Cannot include | in flight name"
|
|
return None
|
|
|
|
def create_flight(self) -> None:
|
|
error = self.verify_form()
|
|
if error is not None:
|
|
QMessageBox.critical(self, "Could not create flight", error, QMessageBox.Ok)
|
|
return
|
|
|
|
task = self.task_selector.currentData()
|
|
squadron = self.squadron_selector.currentData()
|
|
divert = self.divert.currentData()
|
|
roster = self.roster_editor.roster
|
|
|
|
flight = Flight(
|
|
self.package,
|
|
self.country,
|
|
squadron,
|
|
# A bit of a hack to work around the old API. Not actually relevant because
|
|
# the roster is passed explicitly. Needs a refactor.
|
|
roster.max_size,
|
|
task,
|
|
self.start_type.currentData(),
|
|
divert,
|
|
custom_name=self.custom_name_text,
|
|
roster=roster,
|
|
)
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
self.created.emit(flight)
|
|
self.accept()
|
|
|
|
def on_aircraft_changed(self, index: int) -> None:
|
|
new_aircraft = self.aircraft_selector.itemData(index)
|
|
self.squadron_selector.update_items(
|
|
self.task_selector.currentData(), new_aircraft
|
|
)
|
|
self.divert.change_aircraft(new_aircraft)
|
|
|
|
def on_departure_changed(self, departure: ControlPoint) -> None:
|
|
if isinstance(departure, OffMapSpawn):
|
|
previous_type = self.start_type.currentData()
|
|
if previous_type != StartType.IN_FLIGHT:
|
|
self.restore_start_type = previous_type
|
|
self.start_type.setCurrentText(StartType.IN_FLIGHT.value)
|
|
self.start_type.setEnabled(False)
|
|
else:
|
|
self.start_type.setEnabled(True)
|
|
if self.restore_start_type is not None:
|
|
self.start_type.setCurrentText(self.restore_start_type)
|
|
self.restore_start_type = None
|
|
|
|
def on_task_changed(self, index: int) -> None:
|
|
task = self.task_selector.itemData(index)
|
|
self.aircraft_selector.update_items(
|
|
self.game.blue.air_wing.best_available_aircrafts_for(task)
|
|
)
|
|
self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
|
|
|
|
def on_squadron_changed(self, index: int) -> None:
|
|
squadron: Optional[Squadron] = self.squadron_selector.itemData(index)
|
|
self.update_max_size(self.squadron_selector.aircraft_available)
|
|
# Clear the roster first so we return the pilots to the pool. This way if we end
|
|
# up repopulating from the same squadron we'll get the same pilots back.
|
|
self.roster_editor.replace(None)
|
|
if squadron is not None:
|
|
self.roster_editor.replace(
|
|
FlightRoster(squadron, self.flight_size_spinner.value())
|
|
)
|
|
self.on_departure_changed(squadron.location)
|
|
|
|
def update_max_size(self, available: int) -> None:
|
|
aircraft = self.aircraft_selector.currentData()
|
|
if aircraft is None:
|
|
self.flight_size_spinner.setMaximum(0)
|
|
return
|
|
|
|
self.flight_size_spinner.setMaximum(min(available, aircraft.max_group_size))
|
|
|
|
default_size = max(2, available, aircraft.max_group_size)
|
|
self.flight_size_spinner.setValue(default_size)
|