mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into helipads
# Conflicts: # game/game.py # game/operation/operation.py # game/theater/conflicttheater.py # game/theater/controlpoint.py # gen/groundobjectsgen.py # resources/campaigns/golan_heights_lite.miz
This commit is contained in:
@@ -112,7 +112,7 @@ def replace_mission_scripting_file():
|
||||
)
|
||||
liberation_scripting_path = "./resources/scripts/MissionScripting.lua"
|
||||
backup_scripting_path = "./resources/scripts/MissionScripting.original.lua"
|
||||
if os.path.isfile(mission_scripting_path):
|
||||
if install_dir != "" and os.path.isfile(mission_scripting_path):
|
||||
with open(mission_scripting_path, "r") as ms:
|
||||
current_file_content = ms.read()
|
||||
with open(liberation_scripting_path, "r") as libe_ms:
|
||||
@@ -133,5 +133,9 @@ def restore_original_mission_scripting():
|
||||
)
|
||||
backup_scripting_path = "./resources/scripts/MissionScripting.original.lua"
|
||||
|
||||
if os.path.isfile(backup_scripting_path) and os.path.isfile(mission_scripting_path):
|
||||
if (
|
||||
install_dir != ""
|
||||
and os.path.isfile(backup_scripting_path)
|
||||
and os.path.isfile(mission_scripting_path)
|
||||
):
|
||||
copyfile(backup_scripting_path, mission_scripting_path)
|
||||
|
||||
@@ -3,6 +3,8 @@ import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from qt_ui.logging_handler import HookableInMemoryHandler
|
||||
|
||||
|
||||
def init_logging(version: str) -> None:
|
||||
"""Initializes the logging configuration."""
|
||||
@@ -10,13 +12,22 @@ def init_logging(version: str) -> None:
|
||||
os.mkdir("logs")
|
||||
|
||||
fmt = "%(asctime)s :: %(levelname)s :: %(message)s"
|
||||
formatter = logging.Formatter(fmt)
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG, format=fmt)
|
||||
logger = logging.getLogger()
|
||||
|
||||
handler = RotatingFileHandler("./logs/liberation.log", "a", 5000000, 1)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
handler.setFormatter(logging.Formatter(fmt))
|
||||
rotating_file_handler = RotatingFileHandler(
|
||||
"./logs/liberation.log", "a", 5000000, 1
|
||||
)
|
||||
rotating_file_handler.setLevel(logging.DEBUG)
|
||||
rotating_file_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(handler)
|
||||
hookable_in_memory_handler = HookableInMemoryHandler()
|
||||
hookable_in_memory_handler.setLevel(logging.DEBUG)
|
||||
hookable_in_memory_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(rotating_file_handler)
|
||||
logger.addHandler(hookable_in_memory_handler)
|
||||
|
||||
logger.info(f"DCS Liberation {version}")
|
||||
|
||||
38
qt_ui/logging_handler.py
Normal file
38
qt_ui/logging_handler.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
LogHook = typing.Callable[[str], None]
|
||||
|
||||
|
||||
class HookableInMemoryHandler(logging.Handler):
|
||||
"""Hookable in-memory logging handler for logs window"""
|
||||
|
||||
_log: str
|
||||
_hook: typing.Optional[typing.Callable[[str], None]]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HookableInMemoryHandler, self).__init__(*args, **kwargs)
|
||||
self._log = ""
|
||||
self._hook = None
|
||||
|
||||
@property
|
||||
def log(self) -> str:
|
||||
return self._log
|
||||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
self._log += msg + "\n"
|
||||
if self._hook is not None:
|
||||
self._hook(msg)
|
||||
|
||||
def write(self, m):
|
||||
pass
|
||||
|
||||
def clearLog(self) -> None:
|
||||
self._log = ""
|
||||
|
||||
def setHook(self, hook: typing.Callable[[str], None]) -> None:
|
||||
self._hook = hook
|
||||
|
||||
def clearHook(self) -> None:
|
||||
self._hook = None
|
||||
@@ -7,18 +7,15 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
from dcs.payloads import PayloadDirectories
|
||||
from dcs.weapons_data import weapon_ids
|
||||
|
||||
from game import Game, VERSION, persistency
|
||||
from game.data.weapons import (
|
||||
WEAPON_FALLBACK_MAP,
|
||||
WEAPON_INTRODUCTION_YEARS,
|
||||
Weapon,
|
||||
)
|
||||
from game.data.weapons import WeaponGroup, Pylon, Weapon
|
||||
from game.db import FACTIONS
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.profiling import logged_duration
|
||||
from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSettings
|
||||
@@ -62,6 +59,8 @@ def run_ui(game: Optional[Game]) -> None:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
app.setAttribute(Qt.AA_DisableWindowContextHelpButton)
|
||||
|
||||
# init the theme and load the stylesheet based on the theme index
|
||||
liberation_theme.init()
|
||||
with open(
|
||||
@@ -97,6 +96,22 @@ def run_ui(game: Optional[Game]) -> None:
|
||||
uiconstants.load_aircraft_banners()
|
||||
uiconstants.load_vehicle_banners()
|
||||
|
||||
# Show warning if no DCS Installation directory was set
|
||||
if liberation_install.get_dcs_install_directory() == "":
|
||||
QtWidgets.QMessageBox.warning(
|
||||
splash,
|
||||
"No DCS installation directory.",
|
||||
"The DCS Installation directory is not set correctly. "
|
||||
"This will prevent DCS Liberation to work properly as the MissionScripting "
|
||||
"file will not be modified."
|
||||
"<br/><br/>To solve this problem, you can set the Installation directory "
|
||||
"within the preferences menu. You can also manually edit or replace the "
|
||||
"following file:"
|
||||
"<br/><br/><strong><dcs_installation_directory>/Scripts/MissionScripting.lua</strong>"
|
||||
"<br/><br/>The easiest way to do it is to replace the original file with the file in dcs-liberation distribution (<dcs_liberation_installation>/resources/scripts/MissionScripting.lua)."
|
||||
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>",
|
||||
QtWidgets.QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
# Replace DCS Mission scripting file to allow DCS Liberation to work
|
||||
try:
|
||||
liberation_install.replace_mission_scripting_file()
|
||||
@@ -169,8 +184,24 @@ def parse_args() -> argparse.Namespace:
|
||||
"--inverted", action="store_true", help="Invert the campaign."
|
||||
)
|
||||
|
||||
new_game.add_argument(
|
||||
"--date",
|
||||
type=datetime.fromisoformat,
|
||||
default=datetime.today(),
|
||||
help="Start date of the campaign.",
|
||||
)
|
||||
|
||||
new_game.add_argument(
|
||||
"--restrict-weapons-by-date",
|
||||
action="store_true",
|
||||
help="Enable campaign date restricted weapons.",
|
||||
)
|
||||
|
||||
new_game.add_argument("--cheats", action="store_true", help="Enable cheats.")
|
||||
|
||||
lint_weapons = subparsers.add_parser("lint-weapons")
|
||||
lint_weapons.add_argument("aircraft", help="Name of the aircraft variant to lint.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -182,6 +213,8 @@ def create_game(
|
||||
auto_procurement: bool,
|
||||
inverted: bool,
|
||||
cheats: bool,
|
||||
start_date: datetime,
|
||||
restrict_weapons_by_date: bool,
|
||||
) -> Game:
|
||||
first_start = liberation_install.init()
|
||||
if first_start:
|
||||
@@ -210,9 +243,10 @@ def create_game(
|
||||
automate_aircraft_reinforcements=auto_procurement,
|
||||
enable_frontline_cheats=cheats,
|
||||
enable_base_capture_cheat=cheats,
|
||||
restrict_weapons_by_date=restrict_weapons_by_date,
|
||||
),
|
||||
GeneratorSettings(
|
||||
start_date=datetime.today(),
|
||||
start_date=start_date,
|
||||
player_budget=DEFAULT_BUDGET,
|
||||
enemy_budget=DEFAULT_BUDGET,
|
||||
midgame=False,
|
||||
@@ -232,16 +266,24 @@ def create_game(
|
||||
high_digit_sams=False,
|
||||
),
|
||||
)
|
||||
return generator.generate()
|
||||
game = generator.generate()
|
||||
game.begin_turn_0()
|
||||
return game
|
||||
|
||||
|
||||
def lint_weapon_data() -> None:
|
||||
for clsid in weapon_ids:
|
||||
weapon = Weapon.from_clsid(clsid)
|
||||
if weapon not in WEAPON_INTRODUCTION_YEARS:
|
||||
logging.warning(f"{weapon} has no introduction date")
|
||||
if weapon not in WEAPON_FALLBACK_MAP:
|
||||
logging.warning(f"{weapon} has no fallback")
|
||||
def lint_all_weapon_data() -> None:
|
||||
for weapon in WeaponGroup.named("Unknown").weapons:
|
||||
logging.warning(f"No weapon data for {weapon}: {weapon.clsid}")
|
||||
|
||||
|
||||
def lint_weapon_data_for_aircraft(aircraft: AircraftType) -> None:
|
||||
all_weapons: set[Weapon] = set()
|
||||
for pylon in Pylon.iter_pylons(aircraft):
|
||||
all_weapons |= pylon.allowed
|
||||
|
||||
for weapon in all_weapons:
|
||||
if weapon.weapon_group.name == "Unknown":
|
||||
logging.warning(f'{weapon.clsid} "{weapon.name}" has no weapon data')
|
||||
|
||||
|
||||
def main():
|
||||
@@ -255,7 +297,7 @@ def main():
|
||||
|
||||
# TODO: Flesh out data and then make unconditional.
|
||||
if args.warn_missing_weapon_data:
|
||||
lint_weapon_data()
|
||||
lint_all_weapon_data()
|
||||
|
||||
if args.subcommand == "new-game":
|
||||
with logged_duration("New game creation"):
|
||||
@@ -267,7 +309,12 @@ def main():
|
||||
args.auto_procurement,
|
||||
args.inverted,
|
||||
args.cheats,
|
||||
args.date,
|
||||
args.restrict_weapons_by_date,
|
||||
)
|
||||
if args.subcommand == "lint-weapons":
|
||||
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
|
||||
return
|
||||
|
||||
run_ui(game)
|
||||
|
||||
|
||||
@@ -12,11 +12,10 @@ from PySide2.QtCore import (
|
||||
)
|
||||
from PySide2.QtGui import QIcon
|
||||
|
||||
from game import db
|
||||
from game.game import Game
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.transfers import TransferOrder
|
||||
from game.transfers import TransferOrder, PendingTransfers
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
@@ -281,9 +280,9 @@ class AtoModel(QAbstractListModel):
|
||||
self.package_models.clear()
|
||||
if self.game is not None:
|
||||
if player:
|
||||
self.ato = self.game.blue_ato
|
||||
self.ato = self.game.blue.ato
|
||||
else:
|
||||
self.ato = self.game.red_ato
|
||||
self.ato = self.game.red.ato
|
||||
else:
|
||||
self.ato = AirTaskingOrder()
|
||||
self.endResetModel()
|
||||
@@ -316,8 +315,12 @@ class TransferModel(QAbstractListModel):
|
||||
super().__init__()
|
||||
self.game_model = game_model
|
||||
|
||||
@property
|
||||
def transfers(self) -> PendingTransfers:
|
||||
return self.game_model.game.coalition_for(player=True).transfers
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return self.game_model.game.transfers.pending_transfer_count
|
||||
return self.transfers.pending_transfer_count
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
@@ -345,7 +348,7 @@ class TransferModel(QAbstractListModel):
|
||||
"""Updates the game with the new unit transfer."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
# TODO: Needs to regenerate base inventory tab.
|
||||
self.game_model.game.transfers.new_transfer(transfer)
|
||||
self.transfers.new_transfer(transfer)
|
||||
self.endInsertRows()
|
||||
|
||||
def cancel_transfer_at_index(self, index: QModelIndex) -> None:
|
||||
@@ -354,15 +357,15 @@ class TransferModel(QAbstractListModel):
|
||||
|
||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
||||
"""Cancels the planned unit transfer at the given index."""
|
||||
index = self.game_model.game.transfers.index_of_transfer(transfer)
|
||||
index = self.transfers.index_of_transfer(transfer)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
# TODO: Needs to regenerate base inventory tab.
|
||||
self.game_model.game.transfers.cancel_transfer(transfer)
|
||||
self.transfers.cancel_transfer(transfer)
|
||||
self.endRemoveRows()
|
||||
|
||||
def transfer_at_index(self, index: QModelIndex) -> TransferOrder:
|
||||
"""Returns the transfer located at the given index."""
|
||||
return self.game_model.game.transfers.transfer_at_index(index.row())
|
||||
return self.transfers.transfer_at_index(index.row())
|
||||
|
||||
|
||||
class AirWingModel(QAbstractListModel):
|
||||
@@ -488,8 +491,8 @@ class GameModel:
|
||||
self.ato_model = AtoModel(self, AirTaskingOrder())
|
||||
self.red_ato_model = AtoModel(self, AirTaskingOrder())
|
||||
else:
|
||||
self.ato_model = AtoModel(self, self.game.blue_ato)
|
||||
self.red_ato_model = AtoModel(self, self.game.red_ato)
|
||||
self.ato_model = AtoModel(self, self.game.blue.ato)
|
||||
self.red_ato_model = AtoModel(self, self.game.red.ato)
|
||||
|
||||
def ato_model_for(self, player: bool) -> AtoModel:
|
||||
if player:
|
||||
|
||||
@@ -68,6 +68,7 @@ def load_icons():
|
||||
ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif")
|
||||
ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif")
|
||||
ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif")
|
||||
ICONS["Terrain_MarianaIslands"] = QPixmap("./resources/ui/terrain_marianas.gif")
|
||||
|
||||
ICONS["Dawn"] = QPixmap("./resources/ui/conditions/timeofday/dawn.png")
|
||||
ICONS["Day"] = QPixmap("./resources/ui/conditions/timeofday/day.png")
|
||||
@@ -106,6 +107,7 @@ def load_icons():
|
||||
ICONS["PluginsOptions"] = QPixmap(
|
||||
"./resources/ui/misc/" + get_theme_icons() + "/pluginsoptions.png"
|
||||
)
|
||||
ICONS["Notes"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/notes.png")
|
||||
|
||||
ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
|
||||
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QPushButton
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.income import Income
|
||||
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
|
||||
|
||||
@@ -10,7 +11,7 @@ class QBudgetBox(QGroupBox):
|
||||
UI Component to display current budget and player's money
|
||||
"""
|
||||
|
||||
def __init__(self, game):
|
||||
def __init__(self, game: Game):
|
||||
super(QBudgetBox, self).__init__("Budget")
|
||||
|
||||
self.game = game
|
||||
@@ -40,7 +41,7 @@ class QBudgetBox(QGroupBox):
|
||||
return
|
||||
|
||||
self.game = game
|
||||
self.setBudget(self.game.budget, Income(self.game, player=True).total)
|
||||
self.setBudget(self.game.blue.budget, Income(self.game, player=True).total)
|
||||
self.finances.setEnabled(True)
|
||||
|
||||
def openFinances(self):
|
||||
|
||||
@@ -24,8 +24,8 @@ class QFactionsInfos(QGroupBox):
|
||||
|
||||
def setGame(self, game: Game):
|
||||
if game is not None:
|
||||
self.player_name.setText(game.player_faction.name)
|
||||
self.enemy_name.setText(game.enemy_faction.name)
|
||||
self.player_name.setText(game.blue.faction.name)
|
||||
self.enemy_name.setText(game.red.faction.name)
|
||||
else:
|
||||
self.player_name.setText("")
|
||||
self.enemy_name.setText("")
|
||||
|
||||
@@ -13,6 +13,7 @@ import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.event.airwar import AirWarEvent
|
||||
from game.profiling import logged_duration
|
||||
from game.utils import meters
|
||||
from gen.ato import Package
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import GameModel
|
||||
@@ -112,6 +113,14 @@ class QTopPanel(QFrame):
|
||||
self.transfers.setEnabled(True)
|
||||
|
||||
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
|
||||
|
||||
if game.conditions.weather.clouds:
|
||||
base_m = game.conditions.weather.clouds.base
|
||||
base_ft = int(meters(base_m).feet)
|
||||
self.conditionsWidget.setToolTip(f"Cloud Base: {base_m}m / {base_ft}ft")
|
||||
else:
|
||||
self.conditionsWidget.setToolTip("")
|
||||
|
||||
self.intel_box.set_game(game)
|
||||
self.budgetBox.setGame(game)
|
||||
self.factionsInfos.setGame(game)
|
||||
@@ -159,7 +168,7 @@ class QTopPanel(QFrame):
|
||||
package.time_over_target = estimator.earliest_tot()
|
||||
|
||||
def ato_has_clients(self) -> bool:
|
||||
for package in self.game.blue_ato.packages:
|
||||
for package in self.game.blue.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.client_count > 0:
|
||||
return True
|
||||
@@ -227,7 +236,7 @@ class QTopPanel(QFrame):
|
||||
|
||||
def check_no_missing_pilots(self) -> bool:
|
||||
missing_pilots = []
|
||||
for package in self.game.blue_ato.packages:
|
||||
for package in self.game.blue.ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.missing_pilots > 0:
|
||||
missing_pilots.append((package, flight))
|
||||
@@ -273,8 +282,8 @@ class QTopPanel(QFrame):
|
||||
closest_cps[0],
|
||||
closest_cps[1],
|
||||
self.game.theater.controlpoints[0].position,
|
||||
self.game.player_faction.name,
|
||||
self.game.enemy_faction.name,
|
||||
self.game.blue.faction.name,
|
||||
self.game.red.faction.name,
|
||||
)
|
||||
|
||||
unit_map = self.game.initiate_event(game_event)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Iterable, Type
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
@@ -13,16 +12,12 @@ class QAircraftTypeSelector(QComboBox):
|
||||
"""Combo box for selecting among the given aircraft types."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
aircraft_types: Iterable[Type[FlyingType]],
|
||||
country: str,
|
||||
mission_type: FlightType,
|
||||
self, aircraft_types: Iterable[Type[FlyingType]], mission_type: FlightType
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.model().sort(0)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
self.country = country
|
||||
self.update_items(mission_type, aircraft_types)
|
||||
|
||||
def update_items(self, mission_type: FlightType, aircraft_types):
|
||||
|
||||
@@ -8,11 +8,18 @@ from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import vehicle_map
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPolygon
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
MultiPolygon,
|
||||
MultiLineString,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.navmesh import NavMesh
|
||||
from game.flightplan import JoinZoneGeometry, HoldZoneGeometry
|
||||
from game.navmesh import NavMesh, NavMeshPoly
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
@@ -27,7 +34,12 @@ from game.transfers import MultiGroupTransport, TransportMap
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan, CasFlightPlan
|
||||
from gen.flights.flightplan import (
|
||||
FlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
CasFlightPlan,
|
||||
)
|
||||
from game.flightplan.ipzonegeometry import IpZoneGeometry
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel, AtoModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
@@ -39,6 +51,10 @@ LeafletPoly = list[LeafletLatLon]
|
||||
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
|
||||
# Set to True to enable computing expensive debugging information. At the time of
|
||||
# writing this only controls computing the waypoint placement zones.
|
||||
ENABLE_EXPENSIVE_DEBUG_TOOLS = False
|
||||
|
||||
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
|
||||
#
|
||||
# https://bugreports.qt.io/browse/PYSIDE-1426
|
||||
@@ -73,6 +89,18 @@ def shapely_to_leaflet_polys(
|
||||
return [shapely_poly_to_leaflet_points(poly, theater) for poly in polys]
|
||||
|
||||
|
||||
def shapely_line_to_leaflet_points(
|
||||
line: LineString, theater: ConflictTheater
|
||||
) -> list[LeafletLatLon]:
|
||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
|
||||
|
||||
|
||||
def shapely_lines_to_leaflet_points(
|
||||
lines: MultiLineString, theater: ConflictTheater
|
||||
) -> list[list[LeafletLatLon]]:
|
||||
return [shapely_line_to_leaflet_points(l, theater) for l in lines.geoms]
|
||||
|
||||
|
||||
class ControlPointJs(QObject):
|
||||
nameChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
@@ -336,8 +364,12 @@ class SupplyRouteJs(QObject):
|
||||
|
||||
def find_transports(self) -> List[MultiGroupTransport]:
|
||||
if self.sea_route:
|
||||
return self.find_in_transport_map(self.game.transfers.cargo_ships)
|
||||
return self.find_in_transport_map(self.game.transfers.convoys)
|
||||
return self.find_in_transport_map(
|
||||
self.game.blue.transfers.cargo_ships
|
||||
) + self.find_in_transport_map(self.game.red.transfers.cargo_ships)
|
||||
return self.find_in_transport_map(
|
||||
self.game.blue.transfers.convoys
|
||||
) + self.find_in_transport_map(self.game.red.transfers.convoys)
|
||||
|
||||
@Property(list, notify=activeTransportsChanged)
|
||||
def activeTransports(self) -> List[str]:
|
||||
@@ -385,12 +417,12 @@ class FrontLineJs(QObject):
|
||||
def extents(self) -> List[LeafletLatLon]:
|
||||
a = self.theater.point_to_ll(
|
||||
self.front_line.position.point_from_heading(
|
||||
self.front_line.attack_heading + 90, nautical_miles(2).meters
|
||||
self.front_line.attack_heading.right.degrees, nautical_miles(2).meters
|
||||
)
|
||||
)
|
||||
b = self.theater.point_to_ll(
|
||||
self.front_line.position.point_from_heading(
|
||||
self.front_line.attack_heading + 270, nautical_miles(2).meters
|
||||
self.front_line.attack_heading.left.degrees, nautical_miles(2).meters
|
||||
)
|
||||
)
|
||||
return [[a.latitude, a.longitude], [b.latitude, b.longitude]]
|
||||
@@ -508,6 +540,19 @@ class FlightJs(QObject):
|
||||
selectedChanged = Signal()
|
||||
commitBoundaryChanged = Signal()
|
||||
|
||||
originChanged = Signal()
|
||||
|
||||
@Property(list, notify=originChanged)
|
||||
def origin(self) -> LeafletLatLon:
|
||||
return self._waypoints[0].position
|
||||
|
||||
targetChanged = Signal()
|
||||
|
||||
@Property(list, notify=targetChanged)
|
||||
def target(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.flight.package.target.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
@@ -642,11 +687,35 @@ class ThreatZoneContainerJs(QObject):
|
||||
return self._red
|
||||
|
||||
|
||||
class NavMeshPolyJs(QObject):
|
||||
polyChanged = Signal()
|
||||
threatenedChanged = Signal()
|
||||
|
||||
def __init__(self, poly: LeafletPoly, threatened: bool) -> None:
|
||||
super().__init__()
|
||||
self._poly = poly
|
||||
self._threatened = threatened
|
||||
|
||||
@Property(list, notify=polyChanged)
|
||||
def poly(self) -> LeafletPoly:
|
||||
return self._poly
|
||||
|
||||
@Property(bool, notify=threatenedChanged)
|
||||
def threatened(self) -> bool:
|
||||
return self._threatened
|
||||
|
||||
@classmethod
|
||||
def from_navmesh(cls, poly: NavMeshPoly, theater: ConflictTheater) -> NavMeshPolyJs:
|
||||
return NavMeshPolyJs(
|
||||
shapely_poly_to_leaflet_points(poly.poly, theater), poly.threatened
|
||||
)
|
||||
|
||||
|
||||
class NavMeshJs(QObject):
|
||||
blueChanged = Signal()
|
||||
redChanged = Signal()
|
||||
|
||||
def __init__(self, blue: list[LeafletPoly], red: list[LeafletPoly]) -> None:
|
||||
def __init__(self, blue: list[NavMeshPolyJs], red: list[NavMeshPolyJs]) -> None:
|
||||
super().__init__()
|
||||
self._blue = blue
|
||||
self._red = red
|
||||
@@ -663,17 +732,17 @@ class NavMeshJs(QObject):
|
||||
return self._red
|
||||
|
||||
@staticmethod
|
||||
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[LeafletPoly]:
|
||||
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[NavMeshPolyJs]:
|
||||
polys = []
|
||||
for poly in navmesh.polys:
|
||||
polys.append(shapely_poly_to_leaflet_points(poly.poly, theater))
|
||||
polys.append(NavMeshPolyJs.from_navmesh(poly, theater))
|
||||
return polys
|
||||
|
||||
@classmethod
|
||||
def from_game(cls, game: Game) -> NavMeshJs:
|
||||
return NavMeshJs(
|
||||
cls.to_polys(game.blue_navmesh, game.theater),
|
||||
cls.to_polys(game.red_navmesh, game.theater),
|
||||
cls.to_polys(game.blue.nav_mesh, game.theater),
|
||||
cls.to_polys(game.red.nav_mesh, game.theater),
|
||||
)
|
||||
|
||||
|
||||
@@ -741,6 +810,209 @@ class UnculledZone(QObject):
|
||||
)
|
||||
|
||||
|
||||
class IpZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
ipBubbleChanged = Signal()
|
||||
permissibleZoneChanged = Signal()
|
||||
safeZonesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: LeafletPoly,
|
||||
ip_bubble: LeafletPoly,
|
||||
permissible_zone: LeafletPoly,
|
||||
safe_zones: list[LeafletPoly],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._ip_bubble = ip_bubble
|
||||
self._permissible_zone = permissible_zone
|
||||
self._safe_zones = safe_zones
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=ipBubbleChanged)
|
||||
def ipBubble(self) -> LeafletPoly:
|
||||
return self._ip_bubble
|
||||
|
||||
@Property(list, notify=permissibleZoneChanged)
|
||||
def permissibleZone(self) -> LeafletPoly:
|
||||
return self._permissible_zone
|
||||
|
||||
@Property(list, notify=safeZonesChanged)
|
||||
def safeZones(self) -> list[LeafletPoly]:
|
||||
return self._safe_zones
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> IpZonesJs:
|
||||
return IpZonesJs([], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return IpZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
||||
return IpZonesJs(
|
||||
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.permissible_zone, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.safe_zones, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class JoinZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
targetBubbleChanged = Signal()
|
||||
ipBubbleChanged = Signal()
|
||||
excludedZonesChanged = Signal()
|
||||
permissibleZonesChanged = Signal()
|
||||
preferredLinesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: LeafletPoly,
|
||||
target_bubble: LeafletPoly,
|
||||
ip_bubble: LeafletPoly,
|
||||
excluded_zones: list[LeafletPoly],
|
||||
permissible_zones: list[LeafletPoly],
|
||||
preferred_lines: list[list[LeafletLatLon]],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._target_bubble = target_bubble
|
||||
self._ip_bubble = ip_bubble
|
||||
self._excluded_zones = excluded_zones
|
||||
self._permissible_zones = permissible_zones
|
||||
self._preferred_lines = preferred_lines
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=targetBubbleChanged)
|
||||
def targetBubble(self) -> LeafletPoly:
|
||||
return self._target_bubble
|
||||
|
||||
@Property(list, notify=ipBubbleChanged)
|
||||
def ipBubble(self) -> LeafletPoly:
|
||||
return self._ip_bubble
|
||||
|
||||
@Property(list, notify=excludedZonesChanged)
|
||||
def excludedZones(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zones
|
||||
|
||||
@Property(list, notify=permissibleZonesChanged)
|
||||
def permissibleZones(self) -> list[LeafletPoly]:
|
||||
return self._permissible_zones
|
||||
|
||||
@Property(list, notify=preferredLinesChanged)
|
||||
def preferredLines(self) -> list[list[LeafletLatLon]]:
|
||||
return self._preferred_lines
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> JoinZonesJs:
|
||||
return JoinZonesJs([], [], [], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return JoinZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
if flight.package.waypoints is None:
|
||||
return JoinZonesJs.empty()
|
||||
ip = flight.package.waypoints.ingress
|
||||
geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue)
|
||||
return JoinZonesJs(
|
||||
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
|
||||
shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class HoldZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
targetBubbleChanged = Signal()
|
||||
joinBubbleChanged = Signal()
|
||||
excludedZonesChanged = Signal()
|
||||
permissibleZonesChanged = Signal()
|
||||
preferredLinesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: LeafletPoly,
|
||||
target_bubble: LeafletPoly,
|
||||
join_bubble: LeafletPoly,
|
||||
excluded_zones: list[LeafletPoly],
|
||||
permissible_zones: list[LeafletPoly],
|
||||
preferred_lines: list[list[LeafletLatLon]],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._target_bubble = target_bubble
|
||||
self._join_bubble = join_bubble
|
||||
self._excluded_zones = excluded_zones
|
||||
self._permissible_zones = permissible_zones
|
||||
self._preferred_lines = preferred_lines
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> LeafletPoly:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=targetBubbleChanged)
|
||||
def targetBubble(self) -> LeafletPoly:
|
||||
return self._target_bubble
|
||||
|
||||
@Property(list, notify=joinBubbleChanged)
|
||||
def joinBubble(self) -> LeafletPoly:
|
||||
return self._join_bubble
|
||||
|
||||
@Property(list, notify=excludedZonesChanged)
|
||||
def excludedZones(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zones
|
||||
|
||||
@Property(list, notify=permissibleZonesChanged)
|
||||
def permissibleZones(self) -> list[LeafletPoly]:
|
||||
return self._permissible_zones
|
||||
|
||||
@Property(list, notify=preferredLinesChanged)
|
||||
def preferredLines(self) -> list[list[LeafletLatLon]]:
|
||||
return self._preferred_lines
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> HoldZonesJs:
|
||||
return HoldZonesJs([], [], [], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return JoinZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
if flight.package.waypoints is None:
|
||||
return HoldZonesJs.empty()
|
||||
ip = flight.package.waypoints.ingress
|
||||
join = flight.package.waypoints.join
|
||||
geometry = HoldZoneGeometry(
|
||||
target.position, home.position, ip, join, game.blue, game.theater
|
||||
)
|
||||
return HoldZonesJs(
|
||||
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
|
||||
shapely_poly_to_leaflet_points(geometry.join_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
|
||||
shapely_lines_to_leaflet_points(geometry.preferred_lines, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class MapModel(QObject):
|
||||
cleared = Signal()
|
||||
|
||||
@@ -754,6 +1026,9 @@ class MapModel(QObject):
|
||||
navmeshesChanged = Signal()
|
||||
mapZonesChanged = Signal()
|
||||
unculledZonesChanged = Signal()
|
||||
ipZonesChanged = Signal()
|
||||
joinZonesChanged = Signal()
|
||||
holdZonesChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@@ -770,6 +1045,9 @@ class MapModel(QObject):
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._hold_zones = HoldZonesJs.empty()
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
@@ -793,6 +1071,7 @@ class MapModel(QObject):
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self.cleared.emit()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
@@ -868,11 +1147,30 @@ class MapModel(QObject):
|
||||
)
|
||||
return flights
|
||||
|
||||
def _get_selected_flight(self) -> Optional[Flight]:
|
||||
for p_idx, package in enumerate(self.game.blue.ato.packages):
|
||||
for f_idx, flight in enumerate(package.flights):
|
||||
if (p_idx, f_idx) == self._selected_flight_index:
|
||||
return flight
|
||||
return None
|
||||
|
||||
def reset_atos(self) -> None:
|
||||
self._flights = self._flights_in_ato(
|
||||
self.game.blue_ato, blue=True
|
||||
) + self._flights_in_ato(self.game.red_ato, blue=False)
|
||||
self.game.blue.ato, blue=True
|
||||
) + self._flights_in_ato(self.game.red.ato, blue=False)
|
||||
self.flightsChanged.emit()
|
||||
selected_flight = self._get_selected_flight()
|
||||
if selected_flight is None:
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._hold_zones = HoldZonesJs.empty()
|
||||
else:
|
||||
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
||||
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
|
||||
self._hold_zones = HoldZonesJs.for_flight(selected_flight, self.game)
|
||||
self.ipZonesChanged.emit()
|
||||
self.joinZonesChanged.emit()
|
||||
self.holdZonesChanged.emit()
|
||||
|
||||
@Property(list, notify=flightsChanged)
|
||||
def flights(self) -> List[FlightJs]:
|
||||
@@ -1001,6 +1299,18 @@ class MapModel(QObject):
|
||||
def unculledZones(self) -> list[UnculledZone]:
|
||||
return self._unculled_zones
|
||||
|
||||
@Property(IpZonesJs, notify=ipZonesChanged)
|
||||
def ipZones(self) -> IpZonesJs:
|
||||
return self._ip_zones
|
||||
|
||||
@Property(JoinZonesJs, notify=joinZonesChanged)
|
||||
def joinZones(self) -> JoinZonesJs:
|
||||
return self._join_zones
|
||||
|
||||
@Property(HoldZonesJs, notify=holdZonesChanged)
|
||||
def holdZones(self) -> HoldZonesJs:
|
||||
return self._hold_zones
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
326
qt_ui/windows/AirWingConfigurationDialog.py
Normal file
326
qt_ui/windows/AirWingConfigurationDialog.py
Normal file
@@ -0,0 +1,326 @@
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
QModelIndex,
|
||||
QSize,
|
||||
Qt,
|
||||
QItemSelection,
|
||||
Signal,
|
||||
)
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QDialog,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
QLineEdit,
|
||||
QTextEdit,
|
||||
QCheckBox,
|
||||
QHBoxLayout,
|
||||
QStackedLayout,
|
||||
QTabWidget,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons import Squadron, AirWing, Pilot
|
||||
from gen.flights.flight import FlightType
|
||||
from qt_ui.models import AirWingModel, SquadronModel
|
||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||
from qt_ui.windows.AirWingDialog import SquadronDelegate
|
||||
from qt_ui.windows.SquadronDialog import SquadronDialog
|
||||
|
||||
|
||||
class SquadronList(QListView):
|
||||
"""List view for displaying the air wing's squadrons."""
|
||||
|
||||
def __init__(self, air_wing_model: AirWingModel) -> None:
|
||||
super().__init__()
|
||||
self.air_wing_model = air_wing_model
|
||||
self.dialog: Optional[SquadronDialog] = None
|
||||
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
|
||||
self.setModel(self.air_wing_model)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
|
||||
)
|
||||
|
||||
# self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
self.doubleClicked.connect(self.on_double_click)
|
||||
|
||||
def on_double_click(self, index: QModelIndex) -> None:
|
||||
if not index.isValid():
|
||||
return
|
||||
self.dialog = SquadronDialog(
|
||||
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
|
||||
)
|
||||
self.dialog.show()
|
||||
|
||||
|
||||
class AllowedMissionTypeControls(QVBoxLayout):
|
||||
def __init__(self, squadron: Squadron) -> None:
|
||||
super().__init__()
|
||||
self.squadron = squadron
|
||||
self.allowed_mission_types = set()
|
||||
|
||||
self.addWidget(QLabel("Allowed mission types"))
|
||||
|
||||
def make_callback(toggled_task: FlightType) -> Callable[[bool], None]:
|
||||
def callback(checked: bool) -> None:
|
||||
self.on_toggled(toggled_task, checked)
|
||||
|
||||
return callback
|
||||
|
||||
for task in FlightType:
|
||||
enabled = task in squadron.mission_types
|
||||
if enabled:
|
||||
self.allowed_mission_types.add(task)
|
||||
checkbox = QCheckBox(text=task.value)
|
||||
checkbox.setChecked(enabled)
|
||||
checkbox.toggled.connect(make_callback(task))
|
||||
self.addWidget(checkbox)
|
||||
|
||||
self.addStretch()
|
||||
|
||||
def on_toggled(self, task: FlightType, checked: bool) -> None:
|
||||
if checked:
|
||||
self.allowed_mission_types.add(task)
|
||||
else:
|
||||
self.allowed_mission_types.remove(task)
|
||||
|
||||
|
||||
class SquadronConfigurationBox(QGroupBox):
|
||||
def __init__(self, squadron: Squadron) -> None:
|
||||
super().__init__()
|
||||
self.setCheckable(True)
|
||||
self.squadron = squadron
|
||||
self.reset_title()
|
||||
|
||||
columns = QHBoxLayout()
|
||||
self.setLayout(columns)
|
||||
|
||||
left_column = QVBoxLayout()
|
||||
columns.addLayout(left_column)
|
||||
|
||||
left_column.addWidget(QLabel("Name:"))
|
||||
self.name_edit = QLineEdit(squadron.name)
|
||||
self.name_edit.textChanged.connect(self.on_name_changed)
|
||||
left_column.addWidget(self.name_edit)
|
||||
|
||||
left_column.addWidget(QLabel("Nickname:"))
|
||||
self.nickname_edit = QLineEdit(squadron.nickname)
|
||||
self.nickname_edit.textChanged.connect(self.on_nickname_changed)
|
||||
left_column.addWidget(self.nickname_edit)
|
||||
|
||||
if squadron.player:
|
||||
player_label = QLabel(
|
||||
"Players (one per line, leave empty for an AI-only squadron):"
|
||||
)
|
||||
else:
|
||||
player_label = QLabel("Player slots not available for opfor")
|
||||
left_column.addWidget(player_label)
|
||||
|
||||
players = [p for p in squadron.pilot_pool if p.player]
|
||||
for player in players:
|
||||
squadron.pilot_pool.remove(player)
|
||||
if not squadron.player:
|
||||
players = []
|
||||
self.player_list = QTextEdit("<br />".join(p.name for p in players))
|
||||
self.player_list.setAcceptRichText(False)
|
||||
self.player_list.setEnabled(squadron.player)
|
||||
left_column.addWidget(self.player_list)
|
||||
|
||||
left_column.addStretch()
|
||||
|
||||
self.allowed_missions = AllowedMissionTypeControls(squadron)
|
||||
columns.addLayout(self.allowed_missions)
|
||||
|
||||
def on_name_changed(self, text: str) -> None:
|
||||
self.squadron.name = text
|
||||
self.reset_title()
|
||||
|
||||
def on_nickname_changed(self, text: str) -> None:
|
||||
self.squadron.nickname = text
|
||||
|
||||
def reset_title(self) -> None:
|
||||
self.setTitle(f"{self.squadron.name} - {self.squadron.aircraft}")
|
||||
|
||||
def apply(self) -> Squadron:
|
||||
player_names = self.player_list.toPlainText().splitlines()
|
||||
# Prepend player pilots so they get set active first.
|
||||
self.squadron.pilot_pool = [
|
||||
Pilot(n, player=True) for n in player_names
|
||||
] + self.squadron.pilot_pool
|
||||
self.squadron.mission_types = tuple(self.allowed_missions.allowed_mission_types)
|
||||
return self.squadron
|
||||
|
||||
|
||||
class SquadronConfigurationLayout(QVBoxLayout):
|
||||
def __init__(self, squadrons: list[Squadron]) -> None:
|
||||
super().__init__()
|
||||
self.squadron_configs = []
|
||||
for squadron in squadrons:
|
||||
squadron_config = SquadronConfigurationBox(squadron)
|
||||
self.squadron_configs.append(squadron_config)
|
||||
self.addWidget(squadron_config)
|
||||
|
||||
def apply(self) -> list[Squadron]:
|
||||
keep_squadrons = []
|
||||
for squadron_config in self.squadron_configs:
|
||||
if squadron_config.isChecked():
|
||||
keep_squadrons.append(squadron_config.apply())
|
||||
return keep_squadrons
|
||||
|
||||
|
||||
class AircraftSquadronsPage(QWidget):
|
||||
def __init__(self, squadrons: list[Squadron]) -> None:
|
||||
super().__init__()
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.squadrons_config = SquadronConfigurationLayout(squadrons)
|
||||
|
||||
scrolling_widget = QWidget()
|
||||
scrolling_widget.setLayout(self.squadrons_config)
|
||||
|
||||
scrolling_area = QScrollArea()
|
||||
scrolling_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scrolling_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scrolling_area.setWidgetResizable(True)
|
||||
scrolling_area.setWidget(scrolling_widget)
|
||||
|
||||
layout.addWidget(scrolling_area)
|
||||
|
||||
def apply(self) -> list[Squadron]:
|
||||
return self.squadrons_config.apply()
|
||||
|
||||
|
||||
class AircraftSquadronsPanel(QStackedLayout):
|
||||
def __init__(self, air_wing: AirWing) -> None:
|
||||
super().__init__()
|
||||
self.air_wing = air_wing
|
||||
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
|
||||
for aircraft, squadrons in self.air_wing.squadrons.items():
|
||||
page = AircraftSquadronsPage(squadrons)
|
||||
self.addWidget(page)
|
||||
self.squadrons_pages[aircraft] = page
|
||||
|
||||
def apply(self) -> None:
|
||||
for aircraft, page in self.squadrons_pages.items():
|
||||
self.air_wing.squadrons[aircraft] = page.apply()
|
||||
|
||||
|
||||
class AircraftTypeList(QListView):
|
||||
page_index_changed = Signal(int)
|
||||
|
||||
def __init__(self, air_wing: AirWing) -> None:
|
||||
super().__init__()
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setMinimumWidth(300)
|
||||
|
||||
model = QStandardItemModel(self)
|
||||
self.setModel(model)
|
||||
|
||||
self.selectionModel().setCurrentIndex(
|
||||
model.index(0, 0), QItemSelectionModel.Select
|
||||
)
|
||||
self.selectionModel().selectionChanged.connect(self.on_selection_changed)
|
||||
for aircraft in 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)
|
||||
model.appendRow(aircraft_item)
|
||||
|
||||
def on_selection_changed(
|
||||
self, selected: QItemSelection, _deselected: QItemSelection
|
||||
) -> None:
|
||||
indexes = selected.indexes()
|
||||
if len(indexes) > 1:
|
||||
raise RuntimeError("Aircraft list should not allow multi-selection")
|
||||
if not indexes:
|
||||
return
|
||||
self.page_index_changed.emit(indexes[0].row())
|
||||
|
||||
@staticmethod
|
||||
def icon_for(aircraft: AircraftType) -> Optional[QIcon]:
|
||||
name = aircraft.dcs_id
|
||||
if name in AIRCRAFT_ICONS:
|
||||
return QIcon(AIRCRAFT_ICONS[name])
|
||||
return None
|
||||
|
||||
|
||||
class AirWingConfigurationTab(QWidget):
|
||||
def __init__(self, air_wing: AirWing) -> None:
|
||||
super().__init__()
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
type_list = AircraftTypeList(air_wing)
|
||||
type_list.page_index_changed.connect(self.on_aircraft_changed)
|
||||
layout.addWidget(type_list)
|
||||
|
||||
self.squadrons_panel = AircraftSquadronsPanel(air_wing)
|
||||
layout.addLayout(self.squadrons_panel)
|
||||
|
||||
def apply(self) -> None:
|
||||
self.squadrons_panel.apply()
|
||||
|
||||
def on_aircraft_changed(self, index: QModelIndex) -> None:
|
||||
self.squadrons_panel.setCurrentIndex(index)
|
||||
|
||||
|
||||
class AirWingConfigurationDialog(QDialog):
|
||||
"""Dialog window for air wing configuration."""
|
||||
|
||||
def __init__(self, game: Game, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(500, 800)
|
||||
self.setWindowTitle(f"Air Wing Configuration")
|
||||
# TODO: self.setWindowIcon()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
doc_url = (
|
||||
"https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots"
|
||||
)
|
||||
doc_label = QLabel(
|
||||
"Use this opportunity to customize the squadrons available to your "
|
||||
"coalition. <strong>This is your only opportunity to make changes.</strong>"
|
||||
"<br /><br />"
|
||||
"To accept your changes and continue, close this window.<br />"
|
||||
"<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)
|
||||
|
||||
tab_widget = QTabWidget()
|
||||
layout.addWidget(tab_widget)
|
||||
|
||||
self.tabs = []
|
||||
for coalition in game.coalitions:
|
||||
coalition_tab = AirWingConfigurationTab(coalition.air_wing)
|
||||
name = "Blue" if coalition.player else "Red"
|
||||
tab_widget.addTab(coalition_tab, name)
|
||||
self.tabs.append(coalition_tab)
|
||||
|
||||
def reject(self) -> None:
|
||||
for tab in self.tabs:
|
||||
tab.apply()
|
||||
super().reject()
|
||||
@@ -3,12 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Iterator
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
QModelIndex,
|
||||
Qt,
|
||||
QSize,
|
||||
)
|
||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
@@ -183,7 +178,7 @@ class AirInventoryView(QWidget):
|
||||
self.table.setSortingEnabled(True)
|
||||
|
||||
def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
||||
for package in self.game_model.game.blue_ato.packages:
|
||||
for package in self.game_model.game.blue.ato.packages:
|
||||
for flight in package.flights:
|
||||
yield from AircraftInventoryData.from_flight(flight)
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
|
||||
)
|
||||
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
from qt_ui.windows.notes.QNotesWindow import QNotesWindow
|
||||
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
|
||||
|
||||
|
||||
class QLiberationWindow(QMainWindow):
|
||||
@@ -150,6 +152,9 @@ class QLiberationWindow(QMainWindow):
|
||||
)
|
||||
)
|
||||
|
||||
self.openLogsAction = QAction("Show &logs", self)
|
||||
self.openLogsAction.triggered.connect(self.showLogsDialog)
|
||||
|
||||
self.openSettingsAction = QAction("Settings", self)
|
||||
self.openSettingsAction.setIcon(CONST.ICONS["Settings"])
|
||||
self.openSettingsAction.triggered.connect(self.showSettingsDialog)
|
||||
@@ -158,6 +163,10 @@ class QLiberationWindow(QMainWindow):
|
||||
self.openStatsAction.setIcon(CONST.ICONS["Statistics"])
|
||||
self.openStatsAction.triggered.connect(self.showStatsDialog)
|
||||
|
||||
self.openNotesAction = QAction("Notes", self)
|
||||
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
|
||||
self.openNotesAction.triggered.connect(self.showNotesDialog)
|
||||
|
||||
def initToolbar(self):
|
||||
self.tool_bar = self.addToolBar("File")
|
||||
self.tool_bar.addAction(self.newGameAction)
|
||||
@@ -171,6 +180,7 @@ class QLiberationWindow(QMainWindow):
|
||||
self.actions_bar = self.addToolBar("Actions")
|
||||
self.actions_bar.addAction(self.openSettingsAction)
|
||||
self.actions_bar.addAction(self.openStatsAction)
|
||||
self.actions_bar.addAction(self.openNotesAction)
|
||||
|
||||
def initMenuBar(self):
|
||||
self.menu = self.menuBar()
|
||||
@@ -204,6 +214,7 @@ class QLiberationWindow(QMainWindow):
|
||||
help_menu.addAction(
|
||||
"Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"])
|
||||
)
|
||||
help_menu.addAction(self.openLogsAction)
|
||||
|
||||
help_menu.addSeparator()
|
||||
help_menu.addAction(self.showAboutDialogAction)
|
||||
@@ -351,6 +362,14 @@ class QLiberationWindow(QMainWindow):
|
||||
self.dialog = QStatsWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def showNotesDialog(self):
|
||||
self.dialog = QNotesWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def showLogsDialog(self):
|
||||
self.dialog = QLogsWindow()
|
||||
self.dialog.show()
|
||||
|
||||
def onDebriefing(self, debrief: Debriefing):
|
||||
logging.info("On Debriefing")
|
||||
self.debriefing = QDebriefingWindow(debrief)
|
||||
|
||||
@@ -94,6 +94,9 @@ class QUnitInfoWindow(QDialog):
|
||||
self.details_text = QTextBrowser()
|
||||
self.details_text.setProperty("style", "info-desc")
|
||||
self.details_text.setText(unit_type.description)
|
||||
self.details_text.setOpenExternalLinks(
|
||||
True
|
||||
) # in aircrafttype.py and groundunittype.py, for the descriptions, if No Data. including a google search link
|
||||
self.gridLayout.addWidget(self.details_text, 3, 0)
|
||||
|
||||
self.layout.addLayout(self.gridLayout, 1, 0)
|
||||
|
||||
@@ -73,11 +73,15 @@ class DepartingConvoysList(QFrame):
|
||||
task_box_layout = QGridLayout()
|
||||
scroll_content.setLayout(task_box_layout)
|
||||
|
||||
for convoy in game_model.game.transfers.convoys.departing_from(cp):
|
||||
for convoy in game_model.game.coalition_for(
|
||||
cp.captured
|
||||
).transfers.convoys.departing_from(cp):
|
||||
group_info = DepartingConvoyInfo(convoy)
|
||||
task_box_layout.addWidget(group_info)
|
||||
|
||||
for cargo_ship in game_model.game.transfers.cargo_ships.departing_from(cp):
|
||||
for cargo_ship in game_model.game.coalition_for(
|
||||
cp.captured
|
||||
).transfers.cargo_ships.departing_from(cp):
|
||||
group_info = DepartingConvoyInfo(cargo_ship)
|
||||
task_box_layout.addWidget(group_info)
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class QBaseMenu2(QDialog):
|
||||
capture_button.clicked.connect(self.cheat_capture)
|
||||
|
||||
self.budget_display = QLabel(
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.blue.budget)
|
||||
)
|
||||
self.budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
|
||||
self.budget_display.setProperty("style", "budget-label")
|
||||
@@ -124,7 +124,6 @@ class QBaseMenu2(QDialog):
|
||||
self.cp.capture(self.game_model.game, for_player=not self.cp.captured)
|
||||
# Reinitialized ground planners and the like. The ATO needs to be reset because
|
||||
# missions planned against the flipped base are no longer valid.
|
||||
self.game_model.game.reset_ato()
|
||||
self.game_model.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
@@ -140,7 +139,7 @@ class QBaseMenu2(QDialog):
|
||||
|
||||
@property
|
||||
def can_afford_runway_repair(self) -> bool:
|
||||
return self.game_model.game.budget >= db.RUNWAY_REPAIR_COST
|
||||
return self.game_model.game.blue.budget >= db.RUNWAY_REPAIR_COST
|
||||
|
||||
def begin_runway_repair(self) -> None:
|
||||
if not self.can_afford_runway_repair:
|
||||
@@ -148,7 +147,7 @@ class QBaseMenu2(QDialog):
|
||||
self,
|
||||
"Cannot repair runway",
|
||||
f"Runway repair costs ${db.RUNWAY_REPAIR_COST}M but you have "
|
||||
f"only ${self.game_model.game.budget}M available.",
|
||||
f"only ${self.game_model.game.blue.budget}M available.",
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
return
|
||||
@@ -162,7 +161,7 @@ class QBaseMenu2(QDialog):
|
||||
return
|
||||
|
||||
self.cp.begin_runway_repair()
|
||||
self.game_model.game.budget -= db.RUNWAY_REPAIR_COST
|
||||
self.game_model.game.blue.budget -= db.RUNWAY_REPAIR_COST
|
||||
self.update_repair_button()
|
||||
self.update_intel_summary()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
@@ -196,7 +195,9 @@ class QBaseMenu2(QDialog):
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
deployable_unit_info = ""
|
||||
|
||||
allocated = self.cp.allocated_ground_units(self.game_model.game.transfers)
|
||||
allocated = self.cp.allocated_ground_units(
|
||||
self.game_model.game.coalition_for(self.cp.captured).transfers
|
||||
)
|
||||
unit_overage = max(
|
||||
allocated.total_present - self.cp.frontline_unit_count_limit, 0
|
||||
)
|
||||
@@ -256,4 +257,6 @@ class QBaseMenu2(QDialog):
|
||||
NewUnitTransferDialog(self.game_model, self.cp, parent=self.window()).show()
|
||||
|
||||
def update_budget(self, game: Game) -> None:
|
||||
self.budget_display.setText(QRecruitBehaviour.BUDGET_FORMAT.format(game.budget))
|
||||
self.budget_display.setText(
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(game.blue.budget)
|
||||
)
|
||||
|
||||
@@ -103,11 +103,11 @@ class QRecruitBehaviour:
|
||||
|
||||
@property
|
||||
def budget(self) -> float:
|
||||
return self.game_model.game.budget
|
||||
return self.game_model.game.blue.budget
|
||||
|
||||
@budget.setter
|
||||
def budget(self, value: int) -> None:
|
||||
self.game_model.game.budget = value
|
||||
self.game_model.game.blue.budget = value
|
||||
|
||||
def add_purchase_row(
|
||||
self,
|
||||
@@ -209,8 +209,6 @@ class QRecruitBehaviour:
|
||||
if self.pending_deliveries.available_next_turn(unit_type) > 0:
|
||||
self.budget += unit_type.price
|
||||
self.pending_deliveries.sell({unit_type: 1})
|
||||
if self.pending_deliveries.units[unit_type] == 0:
|
||||
del self.pending_deliveries.units[unit_type]
|
||||
self.update_purchase_controls()
|
||||
self.update_available_budget()
|
||||
return True
|
||||
|
||||
@@ -45,7 +45,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
row = 0
|
||||
|
||||
unit_types: Set[AircraftType] = set()
|
||||
for unit_type in self.game_model.game.player_faction.aircrafts:
|
||||
for unit_type in self.game_model.game.blue.faction.aircrafts:
|
||||
if self.cp.is_carrier and not unit_type.carrier_capable:
|
||||
continue
|
||||
if self.cp.is_lha and not unit_type.lha_capable:
|
||||
|
||||
@@ -56,6 +56,5 @@ class QGroundForcesStrategy(QGroupBox):
|
||||
self.cp.base.affect_strength(amount)
|
||||
enemy_point.base.affect_strength(-amount)
|
||||
# Clear the ATO to replan missions affected by the front line.
|
||||
self.game.reset_ato()
|
||||
self.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
@@ -57,10 +57,7 @@ class FinancesLayout(QGridLayout):
|
||||
middle=f"Income multiplier: {income.multiplier:.1f}",
|
||||
right=f"<b>{income.total}M</b>",
|
||||
)
|
||||
if player:
|
||||
budget = game.budget
|
||||
else:
|
||||
budget = game.enemy_budget
|
||||
budget = game.coalition_for(player).budget
|
||||
self.add_row(middle="Balance", right=f"<b>{budget}M</b>")
|
||||
self.setRowStretch(next(self.row), 1)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtGui import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
@@ -238,8 +237,8 @@ class QGroundObjectMenu(QDialog):
|
||||
self.total_value = total_value
|
||||
|
||||
def repair_unit(self, group, unit, price):
|
||||
if self.game.budget > price:
|
||||
self.game.budget -= price
|
||||
if self.game.blue.budget > price:
|
||||
self.game.blue.budget -= price
|
||||
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
|
||||
group.units.append(unit)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
@@ -257,8 +256,16 @@ class QGroundObjectMenu(QDialog):
|
||||
|
||||
def sell_all(self):
|
||||
self.update_total_value()
|
||||
self.game.budget = self.game.budget + self.total_value
|
||||
self.game.blue.budget = self.game.blue.budget + self.total_value
|
||||
self.ground_object.groups = []
|
||||
|
||||
# Replan if the tgo was a target of the redfor
|
||||
if any(
|
||||
package.target == self.ground_object
|
||||
for package in self.game.ato_for(player=False).packages
|
||||
):
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
self.do_refresh_layout()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
@@ -299,14 +306,17 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
self.buySamBox = QGroupBox("Buy SAM site :")
|
||||
self.buyArmorBox = QGroupBox("Buy defensive position :")
|
||||
|
||||
faction = self.game.player_faction
|
||||
faction = self.game.blue.faction
|
||||
|
||||
# Sams
|
||||
|
||||
possible_sams = get_faction_possible_sams_generator(faction)
|
||||
for sam in possible_sams:
|
||||
# Pre Generate SAM to get the real price
|
||||
generator = sam(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.samCombo.addItem(
|
||||
sam.name + " [$" + str(sam.price) + "M]", userData=sam
|
||||
generator.name + " [$" + str(generator.price) + "M]", userData=generator
|
||||
)
|
||||
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
|
||||
|
||||
@@ -331,8 +341,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
|
||||
ewr_types = get_faction_possible_ewrs_generator(faction)
|
||||
for ewr_type in ewr_types:
|
||||
# Pre Generate to get the real price
|
||||
generator = ewr_type(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.ewr_selector.addItem(
|
||||
f"{ewr_type.name()} [${ewr_type.price()}M]", ewr_type
|
||||
generator.name() + " [$" + str(generator.price) + "M]",
|
||||
userData=generator,
|
||||
)
|
||||
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
|
||||
|
||||
@@ -402,7 +416,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
def on_ewr_selection_changed(self, index):
|
||||
ewr = self.ewr_selector.itemData(index)
|
||||
self.buy_ewr_button.setText(
|
||||
f"Buy [${ewr.price()}M][-${self.current_group_value}M]"
|
||||
f"Buy [${ewr.price}M][-${self.current_group_value}M]"
|
||||
)
|
||||
|
||||
def armorComboChanged(self, index):
|
||||
@@ -419,12 +433,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
logging.info("Buying Armor ")
|
||||
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
|
||||
price = utype.price * self.amount.value() - self.current_group_value
|
||||
if price > self.game.budget:
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
self.close()
|
||||
return
|
||||
else:
|
||||
self.game.budget -= price
|
||||
self.game.blue.budget -= price
|
||||
|
||||
# Generate Armor
|
||||
group = generate_armor_group_of_type_and_size(
|
||||
@@ -432,36 +446,40 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
)
|
||||
self.ground_object.groups = [group]
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def buySam(self):
|
||||
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
|
||||
price = sam_generator.price - self.current_group_value
|
||||
if price > self.game.budget:
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
return
|
||||
else:
|
||||
self.game.budget -= price
|
||||
self.game.blue.budget -= price
|
||||
|
||||
# Generate SAM
|
||||
generator = sam_generator(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.ground_object.groups = list(generator.groups)
|
||||
self.ground_object.groups = list(sam_generator.groups)
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def buy_ewr(self):
|
||||
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
|
||||
price = ewr_generator.price() - self.current_group_value
|
||||
if price > self.game.budget:
|
||||
price = ewr_generator.price - self.current_group_value
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
return
|
||||
else:
|
||||
self.game.budget -= price
|
||||
self.game.blue.budget -= price
|
||||
|
||||
generator = ewr_generator(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.ground_object.groups = [generator.vg]
|
||||
self.ground_object.groups = [ewr_generator.vg]
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
|
||||
67
qt_ui/windows/logs/QLogsWindow.py
Normal file
67
qt_ui/windows/logs/QLogsWindow.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QPlainTextEdit,
|
||||
QVBoxLayout,
|
||||
QPushButton,
|
||||
)
|
||||
from PySide2.QtGui import QTextCursor
|
||||
|
||||
from qt_ui.logging_handler import HookableInMemoryHandler
|
||||
|
||||
|
||||
class QLogsWindow(QDialog):
|
||||
vbox: QVBoxLayout
|
||||
textbox: QPlainTextEdit
|
||||
clear_button: QPushButton
|
||||
_logging_handler: typing.Optional[HookableInMemoryHandler]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Logs")
|
||||
self.setMinimumSize(400, 100)
|
||||
self.resize(1000, 450)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.textbox = QPlainTextEdit(self)
|
||||
self.textbox.setReadOnly(True)
|
||||
self.textbox.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
self.textbox.move(10, 10)
|
||||
self.textbox.resize(1000, 450)
|
||||
self.textbox.setStyleSheet(
|
||||
"font-family: 'Courier New', monospace; background: #1D2731;"
|
||||
)
|
||||
self.vbox.addWidget(self.textbox)
|
||||
|
||||
self.clear_button = QPushButton(self)
|
||||
self.clear_button.setText("CLEAR")
|
||||
self.clear_button.setProperty("style", "btn-primary")
|
||||
self.clear_button.clicked.connect(self.clearLogs)
|
||||
self.vbox.addWidget(self.clear_button)
|
||||
|
||||
self._logging_handler = None
|
||||
logger = logging.getLogger()
|
||||
for handler in logger.handlers:
|
||||
if isinstance(handler, HookableInMemoryHandler):
|
||||
self._logging_handler = handler
|
||||
break
|
||||
if self._logging_handler is not None:
|
||||
self.textbox.setPlainText(self._logging_handler.log)
|
||||
self.textbox.moveCursor(QTextCursor.End)
|
||||
self._logging_handler.setHook(self.appendLog)
|
||||
else:
|
||||
self.textbox.setPlainText("WARNING: logging not initialized!")
|
||||
|
||||
def clearLogs(self) -> None:
|
||||
if self._logging_handler is not None:
|
||||
self._logging_handler.clearLog()
|
||||
self.textbox.setPlainText("")
|
||||
|
||||
def appendLog(self, msg: str):
|
||||
self.textbox.appendPlainText(msg)
|
||||
self.textbox.moveCursor(QTextCursor.End)
|
||||
@@ -180,7 +180,7 @@ class QPackageDialog(QDialog):
|
||||
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
|
||||
self.package_model.package, self.game.blue, self.game.theater
|
||||
)
|
||||
try:
|
||||
planner.populate_flight_plan(flight)
|
||||
|
||||
@@ -38,7 +38,7 @@ class QFlightCreator(QDialog):
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.custom_name_text = None
|
||||
self.country = self.game.player_country
|
||||
self.country = self.game.blue.country_name
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
@@ -52,7 +52,6 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.aircraft_selector = QAircraftTypeSelector(
|
||||
self.game.aircraft_inventory.available_types_for_player,
|
||||
self.game.player_country,
|
||||
self.task_selector.currentData(),
|
||||
)
|
||||
self.aircraft_selector.setCurrentIndex(0)
|
||||
|
||||
@@ -47,8 +47,20 @@ class QFlightPayloadTab(QFrame):
|
||||
def reload_from_flight(self) -> None:
|
||||
self.loadout_selector.setCurrentText(self.flight.loadout.name)
|
||||
|
||||
def loadout_at(self, index: int) -> Loadout:
|
||||
loadout = self.loadout_selector.itemData(index)
|
||||
if loadout is None:
|
||||
return Loadout.empty_loadout()
|
||||
return loadout
|
||||
|
||||
def current_loadout(self) -> Loadout:
|
||||
loadout = self.loadout_selector.currentData()
|
||||
if loadout is None:
|
||||
return Loadout.empty_loadout()
|
||||
return loadout
|
||||
|
||||
def on_new_loadout(self, index: int) -> None:
|
||||
self.flight.loadout = self.loadout_selector.itemData(index)
|
||||
self.flight.loadout = self.loadout_at(index)
|
||||
self.payload_editor.reset_pylons()
|
||||
|
||||
def on_custom_toggled(self, use_custom: bool) -> None:
|
||||
@@ -56,5 +68,5 @@ class QFlightPayloadTab(QFrame):
|
||||
if use_custom:
|
||||
self.flight.loadout = self.flight.loadout.derive_custom("Custom")
|
||||
else:
|
||||
self.flight.loadout = self.loadout_selector.currentData()
|
||||
self.flight.loadout = self.current_loadout()
|
||||
self.payload_editor.reset_pylons()
|
||||
|
||||
@@ -56,7 +56,7 @@ class QPylonEditor(QComboBox):
|
||||
#
|
||||
# A similar hack exists in Pylon to support forcibly equipping this even when
|
||||
# it's not known to be compatible.
|
||||
if weapon.cls_id == "<CLEAN>":
|
||||
if weapon.clsid == "<CLEAN>":
|
||||
if not self.has_added_clean_item:
|
||||
self.addItem("Clean", weapon)
|
||||
self.has_added_clean_item = True
|
||||
|
||||
@@ -100,6 +100,6 @@ class FlightAirfieldDisplay(QGroupBox):
|
||||
|
||||
def update_flight_plan(self) -> None:
|
||||
planner = FlightPlanBuilder(
|
||||
self.game, self.package_model.package, is_player=True
|
||||
self.package_model.package, self.game.blue, self.game.theater
|
||||
)
|
||||
planner.populate_flight_plan(self.flight)
|
||||
|
||||
@@ -37,7 +37,7 @@ class QFlightWaypointTab(QFrame):
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
|
||||
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
|
||||
|
||||
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
|
||||
self.rtb_waypoint: Optional[QPushButton] = None
|
||||
|
||||
@@ -15,6 +15,7 @@ from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSe
|
||||
from game.factions.faction import Faction
|
||||
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs, CurrencySpinner
|
||||
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
||||
from qt_ui.windows.newgame.QCampaignList import (
|
||||
Campaign,
|
||||
QCampaignList,
|
||||
@@ -125,6 +126,10 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
)
|
||||
self.generatedGame = generator.generate()
|
||||
|
||||
AirWingConfigurationDialog(self.generatedGame, self).exec_()
|
||||
|
||||
self.generatedGame.begin_turn_0()
|
||||
|
||||
super(NewGameWizard, self).accept()
|
||||
|
||||
|
||||
|
||||
67
qt_ui/windows/notes/QNotesWindow.py
Normal file
67
qt_ui/windows/notes/QNotesWindow.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QPlainTextEdit,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QLabel,
|
||||
)
|
||||
from PySide2.QtGui import QTextCursor
|
||||
from PySide2.QtCore import QTimer
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
|
||||
from time import sleep
|
||||
|
||||
|
||||
class QNotesWindow(QDialog):
|
||||
def __init__(self, game: Game):
|
||||
super(QNotesWindow, self).__init__()
|
||||
|
||||
self.game = game
|
||||
self.setWindowTitle("Notes")
|
||||
self.setWindowIcon(CONST.ICONS["Notes"])
|
||||
self.setMinimumSize(400, 100)
|
||||
self.resize(600, 450)
|
||||
|
||||
self.vbox = QVBoxLayout()
|
||||
self.setLayout(self.vbox)
|
||||
|
||||
self.vbox.addWidget(
|
||||
QLabel("Saved notes are available as a page in your kneeboard.")
|
||||
)
|
||||
|
||||
self.textbox = QPlainTextEdit(self)
|
||||
try:
|
||||
self.textbox.setPlainText(self.game.notes)
|
||||
self.textbox.moveCursor(QTextCursor.End)
|
||||
except AttributeError: # old save may not have game.notes
|
||||
pass
|
||||
self.textbox.move(10, 10)
|
||||
self.textbox.resize(600, 450)
|
||||
self.textbox.setStyleSheet("background: #1D2731;")
|
||||
self.vbox.addWidget(self.textbox)
|
||||
|
||||
self.button_row = QHBoxLayout()
|
||||
self.vbox.addLayout(self.button_row)
|
||||
|
||||
self.clear_button = QPushButton(self)
|
||||
self.clear_button.setText("CLEAR")
|
||||
self.clear_button.setProperty("style", "btn-primary")
|
||||
self.clear_button.clicked.connect(self.clearNotes)
|
||||
self.button_row.addWidget(self.clear_button)
|
||||
|
||||
self.save_button = QPushButton(self)
|
||||
self.save_button.setText("SAVE")
|
||||
self.save_button.setProperty("style", "btn-success")
|
||||
self.save_button.clicked.connect(self.saveNotes)
|
||||
self.button_row.addWidget(self.save_button)
|
||||
|
||||
def clearNotes(self) -> None:
|
||||
self.textbox.setPlainText("")
|
||||
|
||||
def saveNotes(self) -> None:
|
||||
self.game.notes = self.textbox.toPlainText()
|
||||
self.save_button.setText("SAVED")
|
||||
QTimer.singleShot(5000, lambda: self.save_button.setText("SAVE"))
|
||||
@@ -58,6 +58,12 @@ class QLiberationFirstStartWindow(QDialog):
|
||||
|
||||
<p>As you click on the button below, the file will be replaced in your DCS installation directory.</p>
|
||||
|
||||
<br/>
|
||||
<p>If you leave the DCS Installation Directory empty, DCS Liberation can not automatically replace the MissionScripting.lua and will therefore not work correctly!
|
||||
In this case, you need to edit the file yourself. The easiest way to do it is to replace the original file with the file in dcs-liberation distribution (<dcs_liberation_installation>/resources/scripts/MissionScripting.lua).
|
||||
<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>
|
||||
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<strong>Thank you for reading !</strong>
|
||||
|
||||
@@ -22,6 +22,7 @@ class QLiberationPreferences(QFrame):
|
||||
super(QLiberationPreferences, self).__init__()
|
||||
self.saved_game_dir = ""
|
||||
self.dcs_install_dir = ""
|
||||
self.install_dir_ignore_warning = False
|
||||
|
||||
self.dcs_install_dir = liberation_install.get_dcs_install_directory()
|
||||
self.saved_game_dir = liberation_install.get_saved_game_dir()
|
||||
@@ -102,17 +103,38 @@ class QLiberationPreferences(QFrame):
|
||||
error_dialog.exec_()
|
||||
return False
|
||||
|
||||
if not os.path.isdir(self.dcs_install_dir):
|
||||
if self.install_dir_ignore_warning and self.dcs_install_dir == "":
|
||||
warning_dialog = QMessageBox.warning(
|
||||
self,
|
||||
"The DCS Installation directory was not set",
|
||||
"You set an empty DCS Installation directory! "
|
||||
"<br/><br/>Without this directory, DCS Liberation can not replace the MissionScripting.lua for you and will not work properly. "
|
||||
"In this case, you need to edit the MissionScripting.lua yourself. The easiest way to do it is to replace the original file (<dcs_installation_directory>/Scripts/MissionScripting.lua) with the file in dcs-liberation distribution (<dcs_liberation_installation>/resources/scripts/MissionScripting.lua)."
|
||||
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>"
|
||||
"<br/><br/>Are you sure that you want to leave the installation directory empty?"
|
||||
"<br/><br/><strong>This is only recommended for expert users!</strong>",
|
||||
QMessageBox.StandardButton.Yes,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
if warning_dialog == QMessageBox.No:
|
||||
return False
|
||||
elif not os.path.isdir(self.dcs_install_dir):
|
||||
error_dialog = QMessageBox.critical(
|
||||
self,
|
||||
"Wrong DCS installation directory.",
|
||||
self.dcs_install_dir + " is not a valid directory",
|
||||
self.dcs_install_dir
|
||||
+ " is not a valid directory. DCS Liberation requires the installation directory to replace the MissionScripting.lua"
|
||||
"<br/><br/>If you ignore this Error, DCS Liberation can not work properly and needs your attention. "
|
||||
"In this case, you need to edit the MissionScripting.lua yourself. The easiest way to do it is to replace the original file (<dcs_installation_directory>/Scripts/MissionScripting.lua) with the file in dcs-liberation distribution (<dcs_liberation_installation>/resources/scripts/MissionScripting.lua)."
|
||||
"<br/><br/>You can find more information on how to manually change this file in the Liberation Wiki (Page: Dedicated Server Guide) on GitHub.</p>"
|
||||
"<br/><br/><strong>This is only recommended for expert users!</strong>",
|
||||
QMessageBox.StandardButton.Ignore,
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
error_dialog.exec_()
|
||||
if error_dialog == QMessageBox.Ignore:
|
||||
self.install_dir_ignore_warning = True
|
||||
return False
|
||||
|
||||
if not os.path.isdir(
|
||||
elif not os.path.isdir(
|
||||
os.path.join(self.dcs_install_dir, "Scripts")
|
||||
) and os.path.isfile(os.path.join(self.dcs_install_dir, "bin", "DCS.exe")):
|
||||
error_dialog = QMessageBox.critical(
|
||||
|
||||
@@ -101,7 +101,7 @@ class HqAutomationSettingsBox(QGroupBox):
|
||||
|
||||
front_line = QCheckBox()
|
||||
front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
|
||||
front_line.toggled.connect(self.set_front_line_automation)
|
||||
front_line.toggled.connect(self.set_front_line_reinforcement_automation)
|
||||
|
||||
layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
|
||||
layout.addWidget(front_line, 1, 1, Qt.AlignRight)
|
||||
@@ -147,12 +147,30 @@ class HqAutomationSettingsBox(QGroupBox):
|
||||
)
|
||||
layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight)
|
||||
|
||||
self.automate_front_line_stance = QCheckBox()
|
||||
self.automate_front_line_stance.setChecked(
|
||||
self.game.settings.automate_front_line_stance
|
||||
)
|
||||
self.automate_front_line_stance.toggled.connect(
|
||||
self.set_front_line_stance_automation
|
||||
)
|
||||
|
||||
layout.addWidget(
|
||||
QLabel("Automatically manage front line stances"),
|
||||
5,
|
||||
0,
|
||||
)
|
||||
layout.addWidget(self.automate_front_line_stance, 5, 1, Qt.AlignRight)
|
||||
|
||||
def set_runway_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_runway_repair = value
|
||||
|
||||
def set_front_line_automation(self, value: bool) -> None:
|
||||
def set_front_line_reinforcement_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_front_line_reinforcements = value
|
||||
|
||||
def set_front_line_stance_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_front_line_stance = value
|
||||
|
||||
def set_aircraft_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_aircraft_reinforcements = value
|
||||
|
||||
@@ -855,7 +873,7 @@ class QSettingsWindow(QDialog):
|
||||
|
||||
def cheatMoney(self, amount):
|
||||
logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M")
|
||||
self.game.budget += amount
|
||||
self.game.blue.budget += amount
|
||||
if amount > 0:
|
||||
self.game.informations.append(
|
||||
Information(
|
||||
|
||||
@@ -42,10 +42,16 @@ class QAircraftChart(QFrame):
|
||||
self.chart.setTitle("Aircraft forces over time")
|
||||
|
||||
self.chart.createDefaultAxes()
|
||||
self.chart.axisX().setTitleText("Turn")
|
||||
self.chart.axisX().setLabelFormat("%i")
|
||||
self.chart.axisX().setRange(0, len(self.alliedAircraft))
|
||||
self.chart.axisX().applyNiceNumbers()
|
||||
|
||||
self.chart.axisY().setLabelFormat("%i")
|
||||
self.chart.axisY().setRange(
|
||||
0, max(max(self.alliedAircraft), max(self.enemyAircraft)) + 10
|
||||
)
|
||||
self.chart.axisY().applyNiceNumbers()
|
||||
|
||||
self.chartView = QtCharts.QChartView(self.chart)
|
||||
self.chartView.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
@@ -42,10 +42,16 @@ class QArmorChart(QFrame):
|
||||
self.chart.setTitle("Combat vehicles over time")
|
||||
|
||||
self.chart.createDefaultAxes()
|
||||
self.chart.axisX().setTitleText("Turn")
|
||||
self.chart.axisX().setLabelFormat("%i")
|
||||
self.chart.axisX().setRange(0, len(self.alliedArmor))
|
||||
self.chart.axisX().applyNiceNumbers()
|
||||
|
||||
self.chart.axisY().setLabelFormat("%i")
|
||||
self.chart.axisY().setRange(
|
||||
0, max(max(self.alliedArmor), max(self.enemyArmor)) + 10
|
||||
)
|
||||
self.chart.axisY().applyNiceNumbers()
|
||||
|
||||
self.chartView = QtCharts.QChartView(self.chart)
|
||||
self.chartView.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
@@ -14,7 +14,7 @@ class QStatsWindow(QDialog):
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Stats")
|
||||
self.setWindowIcon(CONST.ICONS["Statistics"])
|
||||
self.setMinimumSize(600, 250)
|
||||
self.setMinimumSize(600, 300)
|
||||
|
||||
self.layout = QGridLayout()
|
||||
self.aircraft_charts = QAircraftChart(self.game)
|
||||
|
||||
Reference in New Issue
Block a user