Allow per pilot loadouts and properties.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3092.
This commit is contained in:
Dan Albert 2023-07-20 21:49:21 -07:00 committed by Raffson
parent 8e670e1a3c
commit 485229b92f
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
27 changed files with 475 additions and 115 deletions

View File

@ -209,6 +209,7 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
## Features/Improvements ## Features/Improvements
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone. * **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts.
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate. * **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
* **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation. * **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation.
* **[UI]** Added cheats for instantly repairing and destroying runways. * **[UI]** Added cheats for instantly repairing and destroying runways.

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from collections.abc import Iterator
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, List, Optional, TYPE_CHECKING from typing import Any, List, Optional, TYPE_CHECKING
@ -9,10 +10,11 @@ from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from pydcs_extensions.hercules.hercules import Hercules from pydcs_extensions.hercules.hercules import Hercules
from .flightmembers import FlightMembers
from .flightroster import FlightRoster from .flightroster import FlightRoster
from .flightstate import FlightState, Navigating, Uninitialized from .flightstate import FlightState, Navigating, Uninitialized
from .flightstate.killed import Killed from .flightstate.killed import Killed
from .loadouts import Loadout, Weapon from .loadouts import Weapon
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
from ..radio.TacanContainer import TacanContainer from ..radio.TacanContainer import TacanContainer
from ..radio.radios import RadioFrequency from ..radio.radios import RadioFrequency
@ -31,6 +33,8 @@ if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot from game.squadrons import Squadron, Pilot
from game.theater import ControlPoint from game.theater import ControlPoint
from game.transfers import TransferOrder from game.transfers import TransferOrder
from game.data.weapons import WeaponType
from .flightmember import FlightMember
from .flightplans.flightplan import FlightPlan from .flightplans.flightplan import FlightPlan
from .flighttype import FlightType from .flighttype import FlightType
from .flightwaypoint import FlightWaypoint from .flightwaypoint import FlightWaypoint
@ -61,21 +65,16 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
self.package = package self.package = package
self.coalition = squadron.coalition self.coalition = squadron.coalition
self.squadron = squadron self.squadron = squadron
self.flight_type = flight_type
if claim_inv: if claim_inv:
self.squadron.claim_inventory(count) self.squadron.claim_inventory(count)
if roster is None: if roster is None:
self.roster = FlightRoster(self.squadron, initial_size=count) self.roster = FlightMembers(self, initial_size=count)
else: else:
self.roster = roster self.roster = FlightMembers.from_roster(self, roster)
self.divert = divert self.divert = divert
self.flight_type = flight_type
self.loadout = Loadout.default_for(self)
if self.squadron.aircraft.name == "F-15I Ra'am":
self.loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
self.start_type = start_type self.start_type = start_type
self.use_custom_loadout = False
self.custom_name = custom_name self.custom_name = custom_name
self.group_id: int = 0 self.group_id: int = 0
@ -85,6 +84,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
self.tcn_name = callsign self.tcn_name = callsign
self.initialize_fuel() self.initialize_fuel()
self.use_same_loadout_for_all_members = True
# Only used by transport missions. # Only used by transport missions.
self.cargo = cargo self.cargo = cargo
@ -138,7 +138,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
state["state"] = Uninitialized(self, state["squadron"].settings) state["state"] = Uninitialized(self, state["squadron"].settings)
if "use_same_loadout_for_all_members" not in state:
state["use_same_loadout_for_all_members"] = True
self.__dict__.update(state) self.__dict__.update(state)
if isinstance(self.roster, FlightRoster):
self.roster = FlightMembers.from_roster(self, self.roster)
@property @property
def blue(self) -> bool: def blue(self) -> bool:
@ -206,6 +210,9 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
def missing_pilots(self) -> int: def missing_pilots(self) -> int:
return self.roster.missing_pilots return self.roster.missing_pilots
def iter_members(self) -> Iterator[FlightMember]:
yield from self.roster.members
def set_flight_type(self, var: FlightType) -> None: def set_flight_type(self, var: FlightType) -> None:
self.flight_type = var self.flight_type = var
@ -235,6 +242,11 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo: elif self.departure.cptype.name in ["FARP", "FOB"] and not self.is_helo:
self.fuel = unit_type.fuel_max * 0.75 self.fuel = unit_type.fuel_max * 0.75
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
return any(
m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members()
)
def __repr__(self) -> str: def __repr__(self) -> str:
return self.__str__() return self.__str__()
@ -294,7 +306,7 @@ class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
Killed(self.state.estimate_position(), self, self.squadron.settings) Killed(self.state.estimate_position(), self, self.squadron.settings)
) )
events.update_flight(self) events.update_flight(self)
for pilot in self.roster.pilots: for pilot in self.roster.iter_pilots():
if pilot is not None: if pilot is not None:
results.kill_pilot(self, pilot) results.kill_pilot(self, pilot)

22
game/ato/flightmember.py Normal file
View File

@ -0,0 +1,22 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from game.ato.loadouts import Loadout
if TYPE_CHECKING:
from game.squadrons import Pilot
class FlightMember:
def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None:
self.pilot = pilot
self.loadout = loadout
self.use_custom_loadout = False
self.properties: dict[str, bool | float | int] = {}
@property
def is_player(self) -> bool:
if self.pilot is None:
return False
return self.pilot.player

89
game/ato/flightmembers.py Normal file
View File

