Merge 'upstream/develop' into new-plugin-system

This commit is contained in:
David Pierron
2020-10-20 22:10:08 +02:00
47 changed files with 783 additions and 415 deletions

View File

@@ -164,6 +164,7 @@ class PackageModel(QAbstractListModel):
def update_tot(self, tot: int) -> None:
self.package.time_over_target = tot
self.layoutChanged.emit()
@property
def mission_target(self) -> MissionTarget:
@@ -234,7 +235,7 @@ class AtoModel(QAbstractListModel):
"""Returns the package at the given index."""
return self.ato.packages[index.row()]
def replace_from_game(self, game: Optional[Game]) -> None:
def replace_from_game(self, game: Optional[Game], player: bool) -> None:
"""Updates the ATO object to match the updated game object.
If the game is None (as is the case when no game has been loaded), an
@@ -244,7 +245,10 @@ class AtoModel(QAbstractListModel):
self.game = game
self.package_models.clear()
if self.game is not None:
self.ato = game.blue_ato
if player:
self.ato = game.blue_ato
else:
self.ato = game.red_ato
else:
self.ato = AirTaskingOrder()
self.endResetModel()
@@ -268,8 +272,8 @@ class GameModel:
"""
def __init__(self) -> None:
self.game: Optional[Game] = None
# TODO: Add red ATO model, add cheat option to show red flight plan.
self.ato_model = AtoModel(self.game, AirTaskingOrder())
self.red_ato_model = AtoModel(self.game, AirTaskingOrder())
def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object.
@@ -280,4 +284,5 @@ class GameModel:
loaded.
"""
self.game = game
self.ato_model.replace_from_game(self.game)
self.ato_model.replace_from_game(self.game, player=True)
self.red_ato_model.replace_from_game(self.game, player=False)

View File

@@ -6,7 +6,7 @@ from PySide2.QtGui import QColor, QFont, QPixmap
from theater.theatergroundobject import CATEGORY_MAP
from .liberation_theme import get_theme_icons
VERSION_STRING = "2.1.4"
VERSION_STRING = "2.2.0-preview"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",
@@ -40,6 +40,7 @@ COLORS: Dict[str, QColor] = {
"light_blue": QColor(105, 182, 240, 90),
"blue": QColor(0, 132, 255),
"dark_blue": QColor(45, 62, 80),
"sea_blue": QColor(52, 68, 85),
"blue_transparent": QColor(0, 132, 255, 20),
"purple": QColor(187, 137, 255),

View File

@@ -1,4 +1,4 @@
from typing import Optional
from typing import List, Optional
from PySide2.QtWidgets import (
QFrame,
@@ -11,6 +11,8 @@ from PySide2.QtWidgets import (
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
@@ -95,7 +97,7 @@ class QTopPanel(QFrame):
if game is None:
return
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
self.turnCounter.setCurrentTurn(game.turn, game.conditions)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
@@ -117,6 +119,24 @@ class QTopPanel(QFrame):
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) < 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:
@@ -142,12 +162,52 @@ class QTopPanel(QFrame):
)
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
# TODO: Verify no negative start times.
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

View File

@@ -1,7 +1,8 @@
import datetime
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
from game.weather import Conditions, TimeOfDay
import qt_ui.uiconstants as CONST
@@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
def __init__(self):
super(QTurnCounter, self).__init__("Turn")
self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]]
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[0])
self.turn_info = QLabel()
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout()
self.layout.addWidget(self.daytime_icon)
self.layout.addWidget(self.turn_info)
self.setLayout(self.layout)
def setCurrentTurn(self, turn: int, current_day: datetime):
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.daytime_icon)
self.time_column = QVBoxLayout()
self.layout.addLayout(self.time_column)
self.date_display = QLabel()
self.time_column.addWidget(self.date_display)
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
Set the money amount to display
:arg turn Current turn number
:arg current_day Current day
"""
self.daytime_icon.setPixmap(self.icons[turn % 4])
self.turn_info.setText(current_day.strftime("%d %b %Y"))
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
self.time_display.setText(
conditions.start_time.strftime("%H:%M:%S Local"))
self.setTitle("Turn " + str(turn + 1))

View File

