mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Previously we were trying to make every potential flight plan look just like a strike mission's flight plan. This led to a lot of special case behavior in several places that was causing us to misplan TOTs. I've reorganized this such that there's now an explicit `FlightPlan` class, and any specialized behavior is handled by the subclasses. I've also taken the opportunity to alter the behavior of CAS and front-line CAP missions. These no longer involve the usual formation waypoints. Instead the CAP will aim to be on station at the time that the CAS mission reaches its ingress point, and leave at its egress time. Both flights fly directly to the point with a start time configured for a rendezvous. It might be worth adding hold points back to every flight plan just to ensure that non-formation flights don't end up with a very low speed enroute to the target if they perform ground ops quicker than expected.
240 lines
9.1 KiB
Python
240 lines
9.1 KiB
Python
from typing import List, Optional
|
|
|
|
from PySide2.QtWidgets import (
|
|
QFrame,
|
|
QGroupBox,
|
|
QHBoxLayout,
|
|
QMessageBox,
|
|
QPushButton,
|
|
)
|
|
|
|
import qt_ui.uiconstants as CONST
|
|
from game import Game
|
|
from game.event import CAP, CAS, FrontlineAttackEvent
|
|
from gen.ato import Package
|
|
from gen.flights.traveltime import TotEstimator
|
|
from qt_ui.models import GameModel
|
|
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
|
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
|
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.QWaitingForMissionResultWindow import \
|
|
QWaitingForMissionResultWindow
|
|
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
|
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
|
|
|
|
|
class QTopPanel(QFrame):
|
|
|
|
def __init__(self, game_model: GameModel):
|
|
super(QTopPanel, self).__init__()
|
|
self.game_model = game_model
|
|
self.setMaximumHeight(70)
|
|
self.init_ui()
|
|
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
|
|
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
|
|
|
|
@property
|
|
def game(self) -> Optional[Game]:
|
|
return self.game_model.game
|
|
|
|
def init_ui(self):
|
|
|
|
self.turnCounter = QTurnCounter()
|
|
self.budgetBox = QBudgetBox(self.game)
|
|
|
|
self.passTurnButton = QPushButton("Pass Turn")
|
|
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
|
|
self.passTurnButton.setProperty("style", "btn-primary")
|
|
self.passTurnButton.clicked.connect(self.passTurn)
|
|
|
|
self.proceedButton = QPushButton("Take off")
|
|
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
|
|
self.proceedButton.setProperty("style", "start-button")
|
|
self.proceedButton.clicked.connect(self.launch_mission)
|
|
if self.game and self.game.turn == 0:
|
|
self.proceedButton.setEnabled(False)
|
|
|
|
self.factionsInfos = QFactionsInfos(self.game)
|
|
|
|
self.settings = QPushButton("Settings")
|
|
self.settings.setIcon(CONST.ICONS["Settings"])
|
|
self.settings.setProperty("style", "btn-primary")
|
|
self.settings.clicked.connect(self.openSettings)
|
|
|
|
self.statistics = QPushButton("Statistics")
|
|
self.statistics.setIcon(CONST.ICONS["Statistics"])
|
|
self.statistics.setProperty("style", "btn-primary")
|
|
self.statistics.clicked.connect(self.openStatisticsWindow)
|
|
|
|
self.buttonBox = QGroupBox("Misc")
|
|
self.buttonBoxLayout = QHBoxLayout()
|
|
self.buttonBoxLayout.addWidget(self.settings)
|
|
self.buttonBoxLayout.addWidget(self.statistics)
|
|
self.buttonBox.setLayout(self.buttonBoxLayout)
|
|
|
|
self.proceedBox = QGroupBox("Proceed")
|
|
self.proceedBoxLayout = QHBoxLayout()
|
|
self.proceedBoxLayout.addLayout(
|
|
MaxPlayerCount(self.game_model.ato_model))
|
|
self.proceedBoxLayout.addWidget(self.passTurnButton)
|
|
self.proceedBoxLayout.addWidget(self.proceedButton)
|
|
self.proceedBox.setLayout(self.proceedBoxLayout)
|
|
|
|
self.layout = QHBoxLayout()
|
|
self.layout.addWidget(self.factionsInfos)
|
|
self.layout.addWidget(self.turnCounter)
|
|
self.layout.addWidget(self.budgetBox)
|
|
self.layout.addWidget(self.buttonBox)
|
|
self.layout.addStretch(1)
|
|
self.layout.addWidget(self.proceedBox)
|
|
|
|
self.layout.setContentsMargins(0,0,0,0)
|
|
self.setLayout(self.layout)
|
|
|
|
def setGame(self, game: Optional[Game]):
|
|
if game is None:
|
|
return
|
|
|
|
self.turnCounter.setCurrentTurn(game.turn, game.conditions)
|
|
self.budgetBox.setGame(game)
|
|
self.factionsInfos.setGame(game)
|
|
|
|
if game and game.turn == 0:
|
|
self.proceedButton.setEnabled(False)
|
|
else:
|
|
self.proceedButton.setEnabled(True)
|
|
|
|
def openSettings(self):
|
|
self.subwindow = QSettingsWindow(self.game)
|
|
self.subwindow.show()
|
|
|
|
def openStatisticsWindow(self):
|
|
self.subwindow = QStatsWindow(self.game)
|
|
self.subwindow.show()
|
|
|
|
def passTurn(self):
|
|
self.game.pass_turn(no_action=True)
|
|
GameUpdateSignal.get_instance().updateGame(self.game)
|
|
self.proceedButton.setEnabled(True)
|
|
|
|
def negative_start_packages(self) -> List[Package]:
|
|
packages = []
|
|
for package in self.game_model.ato_model.ato.packages:
|
|
if not package.flights:
|
|
continue
|
|
estimator = TotEstimator(package)
|
|
for flight in package.flights:
|
|
if estimator.mission_start_time(flight).total_seconds() < 0:
|
|
packages.append(package)
|
|
break
|
|
return packages
|
|
|
|
@staticmethod
|
|
def fix_tots(packages: List[Package]) -> None:
|
|
for package in packages:
|
|
estimator = TotEstimator(package)
|
|
package.time_over_target = estimator.earliest_tot()
|
|
|
|
def ato_has_clients(self) -> bool:
|
|
for package in self.game.blue_ato.packages:
|
|
for flight in package.flights:
|
|
if flight.client_count > 0:
|
|
return True
|
|
return False
|
|
|
|
def confirm_no_client_launch(self) -> bool:
|
|
result = QMessageBox.question(
|
|
self,
|
|
"Continue without client slots?",
|
|
("No client slots have been created for players. Continuing will "
|
|
"allow the AI to perform the mission, but players will be unable "
|
|
"to participate.<br />"
|
|
"<br />"
|
|
"To add client slots for players, select a package from the "
|
|
"Packages panel on the left of the main window, and then a flight "
|
|
"from the Flights panel below the Packages panel. The edit button "
|
|
"below the Flights panel will allow you to edit the number of "
|
|
"client slots in the flight. Each client slot allows one player.<br />"
|
|
"<br />Click 'Yes' to continue with an AI only mission"
|
|
"<br />Click 'No' if you'd like to make more changes."),
|
|
QMessageBox.No,
|
|
QMessageBox.Yes
|
|
)
|
|
return result == QMessageBox.Yes
|
|
|
|
def confirm_negative_start_time(self,
|
|
negative_starts: List[Package]) -> bool:
|
|
formatted = '<br />'.join(
|
|
[f"{p.primary_task.name} {p.target.name}" for p in negative_starts]
|
|
)
|
|
mbox = QMessageBox(
|
|
QMessageBox.Question,
|
|
"Continue with past start times?",
|
|
("Some flights in the following packages have start times set "
|
|
"earlier than mission start time:<br />"
|
|
"<br />"
|
|
f"{formatted}<br />"
|
|
"<br />"
|
|
"Flight start times are estimated based on the package TOT, so it "
|
|
"is possible that not all flights will be able to reach the "
|
|
"target area at their assigned times.<br />"
|
|
"<br />"
|
|
"You can either continue with the mission as planned, with the "
|
|
"misplanned flights potentially flying too fast and/or missing "
|
|
"their rendezvous; automatically fix negative TOTs; or cancel "
|
|
"mission start and fix the packages manually."),
|
|
parent=self
|
|
)
|
|
auto = mbox.addButton("Fix TOTs automatically", QMessageBox.ActionRole)
|
|
ignore = mbox.addButton("Continue without fixing",
|
|
QMessageBox.DestructiveRole)
|
|
cancel = mbox.addButton(QMessageBox.Cancel)
|
|
mbox.setEscapeButton(cancel)
|
|
mbox.exec_()
|
|
clicked = mbox.clickedButton()
|
|
if clicked == auto:
|
|
self.fix_tots(negative_starts)
|
|
return True
|
|
elif clicked == ignore:
|
|
return True
|
|
return False
|
|
|
|
def launch_mission(self):
|
|
"""Finishes planning and waits for mission completion."""
|
|
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
|
return
|
|
|
|
negative_starts = self.negative_start_packages()
|
|
if negative_starts:
|
|
if not self.confirm_negative_start_time(negative_starts):
|
|
return
|
|
|
|
# TODO: Refactor this nonsense.
|
|
game_event = None
|
|
for event in self.game.events:
|
|
if isinstance(event,
|
|
FrontlineAttackEvent) and event.is_player_attacking:
|
|
game_event = event
|
|
if game_event is None:
|
|
game_event = FrontlineAttackEvent(
|
|
self.game,
|
|
self.game.theater.controlpoints[0],
|
|
self.game.theater.controlpoints[0],
|
|
self.game.theater.controlpoints[0].position,
|
|
self.game.player_name,
|
|
self.game.enemy_name)
|
|
game_event.is_awacs_enabled = True
|
|
game_event.ca_slots = 1
|
|
game_event.departure_cp = self.game.theater.controlpoints[0]
|
|
game_event.player_attacking({CAS: {}, CAP: {}})
|
|
game_event.depart_from = self.game.theater.controlpoints[0]
|
|
|
|
self.game.initiate_event(game_event)
|
|
waiting = QWaitingForMissionResultWindow(game_event, self.game)
|
|
waiting.show()
|
|
|
|
def budget_update(self, game:Game):
|
|
self.budgetBox.setGame(game)
|