@ -0,0 +1,89 @@
from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING
from .flightmember import FlightMember
from .flightroster import FlightRoster
from .iflightroster import IFlightRoster
from .loadouts import Loadout
from ..data.weapons import Weapon
if TYPE_CHECKING:
from game.squadrons import Pilot
from .flight import Flight
class FlightMembers(IFlightRoster):
def __init__(self, flight: Flight, initial_size: int = 0) -> None:
self.flight = flight
self.members: list[FlightMember] = []
self.resize(initial_size)
@staticmethod
def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers:
members = FlightMembers(flight)
loadout = Loadout.default_for(flight)
if flight.squadron.aircraft.name == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
members.members = [FlightMember(p, loadout) for p in roster.pilots]
return members
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from (m.pilot for m in self.members)
def pilot_at(self, idx: int) -> Pilot | None:
return self.members[idx].pilot
@property
def max_size(self) -> int:
return len(self.members)
@property
def player_count(self) -> int:
return len([m for m in self.members if m.is_player])
@property
def missing_pilots(self) -> int:
return len([m for m in self.members if m.pilot is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.flight.squadron.return_pilots(
[m.pilot for m in self.members[new_size:] if m.pilot is not None]
)
self.members = self.members[:new_size]
return
if self.max_size:
loadout = self.members[0].loadout.clone()
else:
loadout = Loadout.default_for(self.flight)
if self.flight.squadron.aircraft.name == "F-15I Ra'am":
loadout.pylons[16] = Weapon.with_clsid(
"{IDF_MODS_PROJECT_F-15I_Raam_Dome}"
)
for _ in range(new_size - self.max_size):
member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout)
member.use_custom_loadout = loadout.is_custom
self.members.append(member)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.flight.squadron.claim_pilot(pilot)
if (current_pilot := self.pilot_at(index)) is not None:
self.flight.squadron.return_pilot(current_pilot)
self.members[index].pilot = pilot
def clear(self) -> None:
self.flight.squadron.return_pilots(
[p for p in self.iter_pilots() if p is not None]
)
def use_same_loadout_for_all_members(self) -> None:
if not self.members:
return
loadout = self.members[0].loadout
for member in self.members[1:]:
member.loadout = loadout.clone()

View File

@ -1,29 +1,30 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterator
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from game.ato.iflightroster import IFlightRoster
if TYPE_CHECKING: if TYPE_CHECKING:
from game.squadrons import Squadron, Pilot from game.squadrons import Squadron, Pilot
class FlightRoster: class FlightRoster(IFlightRoster):
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None: def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron self.squadron = squadron
self.pilots: list[Optional[Pilot]] = [] self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size) self.resize(initial_size)
def iter_pilots(self) -> Iterator[Pilot | None]:
yield from self.pilots
def pilot_at(self, idx: int) -> Pilot | None:
return self.pilots[idx]
@property @property
def max_size(self) -> int: def max_size(self) -> int:
return len(self.pilots) return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None: def resize(self, new_size: int) -> None:
if self.max_size > new_size: if self.max_size > new_size:
self.squadron.return_pilots( self.squadron.return_pilots(

34
game/ato/iflightroster.py Normal file
View File

@ -0,0 +1,34 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING, Iterator
if TYPE_CHECKING:
from game.squadrons import Pilot
class IFlightRoster(ABC):
@abstractmethod
def iter_pilots(self) -> Iterator[Pilot | None]:
...
@abstractmethod
def pilot_at(self, idx: int) -> Pilot | None:
...
@property
@abstractmethod
def max_size(self) -> int:
...
@abstractmethod
def resize(self, new_size: int) -> None:
...
@abstractmethod
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
...
@abstractmethod
def clear(self) -> None:
...

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import copy
import datetime import datetime
import logging import logging
from collections.abc import Iterable from collections.abc import Iterable
@ -35,6 +36,11 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout: def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True) return Loadout(name, self.pylons, self.date, is_custom=True)
def clone(self) -> Loadout:
return Loadout(
self.name, dict(self.pylons), copy.deepcopy(self.date), self.is_custom
)
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool: def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
for weapon in self.pylons.values(): for weapon in self.pylons.values():
if weapon is not None and weapon.weapon_group.type is weapon_type: if weapon is not None and weapon.weapon_group.type is weapon_type:

View File

@ -10,7 +10,7 @@ from pathlib import Path
from typing import Iterator, Optional, Any, ClassVar from typing import Iterator, Optional, Any, ClassVar
import yaml import yaml
from dcs.unitgroup import FlyingGroup from dcs.flyingunit import FlyingUnit
from dcs.weapons_data import weapon_ids from dcs.weapons_data import weapon_ids
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -278,10 +278,10 @@ class Pylon:
# configuration. # configuration.
return weapon in self.allowed or weapon.clsid == "<CLEAN>" return weapon in self.allowed or weapon.clsid == "<CLEAN>"
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None: def equip(self, unit: FlyingUnit, weapon: Weapon) -> None:
if not self.can_equip(weapon): if not self.can_equip(weapon):
logging.error(f"Pylon {self.number} cannot equip {weapon.name}") logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number) unit.load_pylon(self.make_pydcs_assignment(weapon), self.number)
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment: def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
return self.number, weapon.pydcs_data return self.number, weapon.pydcs_data

View File