@@ -19,7 +19,6 @@ class QFlightTypeComboBox(QComboBox):
COMMON_ENEMY_MISSIONS = [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.SEAD,
FlightType.DEAD,
# TODO: FlightType.ELINT,
@@ -27,42 +26,46 @@ class QFlightTypeComboBox(QComboBox):
# TODO: FlightType.RECON,
]
FRIENDLY_AIRBASE_MISSIONS = [
FlightType.CAP,
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
COMMON_FRIENDLY_MISSIONS = [
FlightType.BARCAP,
]
FRIENDLY_AIRBASE_MISSIONS = [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
] + COMMON_FRIENDLY_MISSIONS
FRIENDLY_CARRIER_MISSIONS = [
FlightType.BARCAP,
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
]
] + COMMON_FRIENDLY_MISSIONS
ENEMY_CARRIER_MISSIONS = [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.BARCAP,
# TODO: FlightType.ANTISHIP
]
ENEMY_AIRBASE_MISSIONS = [
FlightType.BARCAP,
# TODO: FlightType.STRIKE
] + COMMON_ENEMY_MISSIONS
FRIENDLY_GROUND_OBJECT_MISSIONS = [
FlightType.CAP,
# TODO: FlightType.LOGISTICS
# TODO: FlightType.TROOP_TRANSPORT
]
] + COMMON_FRIENDLY_MISSIONS
ENEMY_GROUND_OBJECT_MISSIONS = [
FlightType.BARCAP,
FlightType.STRIKE,
] + COMMON_ENEMY_MISSIONS
FRONT_LINE_MISSIONS = [
FlightType.CAS,
FlightType.TARCAP,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
] + COMMON_ENEMY_MISSIONS

View File

@@ -21,6 +21,7 @@ from game import Game, db
from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
@@ -55,25 +56,42 @@ class QLiberationMap(QGraphicsView):
self.factor = 1
self.factorized = 1
self.init_scene()
self.connectSignals()
self.setGame(game_model.game)
GameUpdateSignal.get_instance().flight_paths_changed.connect(
lambda: self.draw_flight_plans(self.scene())
)
def update_package_selection(index: Optional[int]) -> None:
self.selected_flight = index, 0
def update_package_selection(index: int) -> None:
# Optional[int] isn't a valid type for a Qt signal. None will be
# converted to zero automatically. We use -1 to indicate no
# selection.
if index == -1:
self.selected_flight = None
else:
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:
def update_flight_selection(index: int) -> None:
if self.selected_flight is None:
logging.error("Flight was selected with no package selected")
if index != -1:
# We don't know what order update_package_selection and
# update_flight_selection will be called in when the last
# package is removed. If no flight is selected, it's not a
# problem to also have no package selected.
logging.error(
"Flight was selected with no package selected")
return
# Optional[int] isn't a valid type for a Qt signal. None will be
# converted to zero automatically. We use -1 to indicate no
# selection.
if index == -1:
self.selected_flight = self.selected_flight[0], None
self.selected_flight = self.selected_flight[0], index
self.draw_flight_plans(self.scene())
@@ -90,9 +108,6 @@ class QLiberationMap(QGraphicsView):
self.setFrameShape(QFrame.NoFrame)
self.setDragMode(QGraphicsView.ScrollHandDrag)
def connectSignals(self):
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
def setGame(self, game: Optional[Game]):
self.game = game
logging.debug("Reloading Map Canvas")
@@ -244,7 +259,7 @@ class QLiberationMap(QGraphicsView):
text.setDefaultTextColor(Qt.white)
text.setPos(pos[0] + CONST.CP_SIZE + 1, pos[1] - CONST.CP_SIZE / 2 + 1)
def draw_flight_plans(self, scene) -> None:
def clear_flight_paths(self, scene: QGraphicsScene) -> None:
for item in self.flight_path_items:
try:
scene.removeItem(item)
@@ -252,12 +267,20 @@ class QLiberationMap(QGraphicsView):
# Something may have caused those items to already be removed.
pass
self.flight_path_items.clear()
def draw_flight_plans(self, scene: QGraphicsScene) -> None:
self.clear_flight_paths(scene)
if DisplayOptions.flight_paths.hide:
return
packages = list(self.game_model.ato_model.packages)
if self.game.settings.show_red_ato:
packages.extend(self.game_model.red_ato_model.packages)
for p_idx, package_model in enumerate(packages):
for f_idx, flight in enumerate(package_model.flights):
selected = (p_idx, f_idx) == self.selected_flight
if self.selected_flight is None:
selected = False
else:
selected = (p_idx, f_idx) == self.selected_flight
if DisplayOptions.flight_paths.only_selected and not selected:
continue
self.draw_flight_plan(scene, package_model.package, flight,
@@ -486,9 +509,9 @@ class QLiberationMap(QGraphicsView):
scene.addPixmap(bg)
# Apply graphical effects to simulate current daytime
if self.game.current_turn_daytime == "day":
if self.game.current_turn_time_of_day == TimeOfDay.Day:
pass
elif self.game.current_turn_daytime == "night":
elif self.game.current_turn_time_of_day == TimeOfDay.Night:
ov = QPixmap(bg.width(), bg.height())
ov.fill(CONST.COLORS["night_overlay"])
overlay = scene.addPixmap(ov)
@@ -507,6 +530,11 @@ class QLiberationMap(QGraphicsView):
# Polygon display mode
if self.game.theater.landmap is not None:
for sea_zone in self.game.theater.landmap[2]:
print(sea_zone)
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone])
scene.addPolygon(poly, CONST.COLORS["sea_blue"], CONST.COLORS["sea_blue"])
for inclusion_zone in self.game.theater.landmap[0]:
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone])
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
@@ -516,3 +544,5 @@ class QLiberationMap(QGraphicsView):
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])