@ -27,6 +27,7 @@ from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter from .aircraftpainter import AircraftPainter
from .flightdata import FlightData from .flightdata import FlightData
from .waypoints import WaypointGenerator from .waypoints import WaypointGenerator
from ...ato.flightmember import FlightMember
from ...ato.flightplans.aewc import AewcFlightPlan from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan from ...ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
@ -67,13 +68,13 @@ class FlightGroupConfigurator:
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group) AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
AircraftPainter(self.flight, self.group).apply_livery() AircraftPainter(self.flight, self.group).apply_livery()
self.setup_props() self.setup_props()
self.setup_payload() self.setup_payloads()
self.setup_fuel() self.setup_fuel()
flight_channel = self.setup_radios() flight_channel = self.setup_radios()
laser_codes: list[Optional[int]] = [] laser_codes: list[Optional[int]] = []
for unit, pilot in zip(self.group.units, self.flight.roster.pilots): for unit, member in zip(self.group.units, self.flight.iter_members()):
self.configure_flight_member(unit, pilot, laser_codes) self.configure_flight_member(unit, member, laser_codes)
divert = None divert = None
if self.flight.divert is not None: if self.flight.divert is not None:
@ -143,21 +144,20 @@ class FlightGroupConfigurator:
) )
def configure_flight_member( def configure_flight_member(
self, unit: FlyingUnit, pilot: Optional[Pilot], laser_codes: list[Optional[int]] self, unit: FlyingUnit, member: FlightMember, laser_codes: list[Optional[int]]
) -> None: ) -> None:
player = pilot is not None and pilot.player self.set_skill(unit, member)
self.set_skill(unit, pilot) if member.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and member.is_player:
if self.flight.loadout.has_weapon_of_type(WeaponTypeEnum.TGP) and player:
laser_codes.append(self.laser_code_registry.get_next_laser_code()) laser_codes.append(self.laser_code_registry.get_next_laser_code())
else: else:
laser_codes.append(None) laser_codes.append(None)
settings = self.flight.coalition.game.settings settings = self.flight.coalition.game.settings
if not player or not settings.plugins.get("ewrj"): if not member.is_player or not settings.plugins.get("ewrj"):
return return
jammer_required = settings.plugin_option("ewrj.ecm_required") jammer_required = settings.plugin_option("ewrj.ecm_required")
if jammer_required: if jammer_required:
ecm = WeaponTypeEnum.JAMMER ecm = WeaponTypeEnum.JAMMER
if not self.flight.loadout.has_weapon_of_type(ecm): if not member.loadout.has_weapon_of_type(ecm):
return return
ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}") ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}")
ewrj_menu_trigger.add_action(DoScript(String(f'EWJamming("{unit.name}")'))) ewrj_menu_trigger.add_action(DoScript(String(f'EWJamming("{unit.name}")')))
@ -221,9 +221,9 @@ class FlightGroupConfigurator:
) )
) )
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> None: def set_skill(self, unit: FlyingUnit, member: FlightMember) -> None:
if pilot is None or not pilot.player: if not member.is_player:
unit.skill = self.skill_level_for(unit, pilot) unit.skill = self.skill_level_for(unit, member.pilot)
return return
if self.use_client or "Pilot #1" not in unit.name: if self.use_client or "Pilot #1" not in unit.name:
@ -260,15 +260,18 @@ class FlightGroupConfigurator:
return levels[new_level] return levels[new_level]
def setup_props(self) -> None: def setup_props(self) -> None:
for prop_id, value in self.flight.props.items(): for unit, member in zip(self.group.units, self.flight.iter_members()):
for unit in self.group.units: for prop_id, value in member.properties.items():
unit.set_property(prop_id, value) unit.set_property(prop_id, value)
def setup_payload(self) -> None: def setup_payloads(self) -> None:
for p in self.group.units: for unit, member in zip(self.group.units, self.flight.iter_members()):
p.pylons.clear() self.setup_payload(unit, member)
loadout = self.flight.loadout def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None:
unit.pylons.clear()
loadout = member.loadout
if self.game.settings.restrict_weapons_by_date: if self.game.settings.restrict_weapons_by_date:
loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date) loadout = loadout.degrade_for_date(self.flight.unit_type, self.game.date)
@ -276,7 +279,7 @@ class FlightGroupConfigurator:
if weapon is None: if weapon is None:
continue continue
pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number) pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number)
pylon.equip(self.group, weapon) pylon.equip(unit, weapon)
def setup_fuel(self) -> None: def setup_fuel(self) -> None:
fuel = self.flight.state.estimate_fuel() fuel = self.flight.state.estimate_fuel()
@ -287,5 +290,8 @@ class FlightGroupConfigurator:
"starting fuel to 100kg." "starting fuel to 100kg."
) )
fuel = 100 fuel = 100
for unit, pilot in zip(self.group.units, self.flight.roster.pilots): for unit, pilot in zip(self.group.units, self.flight.roster.iter_pilots()):
unit.fuel = fuel if pilot is not None and pilot.player:
unit.fuel = fuel
else:
unit.fuel = self.flight.fuel

View File

@ -29,7 +29,7 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
# for more details. # for more details.
# The LGB work around assumes the Airfield position in DCS is on a runway, which seems # The LGB work around assumes the Airfield position in DCS is on a runway, which seems
# to be the case for most if not all airfields. # to be the case for most if not all airfields.
if self.flight.loadout.has_weapon_of_type(WeaponType.LGB): if self.flight.any_member_has_weapon_of_type(WeaponType.LGB):
waypoint.tasks.append( waypoint.tasks.append(
Bombing( Bombing(
position=target.position, position=target.position,

View File

@ -53,7 +53,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
) )
waypoint.tasks.append(attack_task) waypoint.tasks.append(attack_task)
if self.flight.loadout.has_weapon_of_type(WeaponType.ARM): if self.flight.any_member_has_weapon_of_type(WeaponType.ARM):
# Special handling for ARM Weapon types: # Special handling for ARM Weapon types:
# The SEAD flight will Search for the targeted group and then engage it # The SEAD flight will Search for the targeted group and then engage it
# if it is found only. This will prevent AI from having huge problems # if it is found only. This will prevent AI from having huge problems

View File

@ -60,7 +60,7 @@ class MissionResultsProcessor:
def _commit_pilot_experience(ato: AirTaskingOrder) -> None: def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
for idx, pilot in enumerate(flight.roster.pilots): for idx, pilot in enumerate(flight.roster.iter_pilots()):
if pilot is None: if pilot is None:
logging.error( logging.error(
f"Cannot award experience to pilot #{idx} of {flight} " f"Cannot award experience to pilot #{idx} of {flight} "

View File

@ -68,7 +68,7 @@ class UnitMap:
self.airlifts: Dict[str, AirliftUnits] = {} self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None: def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None:
for pilot, unit in zip(flight.roster.pilots, group.units): for pilot, unit in zip(flight.roster.iter_pilots(), group.units):
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)

13
qt_ui/blocksignals.py Normal file
View File

@ -0,0 +1,13 @@
from collections.abc import Iterator
from contextlib import contextmanager
from PySide6.QtWidgets import QWidget
@contextmanager
def block_signals(widget: QWidget) -> Iterator[None]:
blocked = widget.blockSignals(True)
try:
yield
finally:
widget.blockSignals(blocked)

View File

@ -121,7 +121,7 @@ class AircraftInventoryData:
flight_type = flight.flight_type.value flight_type = flight.flight_type.value
target = flight.package.target.name target = flight.package.target.name
for idx in range(0, num_units): for idx in range(0, num_units):
pilot = flight.roster.pilots[idx] pilot = flight.roster.pilot_at(idx)
if pilot is None: if pilot is None:
pilot_name = "Unassigned" pilot_name = "Unassigned"
player = "" player = ""

View File

@ -89,9 +89,10 @@ class QFlightCreator(QDialog):
roster = None roster = None
else: else:
roster = FlightRoster( roster = FlightRoster(
squadron, initial_size=self.flight_size_spinner.value() squadron,
initial_size=self.flight_size_spinner.value(),
) )
self.roster_editor = FlightRosterEditor(roster) self.roster_editor = FlightRosterEditor(squadron, roster)
self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize) self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize)
self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed) self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
roster_layout = QHBoxLayout() roster_layout = QHBoxLayout()
@ -232,7 +233,7 @@ class QFlightCreator(QDialog):
self.roster_editor.replace(None) self.roster_editor.replace(None)
if squadron is not None: if squadron is not None:
self.roster_editor.replace( self.roster_editor.replace(
FlightRoster(squadron, self.flight_size_spinner.value()) squadron, FlightRoster(squadron, self.flight_size_spinner.value())
) )
self.on_departure_changed(squadron.location) self.on_departure_changed(squadron.location)

View File

@ -19,6 +19,11 @@ class QFlightPlanner(QTabWidget):
self.general_settings_tab = QGeneralFlightSettingsTab( self.general_settings_tab = QGeneralFlightSettingsTab(
gm, package_model, flight, self.waypoint_tab.flight_waypoint_list gm, package_model, flight, self.waypoint_tab.flight_waypoint_list
) )
self.payload_tab = QFlightPayloadTab(flight, gm.game)
self.general_settings_tab.flight_size_changed.connect(
self.payload_tab.resize_for_flight
)
self.waypoint_tab = QFlightWaypointTab(gm.game, package_model.package, flight)
self.waypoint_tab.loadout_changed.connect(self.payload_tab.reload_from_flight) self.waypoint_tab.loadout_changed.connect(self.payload_tab.reload_from_flight)
self.addTab(self.general_settings_tab, "General Flight settings") self.addTab(self.general_settings_tab, "General Flight settings")
self.addTab(self.payload_tab, "Payload") self.addTab(self.payload_tab, "Payload")

View File

@ -9,26 +9,41 @@ from PySide2.QtWidgets import (
QWidget, QWidget,
QSpinBox, QSpinBox,
QSlider, QSlider,
QCheckBox,
) )
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightmember import FlightMember
from game.ato.loadouts import Loadout from game.ato.loadouts import Loadout
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from .QLoadoutEditor import QLoadoutEditor from .QLoadoutEditor import QLoadoutEditor
from .propertyeditor import PropertyEditor from .propertyeditor import PropertyEditor
class DcsLoadoutSelector(QComboBox): class DcsLoadoutSelector(QComboBox):
def __init__(self, flight: Flight) -> None: def __init__(self, flight: Flight, member: FlightMember) -> None:
super().__init__() super().__init__()
for loadout in Loadout.iter_for(flight): for loadout in Loadout.iter_for(flight):
self.addItem(loadout.name, loadout) self.addItem(loadout.name, loadout)
self.model().sort(0) self.model().sort(0)
self.setDisabled(flight.loadout.is_custom) self.setDisabled(member.loadout.is_custom)
if flight.loadout.is_custom: if member.loadout.is_custom:
self.setCurrentText(Loadout.default_for(flight).name) self.setCurrentText(Loadout.default_for(flight).name)
else: else:
self.setCurrentText(flight.loadout.name) self.setCurrentText(member.loadout.name)
class FlightMemberSelector(QSpinBox):
def __init__(self, flight: Flight, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.flight = flight
self.setMinimum(0)
self.setMaximum(flight.count - 1)
@property
def selected_member(self) -> FlightMember:
return self.flight.roster.members[self.value()]
class DcsFuelSelector(QHBoxLayout): class DcsFuelSelector(QHBoxLayout):
@ -99,12 +114,41 @@ class QFlightPayloadTab(QFrame):
def __init__(self, flight: Flight, game: Game): def __init__(self, flight: Flight, game: Game):
super(QFlightPayloadTab, self).__init__() super(QFlightPayloadTab, self).__init__()
self.flight = flight self.flight = flight
self.payload_editor = QLoadoutEditor(flight, game) self.payload_editor = QLoadoutEditor(
flight, self.flight.roster.members[0], game
)
self.payload_editor.toggled.connect(self.on_custom_toggled) self.payload_editor.toggled.connect(self.on_custom_toggled)
self.payload_editor.saved.connect(self.on_saved_payload) self.payload_editor.saved.connect(self.on_saved_payload)
layout = QVBoxLayout() layout = QVBoxLayout()
self.member_selector = FlightMemberSelector(self.flight, self)
self.member_selector.valueChanged.connect(self.rebind_to_selected_member)
layout.addLayout(QLabeledWidget("Flight member:", self.member_selector))
self.same_loadout_for_all_checkbox = QCheckBox(
"Use same loadout for all flight members"
)
self.same_loadout_for_all_checkbox.setChecked(
self.flight.use_same_loadout_for_all_members
)
self.same_loadout_for_all_checkbox.toggled.connect(self.on_same_loadout_toggled)
layout.addWidget(self.same_loadout_for_all_checkbox)
layout.addWidget(
QLabel(
"<strong>Warning: AI flights should use the same loadout for all members.</strong>"
)
)
scroll_content = QWidget()
scrolling_layout = QVBoxLayout()
scroll_content.setLayout(scrolling_layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
layout.addWidget(scroll)
# Docs Link # Docs Link
docsText = QLabel( docsText = QLabel(
'<a href="https://github.com/dcs-retribution/dcs-retribution/wiki/Custom-Loadouts"><span style="color:#FFFFFF;">How to create your own default loadout</span></a>' '<a href="https://github.com/dcs-retribution/dcs-retribution/wiki/Custom-Loadouts"><span style="color:#FFFFFF;">How to create your own default loadout</span></a>'
@ -122,6 +166,13 @@ class QFlightPayloadTab(QFrame):
layout.addLayout(self.fuel_selector) layout.addLayout(self.fuel_selector)
self.loadout_selector = DcsLoadoutSelector(flight) self.loadout_selector = DcsLoadoutSelector(flight)
self.property_editor = PropertyEditor(
self.flight, self.member_selector.selected_member
)
scrolling_layout.addLayout(self.property_editor)
self.loadout_selector = DcsLoadoutSelector(
flight, self.member_selector.selected_member
)
self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout) self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout)
layout.addWidget(self.loadout_selector) layout.addWidget(self.loadout_selector)
layout.addWidget(self.payload_editor, stretch=1) layout.addWidget(self.payload_editor, stretch=1)
@ -129,8 +180,27 @@ class QFlightPayloadTab(QFrame):
self.setLayout(layout) self.setLayout(layout)
def resize_for_flight(self) -> None:
self.member_selector.setMaximum(self.flight.count - 1)
def reload_from_flight(self) -> None: def reload_from_flight(self) -> None:
self.loadout_selector.setCurrentText(self.flight.loadout.name) self.loadout_selector.setCurrentText(
self.member_selector.selected_member.loadout.name
)
def rebind_to_selected_member(self) -> None:
member = self.member_selector.selected_member
self.property_editor.set_flight_member(member)
self.loadout_selector.setCurrentText(member.loadout.name)
self.loadout_selector.setDisabled(member.loadout.is_custom)
self.payload_editor.set_flight_member(member)
if self.member_selector.value() != 0:
self.loadout_selector.setDisabled(
self.flight.use_same_loadout_for_all_members
)
self.payload_editor.setDisabled(
self.flight.use_same_loadout_for_all_members
)
def loadout_at(self, index: int) -> Loadout: def loadout_at(self, index: int) -> Loadout:
loadout = self.loadout_selector.itemData(index) loadout = self.loadout_selector.itemData(index)
@ -145,18 +215,30 @@ class QFlightPayloadTab(QFrame):
return loadout return loadout
def on_new_loadout(self, index: int) -> None: def on_new_loadout(self, index: int) -> None:
self.flight.loadout = self.loadout_at(index) self.member_selector.selected_member.loadout = self.loadout_at(index)
self.payload_editor.reset_pylons() self.payload_editor.reset_pylons()
def on_custom_toggled(self, use_custom: bool) -> None: def on_custom_toggled(self, use_custom: bool) -> None:
self.loadout_selector.setDisabled(use_custom) self.loadout_selector.setDisabled(use_custom)
member = self.member_selector.selected_member
member.use_custom_loadout = use_custom
if use_custom: if use_custom:
self.flight.loadout = self.flight.loadout.derive_custom("Custom") member.loadout = member.loadout.derive_custom("Custom")
else: else:
self.flight.loadout = self.current_loadout() member.loadout = self.current_loadout()
self.payload_editor.reset_pylons() self.payload_editor.reset_pylons()
def on_saved_payload(self, payload_name: str) -> None: def on_saved_payload(self, payload_name: str) -> None:
loadout = self.flight.loadout loadout = self.flight.loadout
self.loadout_selector.addItem(payload_name, loadout) self.loadout_selector.addItem(payload_name, loadout)
self.loadout_selector.setCurrentIndex(self.loadout_selector.count() - 1) self.loadout_selector.setCurrentIndex(self.loadout_selector.count() - 1)
def on_same_loadout_toggled(self, checked: bool) -> None:
self.flight.use_same_loadout_for_all_members = checked
if self.member_selector.value():
self.loadout_selector.setDisabled(checked)
self.payload_editor.setDisabled(checked)
if checked:
self.flight.roster.use_same_loadout_for_all_members()
if self.member_selector.value():
self.rebind_to_selected_member()

View File

@ -1,3 +1,4 @@
from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from shutil import copyfile from shutil import copyfile
from typing import Dict, Union from typing import Dict, Union
@ -18,20 +19,23 @@ from dcs import lua
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightmember import FlightMember
from game.data.weapons import Pylon from game.data.weapons import Pylon
from game.persistency import payloads_dir from game.persistency import payloads_dir
from qt_ui.blocksignals import block_signals
from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor
class QLoadoutEditor(QGroupBox): class QLoadoutEditor(QGroupBox):
saved = Signal(str) saved = Signal(str)
def __init__(self, flight: Flight, game: Game) -> None: def __init__(self, flight: Flight, flight_member: FlightMember, game: Game) -> None:
super().__init__("Use custom loadout") super().__init__("Use custom loadout")
self.flight = flight self.flight = flight
self.flight_member = flight_member
self.game = game self.game = game
self.setCheckable(True) self.setCheckable(True)
self.setChecked(flight.loadout.is_custom) self.setChecked(flight_member.loadout.is_custom)
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
layout = QGridLayout(self) layout = QGridLayout(self)
@ -40,7 +44,7 @@ class QLoadoutEditor(QGroupBox):
label = QLabel(f"<b>{pylon.number}</b>") label = QLabel(f"<b>{pylon.number}</b>")
label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
layout.addWidget(label, i, 0) layout.addWidget(label, i, 0)
layout.addWidget(QPylonEditor(game, flight, pylon), i, 1) layout.addWidget(QPylonEditor(game, flight, flight_member, pylon), i, 1)
vbox.addLayout(layout) vbox.addLayout(layout)
@ -60,8 +64,18 @@ class QLoadoutEditor(QGroupBox):
self.setLayout(vbox) self.setLayout(vbox)
for i in self.findChildren(QPylonEditor): for pylon_editor in self.iter_pylon_editors():
i.set_from(self.flight.loadout) pylon_editor.set_from(self.flight_member.loadout)
def iter_pylon_editors(self) -> Iterator[QPylonEditor]:
yield from self.findChildren(QPylonEditor)
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
with block_signals(self):
self.setChecked(self.flight_member.use_custom_loadout)
for pylon_editor in self.iter_pylon_editors():
pylon_editor.set_flight_member(flight_member)
def _backup_payloads(self) -> None: def _backup_payloads(self) -> None:
ac_id = self.flight.unit_type.dcs_unit_type.id ac_id = self.flight.unit_type.dcs_unit_type.id
@ -146,10 +160,10 @@ class QLoadoutEditor(QGroupBox):
return payload_name_input return payload_name_input
def reset_pylons(self) -> None: def reset_pylons(self) -> None:
self.flight.use_custom_loadout = self.isChecked() self.flight_member.use_custom_loadout = self.isChecked()
if not self.isChecked(): if not self.isChecked():
for i in self.findChildren(QPylonEditor): for pylon_editor in self.iter_pylon_editors():
i.set_from(self.flight.loadout) pylon_editor.set_from(self.flight_member.loadout)
@dataclass @dataclass

View File

@ -6,19 +6,23 @@ from PySide2.QtWidgets import QComboBox
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightmember import FlightMember
from game.ato.loadouts import Loadout from game.ato.loadouts import Loadout
from game.data.weapons import Pylon, Weapon from game.data.weapons import Pylon, Weapon
class QPylonEditor(QComboBox): class QPylonEditor(QComboBox):
def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None: def __init__(
self, game: Game, flight: Flight, flight_member: FlightMember, pylon: Pylon
) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight = flight
self.flight_member = flight_member
self.pylon = pylon self.pylon = pylon
self.game = game self.game = game
self.has_added_clean_item = False self.has_added_clean_item = False
current = self.flight.loadout.pylons.get(self.pylon.number) current = self.flight_member.loadout.pylons.get(self.pylon.number)
self.addItem("None", None) self.addItem("None", None)
if self.game.settings.restrict_weapons_by_date: if self.game.settings.restrict_weapons_by_date:
@ -35,7 +39,7 @@ class QPylonEditor(QComboBox):
def on_pylon_change(self): def on_pylon_change(self):
selected: Optional[Weapon] = self.currentData() selected: Optional[Weapon] = self.currentData()
self.flight.loadout.pylons[self.pylon.number] = selected self.flight_member.loadout.pylons[self.pylon.number] = selected
if selected is None: if selected is None:
logging.debug(f"Pylon {self.pylon.number} emptied") logging.debug(f"Pylon {self.pylon.number} emptied")
@ -70,5 +74,9 @@ class QPylonEditor(QComboBox):
return "None" return "None"
return weapon.name return weapon.name
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
self.set_from(self.flight_member.loadout)
def set_from(self, loadout: Loadout) -> None: def set_from(self, loadout: Loadout) -> None:
self.setCurrentText(self.matching_weapon_name(loadout)) self.setCurrentText(self.matching_weapon_name(loadout))

View File

@ -1,21 +1,31 @@
from PySide6.QtWidgets import QCheckBox from PySide6.QtWidgets import QCheckBox
from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unitpropertydescription import UnitPropertyDescription
from game.ato import Flight from game.ato.flightmember import FlightMember
from .missingpropertydataerror import MissingPropertyDataError from .missingpropertydataerror import MissingPropertyDataError
class PropertyCheckBox(QCheckBox): class PropertyCheckBox(QCheckBox):
def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: def __init__(
self, flight_member: FlightMember, prop: UnitPropertyDescription
) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight_member = flight_member
self.prop = prop self.prop = prop
if prop.default is None: if prop.default is None:
raise MissingPropertyDataError("default cannot be None") raise MissingPropertyDataError("default cannot be None")
self.setChecked(self.flight.props.get(self.prop.identifier, self.prop.default)) self.setChecked(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)
self.toggled.connect(self.on_toggle) self.toggled.connect(self.on_toggle)
def on_toggle(self, checked: bool) -> None: def on_toggle(self, checked: bool) -> None:
self.flight.props[self.prop.identifier] = checked self.flight_member.properties[self.prop.identifier] = checked
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
self.setChecked(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)

View File

@ -1,14 +1,16 @@
from PySide6.QtWidgets import QComboBox from PySide6.QtWidgets import QComboBox
from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unitpropertydescription import UnitPropertyDescription
from game.ato import Flight from game.ato.flightmember import FlightMember
from .missingpropertydataerror import MissingPropertyDataError from .missingpropertydataerror import MissingPropertyDataError
class PropertyComboBox(QComboBox): class PropertyComboBox(QComboBox):
def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: def __init__(
self, flight_member: FlightMember, prop: UnitPropertyDescription
) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight_member = flight_member
self.prop = prop self.prop = prop
if prop.values is None: if prop.values is None:
@ -16,7 +18,9 @@ class PropertyComboBox(QComboBox):
if prop.default is None: if prop.default is None:
raise MissingPropertyDataError("default cannot be None") raise MissingPropertyDataError("default cannot be None")
current_value = self.flight.props.get(self.prop.identifier, self.prop.default) current_value = self.flight_member.properties.get(
self.prop.identifier, self.prop.default
)
for ident, text in self.prop.values.items(): for ident, text in self.prop.values.items():
self.addItem(text, ident) self.addItem(text, ident)
@ -26,4 +30,12 @@ class PropertyComboBox(QComboBox):
self.currentIndexChanged.connect(self.on_selection_changed) self.currentIndexChanged.connect(self.on_selection_changed)
def on_selection_changed(self, _index: int) -> None: def on_selection_changed(self, _index: int) -> None:
self.flight.props[self.prop.identifier] = self.currentData() self.flight_member.properties[self.prop.identifier] = self.currentData()
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
self.setCurrentText(
self.flight_member.properties.get(
self.prop.identifier, self.prop.values[self.prop.default]
)
)

View File

@ -1,9 +1,11 @@
import logging import logging
from typing import Callable
from PySide2.QtWidgets import QGridLayout, QLabel from PySide2.QtWidgets import QGridLayout, QLabel
from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unitpropertydescription import UnitPropertyDescription
from game.ato import Flight from game.ato import Flight
from game.ato.flightmember import FlightMember
from .missingpropertydataerror import MissingPropertyDataError from .missingpropertydataerror import MissingPropertyDataError
from .propertycheckbox import PropertyCheckBox from .propertycheckbox import PropertyCheckBox
from .propertycombobox import PropertyComboBox from .propertycombobox import PropertyComboBox
@ -16,9 +18,10 @@ class UnhandledControlTypeError(RuntimeError):
class PropertyEditor(QGridLayout): class PropertyEditor(QGridLayout):
def __init__(self, flight: Flight) -> None: def __init__(self, flight: Flight, flight_member: FlightMember) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight_member = flight_member
self.flight_member_update_listeners: list[Callable[[FlightMember], None]] = []
for row, prop in enumerate(flight.unit_type.iter_props()): for row, prop in enumerate(flight.unit_type.iter_props()):
if prop.label is None: if prop.label is None:
@ -56,12 +59,23 @@ class PropertyEditor(QGridLayout):
# "checkbox", "comboList", "groupbox", "label", "slider", "spinbox" # "checkbox", "comboList", "groupbox", "label", "slider", "spinbox"
match prop.control: match prop.control:
case "checkbox": case "checkbox":
return PropertyCheckBox(self.flight, prop) widget = PropertyCheckBox(self.flight_member, prop)
self.flight_member_update_listeners.append(widget.set_flight_member)
return widget
case "comboList": case "comboList":
return PropertyComboBox(self.flight, prop) widget = PropertyComboBox(self.flight_member, prop)
self.flight_member_update_listeners.append(widget.set_flight_member)
return widget
case "groupbox" | "label": case "groupbox" | "label":
return None return None
case "slider" | "spinbox": case "slider" | "spinbox":
return PropertySpinBox(self.flight, prop) widget = PropertySpinBox(self.flight_member, prop)
self.flight_member_update_listeners.append(widget.set_flight_member)
return widget
case _: case _:
raise UnhandledControlTypeError(prop.control) raise UnhandledControlTypeError(prop.control)
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
for listener in self.flight_member_update_listeners:
listener(self.flight_member)

View File

@ -1,14 +1,16 @@
from PySide6.QtWidgets import QSpinBox from PySide6.QtWidgets import QSpinBox
from dcs.unitpropertydescription import UnitPropertyDescription from dcs.unitpropertydescription import UnitPropertyDescription
from game.ato import Flight from game.ato.flightmember import FlightMember
from .missingpropertydataerror import MissingPropertyDataError from .missingpropertydataerror import MissingPropertyDataError
class PropertySpinBox(QSpinBox): class PropertySpinBox(QSpinBox):
def __init__(self, flight: Flight, prop: UnitPropertyDescription) -> None: def __init__(
self, flight_member: FlightMember, prop: UnitPropertyDescription
) -> None:
super().__init__() super().__init__()
self.flight = flight self.flight_member = flight_member
self.prop = prop self.prop = prop
if prop.minimum is None: if prop.minimum is None:
@ -20,9 +22,17 @@ class PropertySpinBox(QSpinBox):
self.setMinimum(prop.minimum) self.setMinimum(prop.minimum)
self.setMaximum(prop.maximum) self.setMaximum(prop.maximum)
self.setValue(self.flight.props.get(self.prop.identifier, self.prop.default)) self.setValue(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)
self.valueChanged.connect(self.on_value_changed) self.valueChanged.connect(self.on_value_changed)
def on_value_changed(self, value: int) -> None: def on_value_changed(self, value: int) -> None:
self.flight.props[self.prop.identifier] = value self.flight_member.properties[self.prop.identifier] = value
def set_flight_member(self, flight_member: FlightMember) -> None:
self.flight_member = flight_member
self.setValue(
self.flight_member.properties.get(self.prop.identifier, self.prop.default)
)

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Optional, Callable from typing import Optional, Callable, Union
from PySide2.QtCore import Signal, QModelIndex from PySide2.QtCore import Signal, QModelIndex
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
@ -16,6 +16,8 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.ato.flight import Flight from game.ato.flight import Flight
from game.ato.flightroster import FlightRoster from game.ato.flightroster import FlightRoster
from game.ato.iflightroster import IFlightRoster
from game.squadrons import Squadron
from game.squadrons.pilot import Pilot from game.squadrons.pilot import Pilot
from qt_ui.models import PackageModel from qt_ui.models import PackageModel
@ -23,8 +25,11 @@ from qt_ui.models import PackageModel
class PilotSelector(QComboBox): class PilotSelector(QComboBox):
available_pilots_changed = Signal() available_pilots_changed = Signal()
def __init__(self, roster: Optional[FlightRoster], idx: int) -> None: def __init__(
self, squadron: Union[Squadron, None], roster: Optional[IFlightRoster], idx: int
) -> None:
super().__init__() super().__init__()
self.squadron = squadron
self.roster = roster self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.rebuild() self.rebuild()
@ -40,10 +45,13 @@ class PilotSelector(QComboBox):
self.setDisabled(True) self.setDisabled(True)
return return
if self.squadron is None:
raise RuntimeError("squadron cannot be None if roster is set")
self.setEnabled(True) self.setEnabled(True)
self.addItem("Unassigned", None) self.addItem("Unassigned", None)
choices = list(self.roster.squadron.available_pilots) choices = list(self.squadron.available_pilots)
current_pilot = self.roster.pilots[self.pilot_index] current_pilot = self.roster.pilot_at(self.pilot_index)
if current_pilot is not None: if current_pilot is not None:
choices.append(current_pilot) choices.append(current_pilot)
# Put players first, otherwise alphabetically. # Put players first, otherwise alphabetically.
@ -71,23 +79,26 @@ class PilotSelector(QComboBox):
# The roster resize is handled separately, so we have no pilots to remove. # The roster resize is handled separately, so we have no pilots to remove.
return return
pilot = self.itemData(index) pilot = self.itemData(index)
if pilot == self.roster.pilots[self.pilot_index]: if pilot == self.roster.pilot_at(self.pilot_index):
return return
self.roster.set_pilot(self.pilot_index, pilot) self.roster.set_pilot(self.pilot_index, pilot)
self.available_pilots_changed.emit() self.available_pilots_changed.emit()
def replace(self, new_roster: Optional[FlightRoster]) -> None: def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None:
self.squadron = squadron
self.roster = new_roster self.roster = new_roster
self.rebuild() self.rebuild()
class PilotControls(QHBoxLayout): class PilotControls(QHBoxLayout):
def __init__(self, roster: Optional[FlightRoster], idx: int) -> None: def __init__(
self, squadron: Union[Squadron, None], roster: Optional[FlightRoster], idx: int
) -> None:
super().__init__() super().__init__()
self.roster = roster self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.selector = PilotSelector(roster, idx) self.selector = PilotSelector(squadron, roster, idx)
self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.selector.currentIndexChanged.connect(self.on_pilot_changed)
self.addWidget(self.selector) self.addWidget(self.selector)
@ -95,8 +106,8 @@ class PilotControls(QHBoxLayout):
self.player_checkbox.setToolTip("Checked if this pilot is a player.") self.player_checkbox.setToolTip("Checked if this pilot is a player.")
self.on_pilot_changed(self.selector.currentIndex()) self.on_pilot_changed(self.selector.currentIndex())
enabled = False enabled = False
if self.roster is not None and self.roster.squadron is not None: if self.roster is not None and squadron is not None:
enabled = self.roster.squadron.aircraft.flyable enabled = squadron.aircraft.flyable
self.player_checkbox.setEnabled(enabled) self.player_checkbox.setEnabled(enabled)
self.addWidget(self.player_checkbox) self.addWidget(self.player_checkbox)
@ -106,7 +117,7 @@ class PilotControls(QHBoxLayout):
def pilot(self) -> Optional[Pilot]: def pilot(self) -> Optional[Pilot]:
if self.roster is None or self.pilot_index >= self.roster.max_size: if self.roster is None or self.pilot_index >= self.roster.max_size:
return None return None
return self.roster.pilots[self.pilot_index] return self.roster.pilot_at(self.pilot_index)
def on_player_toggled(self, checked: bool) -> None: def on_player_toggled(self, checked: bool) -> None:
pilot = self.pilot pilot = self.pilot
@ -145,19 +156,19 @@ class PilotControls(QHBoxLayout):
finally: finally:
self.player_checkbox.blockSignals(False) self.player_checkbox.blockSignals(False)
def replace(self, new_roster: Optional[FlightRoster]) -> None: def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None:
self.roster = new_roster self.roster = new_roster
if self.roster is None or self.pilot_index >= self.roster.max_size: if self.roster is None or self.pilot_index >= self.roster.max_size:
self.disable_and_clear() self.disable_and_clear()
else: else:
self.enable_and_reset() self.enable_and_reset()
self.selector.replace(new_roster) self.selector.replace(squadron, new_roster)
class FlightRosterEditor(QVBoxLayout): class FlightRosterEditor(QVBoxLayout):
MAX_PILOTS = 4 MAX_PILOTS = 4
def __init__(self, roster: Optional[FlightRoster]) -> None: def __init__(self, squadron: Union[Squadron, None], roster: Union[IFlightRoster, None]) -> None:
super().__init__() super().__init__()
self.roster = roster self.roster = roster
@ -170,7 +181,7 @@ class FlightRosterEditor(QVBoxLayout):
return callback return callback
controls = PilotControls(roster, pilot_idx) controls = PilotControls(squadron, roster, pilot_idx)
controls.selector.available_pilots_changed.connect( controls.selector.available_pilots_changed.connect(
make_reset_callback(pilot_idx) make_reset_callback(pilot_idx)
) )
@ -193,15 +204,17 @@ class FlightRosterEditor(QVBoxLayout):
for controls in self.pilot_controls[new_size:]: for controls in self.pilot_controls[new_size:]:
controls.disable_and_clear() controls.disable_and_clear()
def replace(self, new_roster: Optional[FlightRoster]) -> None: def replace(self, squadron: Squadron, new_roster: Optional[FlightRoster]) -> None:
if self.roster is not None: if self.roster is not None:
self.roster.clear() self.roster.clear()
self.roster = new_roster self.roster = new_roster
for controls in self.pilot_controls: for controls in self.pilot_controls:
controls.replace(new_roster) controls.replace(squadron, new_roster)
class QFlightSlotEditor(QGroupBox): class QFlightSlotEditor(QGroupBox):
flight_resized = Signal(int)
def __init__(self, package_model: PackageModel, flight: Flight, game: Game): def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
super().__init__("Slots") super().__init__("Slots")
self.package_model = package_model self.package_model = package_model
@ -228,7 +241,7 @@ class QFlightSlotEditor(QGroupBox):
layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1) layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1)
layout.addWidget(QLabel("Assigned pilots:"), 2, 0) layout.addWidget(QLabel("Assigned pilots:"), 2, 0)
self.roster_editor = FlightRosterEditor(flight.roster) self.roster_editor = FlightRosterEditor(flight.squadron, flight.roster)
layout.addLayout(self.roster_editor, 2, 1) layout.addLayout(self.roster_editor, 2, 1)
self.setLayout(layout) self.setLayout(layout)
@ -251,3 +264,4 @@ class QFlightSlotEditor(QGroupBox):
self.flight.resize(old_count) self.flight.resize(old_count)
return return
self.roster_editor.resize(new_count) self.roster_editor.resize(new_count)
self.flight_resized.emit(new_count)