View File

@@ -7,6 +7,7 @@ from PySide2.QtWidgets import QGraphicsItem
import qt_ui.uiconstants as const
from game import Game
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import REWARDS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import ControlPoint, TheaterGroundObject
from .QMapObject import QMapObject
@@ -27,7 +28,14 @@ class QMapGroundObject(QMapObject):
self.buildings = buildings if buildings is not None else []
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
self.ground_object_dialog: Optional[QGroundObjectMenu] = None
self.setToolTip(self.tooltip)
@property
def tooltip(self) -> str:
lines = [
f"[{self.ground_object.obj_name}]",
f"${self.production_per_turn} per turn",
]
if self.ground_object.groups:
units = {}
for g in self.ground_object.groups:
@@ -36,16 +44,23 @@ class QMapGroundObject(QMapObject):
units[u.type] = units[u.type]+1
else:
units[u.type] = 1
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
for unit in units.keys():
tooltip = tooltip + str(unit) + "x" + str(units[unit]) + "\n"
self.setToolTip(tooltip[:-1])
lines.append(f"{unit} x {units[unit]}")
else:
tooltip = "[" + self.ground_object.obj_name + "]" + "\n"
for building in buildings:
for building in self.buildings:
if not building.is_dead:
tooltip = tooltip + str(building.dcs_identifier) + "\n"
self.setToolTip(tooltip[:-1])
lines.append(f"{building.dcs_identifier}")
return "\n".join(lines)
@property
def production_per_turn(self) -> int:
production = 0
for g in self.control_point.ground_objects:
if g.category in REWARDS.keys():
production += REWARDS[g.category]
return production
def paint(self, painter, option, widget=None) -> None:
player_icons = "_blue"

View File

@@ -24,8 +24,8 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(DebriefingSignal)
flight_paths_changed = Signal()
package_selection_changed = Signal(int) # Optional[int]
flight_selection_changed = Signal(int) # Optional[int]
package_selection_changed = Signal(int) # -1 indicates no selection.
flight_selection_changed = Signal(int) # -1 indicates no selection.
def __init__(self):
super(GameUpdateSignal, self).__init__()
@@ -33,11 +33,11 @@ class GameUpdateSignal(QObject):
def select_package(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.package_selection_changed.emit(index)
self.package_selection_changed.emit(-1 if index is None else index)
def select_flight(self, index: Optional[int]) -> None:
# noinspection PyUnresolvedReferences
self.flight_selection_changed.emit(index)
self.flight_selection_changed.emit(-1 if index is None else index)
def redraw_flight_paths(self) -> None:
# noinspection PyUnresolvedReferences

View File

@@ -43,6 +43,7 @@ class QLiberationWindow(QMainWindow):
Dialog.set_game(self.game_model)
self.ato_panel = None
self.info_panel = None
self.liberation_map = None
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100)
@@ -224,9 +225,11 @@ class QLiberationWindow(QMainWindow):
if game is not None:
game.on_load()
self.game = game
if self.info_panel:
if self.info_panel is not None:
self.info_panel.setGame(game)
self.game_model.set(self.game)
if self.liberation_map is not None:
self.liberation_map.setGame(game)
def showAboutDialog(self):
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \

View File

@@ -1,15 +1,16 @@
from typing import Optional
from typing import Optional, Set
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (
QFrame,
QGridLayout,
QScrollArea,
QVBoxLayout,
QHBoxLayout,
QLabel,
QScrollArea,
QVBoxLayout,
QWidget,
)
from dcs.unittype import UnitType
from game.event.event import UnitsDeliveryEvent
from qt_ui.models import GameModel
@@ -48,26 +49,27 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
def init_ui(self):
main_layout = QVBoxLayout()
units = {
CAP: db.find_unittype(CAP, self.game_model.game.player_name),
CAS: db.find_unittype(CAS, self.game_model.game.player_name),
}
tasks = [CAP, CAS]
scroll_content = QWidget()
task_box_layout = QGridLayout()
row = 0
for task_type in units.keys():
units_column = list(set(units[task_type]))
if len(units_column) == 0:
unit_types: Set[UnitType] = set()
for task in tasks:
units = db.find_unittype(task, self.game_model.game.player_name)
if not units:
continue
units_column.sort(key=lambda x: db.PRICES[x])
for unit_type in units_column:
if self.cp.is_carrier and not unit_type in db.CARRIER_CAPABLE:
for unit in units:
if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE:
continue
if self.cp.is_lha and not unit_type in db.LHA_CAPABLE:
if self.cp.is_lha and unit not in db.LHA_CAPABLE:
continue
row = self.add_purchase_row(unit_type, task_box_layout, row)
unit_types.add(unit)
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
for unit_type in sorted_units:
row = self.add_purchase_row(unit_type, task_box_layout, row)
stretch = QVBoxLayout()
stretch.addStretch()
task_box_layout.addLayout(stretch, row, 0)

View File

@@ -16,6 +16,7 @@ from game.game import Game
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
from qt_ui.models import AtoModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.ato import QFlightList
@@ -34,12 +35,6 @@ class QPackageDialog(QDialog):
#: Emitted when a change is made to the package.
package_changed = Signal()
#: Emitted when a flight is added to the package.
flight_added = Signal(Flight)
#: Emitted when a flight is removed from the package.
flight_removed = Signal(Flight)
def __init__(self, game: Game, model: PackageModel) -> None:
super().__init__()
self.game = game
@@ -77,20 +72,20 @@ class QPackageDialog(QDialog):
self.tot_label = QLabel("Time Over Target:")
self.tot_column.addWidget(self.tot_label)
if self.package_model.package.time_over_target is None:
time = None
else:
delay = self.package_model.package.time_over_target
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
time = QTime(hours, minutes, seconds)
self.tot_spinner = QTimeEdit(time)
self.tot_spinner = QTimeEdit(self.tot_qtime())
self.tot_spinner.setMinimumTime(QTime(0, 0))
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
self.tot_spinner.timeChanged.connect(self.save_tot)
self.tot_column.addWidget(self.tot_spinner)
self.reset_tot_button = QPushButton("Reset TOT")
self.reset_tot_button.setToolTip(
"Sets the package TOT to the earliest time that all flights can "
"arrive at the target."
)
self.reset_tot_button.clicked.connect(self.reset_tot)
self.tot_column.addWidget(self.reset_tot_button)
self.package_view = QFlightList(self.package_model)
self.package_view.selectionModel().selectionChanged.connect(
self.on_selection_changed
@@ -114,17 +109,40 @@ class QPackageDialog(QDialog):
self.setLayout(self.layout)
self.accepted.connect(self.on_save)
self.finished.connect(self.on_close)
self.rejected.connect(self.on_cancel)
def tot_qtime(self) -> QTime:
delay = self.package_model.package.time_over_target
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
return QTime(hours, minutes, seconds)
def on_cancel(self) -> None:
pass
@staticmethod
def on_close(_result) -> None:
GameUpdateSignal.get_instance().redraw_flight_paths()
def on_save(self) -> None:
self.save_tot()
def save_tot(self) -> None:
time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(seconds)
def reset_tot(self) -> None:
if not list(self.package_model.flights):
self.package_model.update_tot(0)
else:
self.package_model.update_tot(
TotEstimator(self.package_model.package).earliest_tot())
self.tot_spinner.setTime(self.tot_qtime())
def on_selection_changed(self, selected: QItemSelection,
_deselected: QItemSelection) -> None:
"""Updates the state of the delete button."""
@@ -139,14 +157,13 @@ class QPackageDialog(QDialog):
def add_flight(self, flight: Flight) -> None:
"""Adds the new flight to the package."""
self.game.aircraft_inventory.claim_for_flight(flight)
self.package_model.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package_model.package,
is_player=True)
planner.populate_flight_plan(flight)
# noinspection PyUnresolvedReferences
self.package_changed.emit()
# noinspection PyUnresolvedReferences
self.flight_added.emit(flight)
def on_delete_flight(self) -> None:
"""Removes the selected flight from the package."""
@@ -154,11 +171,10 @@ class QPackageDialog(QDialog):
if flight is None:
logging.error(f"Cannot delete flight when no flight is selected.")
return
self.game.aircraft_inventory.return_from_flight(flight)
self.package_model.delete_flight(flight)
# noinspection PyUnresolvedReferences
self.package_changed.emit()
# noinspection PyUnresolvedReferences
self.flight_removed.emit(flight)
class QNewPackageDialog(QPackageDialog):
@@ -174,22 +190,22 @@ class QNewPackageDialog(QPackageDialog):
self.save_button = QPushButton("Save")
self.save_button.setProperty("style", "start-button")
self.save_button.clicked.connect(self.on_save)
self.save_button.clicked.connect(self.accept)
self.button_layout.addWidget(self.save_button)
self.delete_flight_button.clicked.connect(self.on_delete_flight)
def on_save(self) -> None:
"""Saves the created package.
Empty packages may be created. They can be modified later, and will have
no effect if empty when the mission is generated.
"""
self.save_tot()
super().on_save()
self.ato_model.add_package(self.package_model.package)
def on_cancel(self) -> None:
super().on_cancel()
for flight in self.package_model.package.flights:
self.game.aircraft_inventory.claim_for_flight(flight)
self.close()
self.game_model.game.aircraft_inventory.return_from_flight(flight)
class QEditPackageDialog(QPackageDialog):
@@ -210,30 +226,9 @@ class QEditPackageDialog(QPackageDialog):
self.done_button = QPushButton("Done")
self.done_button.setProperty("style", "start-button")
self.done_button.clicked.connect(self.on_done)
self.done_button.clicked.connect(self.accept)
self.button_layout.addWidget(self.done_button)
# noinspection PyUnresolvedReferences
self.flight_added.connect(self.on_flight_added)
# noinspection PyUnresolvedReferences
self.flight_removed.connect(self.on_flight_removed)
# TODO: Make the new package dialog do this too, return on cancel.
# Not claiming the aircraft when they are added to the planner means that
# inventory counts are not updated until after the new package is updated,
# so you can add an infinite number of aircraft to a new package in the UI,
# which will crash when the flight package is saved.
def on_flight_added(self, flight: Flight) -> None:
self.game.aircraft_inventory.claim_for_flight(flight)
def on_flight_removed(self, flight: Flight) -> None:
self.game.aircraft_inventory.return_from_flight(flight)
def on_done(self) -> None:
"""Closes the window."""
self.save_tot()
self.close()
def on_delete(self) -> None:
"""Removes the viewed package from the ATO."""
# The ATO model returns inventory for us when deleting a package.

View File