View File

@ -19,7 +19,7 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import (
class QGeneralFlightSettingsTab(QFrame): class QGeneralFlightSettingsTab(QFrame):
on_flight_settings_changed = Signal() flight_size_changed = Signal()
def __init__( def __init__(
self, self,
@ -30,13 +30,16 @@ class QGeneralFlightSettingsTab(QFrame):
): ):
super().__init__() super().__init__()
self.flight_slot_editor = QFlightSlotEditor(package_model, flight, game.game)
self.flight_slot_editor.flight_resized.connect(self.flight_size_changed)
widgets = [ widgets = [
QFlightTypeTaskInfo(flight), QFlightTypeTaskInfo(flight),
QCommsEditor(flight, game), QCommsEditor(flight, game),
FlightPlanPropertiesGroup( FlightPlanPropertiesGroup(
game.game, package_model, flight, flight_wpt_list game.game, package_model, flight, flight_wpt_list
), ),
QFlightSlotEditor(package_model, flight, game.game), self.flight_slot_editor,
QFlightStartType(package_model, flight), QFlightStartType(package_model, flight),
QFlightCustomName(flight), QFlightCustomName(flight),
] ]
@ -45,6 +48,7 @@ class QGeneralFlightSettingsTab(QFrame):
for w in widgets: for w in widgets:
layout.addWidget(w, row, 0) layout.addWidget(w, row, 0)
row += 1 row += 1
vstretch = QVBoxLayout() vstretch = QVBoxLayout()
vstretch.addStretch() vstretch.addStretch()
layout.addLayout(vstretch, row, 0) layout.addLayout(vstretch, row, 0)

View File

@ -226,9 +226,11 @@ class QFlightWaypointTab(QFrame):
QMessageBox.critical( QMessageBox.critical(
self, "Could not recreate flight", str(ex), QMessageBox.Ok self, "Could not recreate flight", str(ex), QMessageBox.Ok
) )
if not self.flight.loadout.is_custom: for member in self.flight.iter_members():
self.flight.loadout = Loadout.default_for(self.flight) if not member.loadout.is_custom:
self.loadout_changed.emit() member.loadout = Loadout.default_for(self.flight)
self.loadout_changed.emit()
self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_change(self): def on_change(self):