@@ -19,11 +19,15 @@ class QFlightPlanner(QTabWidget):
def __init__(self, package: Package, flight: Flight, game: Game):
super().__init__()
self.general_settings_tab = QGeneralFlightSettingsTab(game, flight)
self.general_settings_tab = QGeneralFlightSettingsTab(
game, package, flight
)
# noinspection PyUnresolvedReferences
self.general_settings_tab.on_flight_settings_changed.connect(
lambda: self.on_planned_flight_changed.emit())
self.payload_tab = QFlightPayloadTab(flight, game)
self.waypoint_tab = QFlightWaypointTab(game, package, flight)
# noinspection PyUnresolvedReferences
self.waypoint_tab.on_flight_changed.connect(
lambda: self.on_planned_flight_changed.emit())
self.addTab(self.general_settings_tab, "General Flight settings")

View File

@@ -0,0 +1,32 @@
import datetime
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.traveltime import TotEstimator
# TODO: Remove?
class QFlightDepartureDisplay(QGroupBox):
def __init__(self, package: Package, flight: Flight):
super().__init__("Departure")
layout = QVBoxLayout()
departure_row = QHBoxLayout()
layout.addLayout(departure_row)
estimator = TotEstimator(package)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
departure_row.addWidget(QLabel(
f"Departing from <b>{flight.from_cp.name}</b>"
))
departure_row.addWidget(QLabel(f"At T+{delay}"))
layout.addWidget(QLabel("Determined based on the package TOT. Edit the "
"package to adjust the TOT."))
self.setLayout(layout)

View File

@@ -1,31 +0,0 @@
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox
# TODO: Remove?
class QFlightDepartureEditor(QGroupBox):
def __init__(self, flight):
super(QFlightDepartureEditor, self).__init__("Departure")
self.flight = flight
layout = QHBoxLayout()
self.depart_from = QLabel("Departing from <b>" + self.flight.from_cp.name + "</b>")
self.depart_at_t = QLabel("At T +")
self.minutes = QLabel(" minutes")
self.departure_delta = QSpinBox(self)
self.departure_delta.setMinimum(0)
self.departure_delta.setMaximum(120)
self.departure_delta.setValue(self.flight.scheduled_in // 60)
self.departure_delta.valueChanged.connect(self.change_scheduled)
layout.addWidget(self.depart_from)
layout.addWidget(self.depart_at_t)
layout.addWidget(self.departure_delta)
layout.addWidget(self.minutes)
self.setLayout(layout)
self.changed = self.departure_delta.valueChanged
def change_scheduled(self):
self.flight.scheduled_in = int(self.departure_delta.value() * 60)

View File

@@ -2,26 +2,29 @@ from PySide2.QtCore import Signal
from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout
from game import Game
from gen.ato import Package
from gen.flights.flight import Flight
from qt_ui.windows.mission.flight.settings.QFlightDepartureEditor import QFlightDepartureEditor
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import QFlightTypeTaskInfo
from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \
QFlightDepartureDisplay
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \
QFlightSlotEditor
from qt_ui.windows.mission.flight.settings.QFlightStartType import \
QFlightStartType
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \
QFlightTypeTaskInfo
class QGeneralFlightSettingsTab(QFrame):
on_flight_settings_changed = Signal()
def __init__(self, game: Game, flight: Flight):
super(QGeneralFlightSettingsTab, self).__init__()
self.flight = flight
self.game = game
def __init__(self, game: Game, package: Package, flight: Flight):
super().__init__()
layout = QGridLayout()
flight_info = QFlightTypeTaskInfo(self.flight)
flight_departure = QFlightDepartureEditor(self.flight)
flight_slots = QFlightSlotEditor(self.flight, self.game)
flight_start_type = QFlightStartType(self.flight)
flight_info = QFlightTypeTaskInfo(flight)
flight_departure = QFlightDepartureDisplay(package, flight)
flight_slots = QFlightSlotEditor(flight, game)
flight_start_type = QFlightStartType(flight)
layout.addWidget(flight_info, 0, 0)
layout.addWidget(flight_departure, 1, 0)
layout.addWidget(flight_slots, 2, 0)
@@ -31,8 +34,6 @@ class QGeneralFlightSettingsTab(QFrame):
layout.addLayout(vstretch, 3, 0)
self.setLayout(layout)
flight_start_type.setEnabled(self.flight.client_count > 0)
flight_start_type.setEnabled(flight.client_count > 0)
flight_slots.changed.connect(
lambda: flight_start_type.setEnabled(self.flight.client_count > 0))
flight_departure.changed.connect(
lambda: self.on_flight_settings_changed.emit())
lambda: flight_start_type.setEnabled(flight.client_count > 0))

View File

@@ -54,6 +54,7 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
# TODO: Filter by objective type.
self.recreate_buttons.clear()
recreate_types = [
FlightType.CAS,
@@ -137,13 +138,16 @@ class QFlightWaypointTab(QFrame):
QMessageBox.Yes
)
if result == QMessageBox.Yes:
# TODO: These should all be just CAP.
# TODO: Should be buttons for both BARCAP and TARCAP.
# BARCAP and TARCAP behave differently. TARCAP arrives a few minutes
# ahead of the rest of the package and stays until the package
# departs, whereas BARCAP usually isn't part of a strike package and
# has a fixed mission time.
if task == FlightType.CAP:
if isinstance(self.package.target, FrontLine):
task = FlightType.TARCAP
elif isinstance(self.package.target, ControlPoint):
if self.package.target.is_fleet:
task = FlightType.BARCAP
task = FlightType.BARCAP
self.flight.flight_type = task
self.planner.populate_flight_plan(self.flight)
self.flight_waypoint_list.update_list()

View File

@@ -1,18 +1,50 @@
import logging
from typing import Callable
from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint
from PySide2.QtGui import QStandardItemModel, QStandardItem
from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \
QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox
from PySide2.QtWidgets import (
QLabel,
QDialog,
QGridLayout,
QListView,
QStackedLayout,
QComboBox,
QWidget,
QAbstractItemView,
QPushButton,
QGroupBox,
QCheckBox,
QVBoxLayout,
QSpinBox,
)
from dcs.forcedoptions import ForcedOptions
import qt_ui.uiconstants as CONST
from game.game import Game
from game.infos.information import Information
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
from plugin import LuaPluginManager
class CheatSettingsBox(QGroupBox):
def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None:
super().__init__("Cheat Settings")
self.main_layout = QVBoxLayout()
self.setLayout(self.main_layout)
self.red_ato_checkbox = QCheckBox()
self.red_ato_checkbox.setChecked(game.settings.show_red_ato)
self.red_ato_checkbox.toggled.connect(apply_settings)
self.red_ato = QLabeledWidget("Show Red ATO:", self.red_ato_checkbox)
self.main_layout.addLayout(self.red_ato)
@property
def show_red_ato(self) -> bool:
return self.red_ato_checkbox.isChecked()
class QSettingsWindow(QDialog):
def __init__(self, game: Game):
@@ -262,9 +294,12 @@ class QSettingsWindow(QDialog):
def initCheatLayout(self):
self.cheatPage = QWidget()
self.cheatLayout = QGridLayout()
self.cheatLayout = QVBoxLayout()
self.cheatPage.setLayout(self.cheatLayout)
self.cheat_options = CheatSettingsBox(self.game, self.applySettings)
self.cheatLayout.addWidget(self.cheat_options)
self.moneyCheatBox = QGroupBox("Money Cheat")
self.moneyCheatBox.setAlignment(Qt.AlignTop)
self.moneyCheatBoxLayout = QGridLayout()
@@ -280,7 +315,7 @@ class QSettingsWindow(QDialog):
btn.setProperty("style", "btn-danger")
btn.clicked.connect(self.cheatLambda(amount))
self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0)
self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1)
def initPluginsLayout(self):
uiPrepared = False
@@ -332,8 +367,6 @@ class QSettingsWindow(QDialog):
self.game.settings.external_views_allowed = self.ext_views.isChecked()
self.game.settings.generate_marks = self.generate_marks.isChecked()
print(self.game.settings.map_coalition_visibility)
self.game.settings.supercarrier = self.supercarrier.isChecked()
self.game.settings.perf_red_alert_state = self.red_alert.isChecked()
@@ -347,6 +380,8 @@ class QSettingsWindow(QDialog):
self.game.settings.perf_culling = self.culling.isChecked()
self.game.settings.perf_culling_distance = int(self.culling_distance.value())
self.game.settings.show_red_ato = self.cheat_options.show_red_ato
GameUpdateSignal.get_instance().updateGame(self.game)
def onSelectionChanged(self):