Faction editor update (#434)

Resolves #166 

* init faction editor

* update persistency

* minor fixes

* typing smh

* small fixes

* forgot the changelog -_-
This commit is contained in:
Druss99 2024-12-30 18:24:12 -05:00 committed by GitHub
parent f36526b5de
commit 0d04e0c72e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 296 additions and 82 deletions

View File

@ -44,6 +44,7 @@
* **[Package Planning]** Ability to plan recovery tanker flights * **[Package Planning]** Ability to plan recovery tanker flights
* **[Modding]** Support for Bandit's cloud presets mod (v15) * **[Modding]** Support for Bandit's cloud presets mod (v15)
* **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap * **[UX]** Reduce size of save-file by loading landmap data on the fly, which also implies no new campaign needs to be started to benefit from an updated landmap
* **[New Game Wizard]** Ability to save an edited faction during new game creation
## Fixes ## Fixes
* **[UI/UX]** A-10A flights can be edited again * **[UI/UX]** A-10A flights can be edited again

View File

@ -321,6 +321,38 @@ class Faction:
return faction return faction
def to_dict(self) -> dict[str, Any]:
return {
"country": self.country.name,
"name": self.name,
"description": self.description,
"authors": self.authors,
"aircrafts": [ac.variant_id for ac in self.aircraft],
"awacs": [ac.variant_id for ac in self.awacs],
"tankers": [ac.variant_id for ac in self.tankers],
"frontline_units": [unit.variant_id for unit in self.frontline_units],
"artillery_units": [unit.variant_id for unit in self.artillery_units],
"logistics_units": [unit.variant_id for unit in self.logistics_units],
"infantry_units": [unit.variant_id for unit in self.infantry_units],
"preset_groups": [group.name for group in self.preset_groups],
"air_defense_units": [unit.variant_id for unit in self.air_defense_units],
"naval_units": [unit.variant_id for unit in self.naval_units],
"missiles": [unit.variant_id for unit in self.missiles],
"has_jtac": self.has_jtac,
"jtac_unit": self.jtac_unit.variant_id if self.jtac_unit else None,
"doctrine": self.doctrine.name,
"building_set": list(self.building_set),
"liveries_overrides": {
ac.variant_id: livery for ac, livery in self.liveries_overrides.items()
},
"liveries_overrides_ground_forces": self.liveries_overrides_ground_forces,
"unrestricted_satnav": self.unrestricted_satnav,
"requirements": self.requirements,
"carriers": {
carrier.variant_id: names for carrier, names in self.carriers.items()
},
}
@property @property
def ground_units(self) -> Iterator[GroundUnitType]: def ground_units(self) -> Iterator[GroundUnitType]:
yield from self.artillery_units yield from self.artillery_units

View File

@ -37,7 +37,7 @@ class FactionLoader:
@classmethod @classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]: def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
user_faction_path = persistency.base_path() / "Retribution/Factions" user_faction_path = persistency.factions_dir()
files = cls.find_faction_files_in( files = cls.find_faction_files_in(
FACTION_DIRECTORY FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path) ) + cls.find_faction_files_in(user_faction_path)

View File

@ -150,6 +150,10 @@ def debug_dir() -> Path:
return _create_dir_if_needed(base_path() / "Retribution" / "Debug") return _create_dir_if_needed(base_path() / "Retribution" / "Debug")
def factions_dir() -> Path:
return _create_dir_if_needed(base_path() / "Retribution" / "Factions")
def groups_dir() -> Path: def groups_dir() -> Path:
return _create_dir_if_needed(base_path() / "Retribution" / "Groups") return _create_dir_if_needed(base_path() / "Retribution" / "Groups")

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json
from copy import deepcopy from copy import deepcopy
from typing import Union, Callable, Set, Optional from typing import Union, Callable, Set, Optional, List
from PySide6 import QtWidgets, QtGui from PySide6 import QtWidgets, QtGui
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
@ -15,14 +17,22 @@ from PySide6.QtWidgets import (
QPushButton, QPushButton,
QComboBox, QComboBox,
QHBoxLayout, QHBoxLayout,
QVBoxLayout,
QLineEdit,
QDialog,
QFileDialog,
) )
from game import persistency
from game.armedforces.forcegroup import ForceGroup
from game.ato import FlightType from game.ato import FlightType
from game.campaignloader import Campaign from game.campaignloader import Campaign
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
from game.factions import Faction, FACTIONS from game.factions import Faction, FACTIONS
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.newgame.jinja_env import jinja_env from qt_ui.windows.newgame.jinja_env import jinja_env
@ -62,69 +72,128 @@ class QFactionUnits(QScrollArea):
self.checkboxes: dict[str, QCheckBox] = {} self.checkboxes: dict[str, QCheckBox] = {}
grid = QGridLayout() grid = QGridLayout()
grid.setColumnStretch(1, 1) grid.setColumnStretch(1, 1)
if len(self.faction.aircraft) > 0: self.add_ac_combo = QComboBox()
self.add_ac_combo = QComboBox() hbox = self._create_aircraft_combobox(
hbox = self._create_aircraft_combobox( self.add_ac_combo,
self.add_ac_combo, lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo),
lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo), self._aircraft_predicate,
self._aircraft_predicate, )
) grid.addWidget(QLabel("<strong>Aircraft:</strong>"), counter, 0)
grid.addWidget(QLabel("<strong>Aircraft:</strong>"), counter, 0) counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox)
counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox)
if len(self.faction.awacs) > 0: self.add_awacs_combo = QComboBox()
self.add_awacs_combo = QComboBox() hbox = self._create_aircraft_combobox(
hbox = self._create_aircraft_combobox( self.add_awacs_combo,
self.add_awacs_combo, lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo),
lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo), self._awacs_predicate,
self._awacs_predicate, )
) grid.addWidget(QLabel("<strong>AWACS:</strong>"), counter, 0)
grid.addWidget(QLabel("<strong>AWACS:</strong>"), counter, 0) counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox)
counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox)
if len(self.faction.tankers) > 0: self.add_tanker_combo = QComboBox()
self.add_tanker_combo = QComboBox() hbox = self._create_aircraft_combobox(
hbox = self._create_aircraft_combobox( self.add_tanker_combo,
self.add_tanker_combo, lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo),
lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo), self._tanker_predicate,
self._tanker_predicate, )
) grid.addWidget(QLabel("<strong>Tankers:</strong>"), counter, 0)
grid.addWidget(QLabel("<strong>Tankers:</strong>"), counter, 0) counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox)
counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox)
if len(self.faction.frontline_units) > 0: self.add_frontline_combo = QComboBox()
self.add_frontline_combo = QComboBox() hbox = self._create_unit_combobox(
hbox = self._create_unit_combobox( self.add_frontline_combo,
self.add_frontline_combo, lambda: self._on_add_unit(
lambda: self._on_add_unit( self.faction.frontline_units, self.add_frontline_combo
self.faction.frontline_units, self.add_frontline_combo ),
), self.faction.frontline_units,
self.faction.frontline_units, ["Frontline vehicles"],
) )
grid.addWidget(QLabel("<strong>Frontlines vehicles:</strong>"), counter, 0) grid.addWidget(QLabel("<strong>Frontlines vehicles:</strong>"), counter, 0)
counter = self._add_checkboxes( counter = self._add_checkboxes(
self.faction.frontline_units, counter, grid, hbox self.faction.frontline_units, counter, grid, hbox
) )
if len(self.faction.artillery_units) > 0:
grid.addWidget(QLabel("<strong>Artillery units:</strong>"), counter, 0) self.add_artillery_combo = QComboBox()
counter = self._add_checkboxes(self.faction.artillery_units, counter, grid) hbox = self._create_unit_combobox(
if len(self.faction.logistics_units) > 0: self.add_artillery_combo,
grid.addWidget(QLabel("<strong>Logistics units:</strong>"), counter, 0) lambda: self._on_add_unit(
counter = self._add_checkboxes(self.faction.logistics_units, counter, grid) self.faction.artillery_units, self.add_artillery_combo
if len(self.faction.infantry_units) > 0: ),
grid.addWidget(QLabel("<strong>Infantry units:</strong>"), counter, 0) self.faction.artillery_units,
counter = self._add_checkboxes(self.faction.infantry_units, counter, grid) ["Artillery"],
if len(self.faction.preset_groups) > 0: )
grid.addWidget(QLabel("<strong>Preset groups:</strong>"), counter, 0) grid.addWidget(QLabel("<strong>Artillery units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.preset_groups, counter, grid) counter = self._add_checkboxes(
if len(self.faction.air_defense_units) > 0: self.faction.artillery_units, counter, grid, hbox
grid.addWidget(QLabel("<strong>Air defenses:</strong>"), counter, 0) )
counter = self._add_checkboxes(
self.faction.air_defense_units, counter, grid self.add_logistics_combo = QComboBox()
) hbox = self._create_unit_combobox(
if len(self.faction.naval_units) > 0: self.add_logistics_combo,
grid.addWidget(QLabel("<strong>Naval units:</strong>"), counter, 0) lambda: self._on_add_unit(
counter = self._add_checkboxes(self.faction.naval_units, counter, grid) self.faction.logistics_units, self.add_logistics_combo
if len(self.faction.missiles) > 0: ),
grid.addWidget(QLabel("<strong>Missile units:</strong>"), counter, 0) self.faction.logistics_units,
self._add_checkboxes(self.faction.missiles, counter, grid) ["Logistics"],
)
grid.addWidget(QLabel("<strong>Logistics units:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.logistics_units, counter, grid, hbox
)
self.add_infantry_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_infantry_combo,
lambda: self._on_add_unit(
self.faction.infantry_units, self.add_infantry_combo
),
self.faction.infantry_units,
["Infantry"],
)
grid.addWidget(QLabel("<strong>Infantry units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.infantry_units, counter, grid, hbox)
self.add_preset_group_combo = QComboBox()
hbox = self._create_preset_group_combobox(
self.add_preset_group_combo,
lambda: self._on_add_preset_group(
self.faction.preset_groups, self.add_preset_group_combo
),
)
grid.addWidget(QLabel("<strong>Preset groups:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.preset_groups, counter, grid, hbox)
self.add_air_defense_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_air_defense_combo,
lambda: self._on_add_unit(
self.faction.air_defense_units, self.add_air_defense_combo
),
self.faction.air_defense_units,
["EarlyWarningRadar", "AAA", "SHORAD"],
)
grid.addWidget(QLabel("<strong>Air defenses:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.air_defense_units, counter, grid, hbox
)
self.add_naval_combo = QComboBox()
hbox = self._create_naval_combobox(
self.add_naval_combo,
lambda: self._on_add_unit(self.faction.naval_units, self.add_naval_combo),
)
grid.addWidget(QLabel("<strong>Naval units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.naval_units, counter, grid, hbox)
self.add_missile_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_missile_combo,
lambda: self._on_add_unit(self.faction.missiles, self.add_missile_combo),
self.faction.missiles,
["Missile"],
)
grid.addWidget(QLabel("<strong>Missile units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.missiles, counter, grid, hbox)
if show_jtac: if show_jtac:
grid.addWidget(QLabel("<strong>JTAC</strong>"), counter, 0) grid.addWidget(QLabel("<strong>JTAC</strong>"), counter, 0)
@ -166,20 +235,18 @@ class QFactionUnits(QScrollArea):
): ):
for ac_dcs in sorted(AircraftType.each_dcs_type(), key=lambda x: x.id): for ac_dcs in sorted(AircraftType.each_dcs_type(), key=lambda x: x.id):
for ac in AircraftType.for_dcs_type(ac_dcs): for ac in AircraftType.for_dcs_type(ac_dcs):
if ac in self.faction.aircraft: if (
ac in self.faction.aircraft
or ac in self.faction.awacs
or ac in self.faction.tankers
):
continue continue
predicate(ac) predicate(ac)
add_ac = QPushButton("+") hbox = self._format(cb, callback)
add_ac.setStyleSheet("QPushButton{ font-weight: bold; }")
add_ac.setFixedWidth(50)
add_ac.clicked.connect(callback)
hbox = QHBoxLayout()
hbox.addWidget(cb)
hbox.addWidget(add_ac)
return hbox return hbox
def _create_unit_combobox( def _create_unit_combobox(
self, cb: QComboBox, callback: Callable, units: Set[GroundUnitType] self, cb: QComboBox, callback: Callable, units: Set[GroundUnitType], type: list
): ):
for dcs_unit in sorted(GroundUnitType.each_dcs_type(), key=lambda x: x.id): for dcs_unit in sorted(GroundUnitType.each_dcs_type(), key=lambda x: x.id):
if dcs_unit not in self.faction.country.vehicles: if dcs_unit not in self.faction.country.vehicles:
@ -187,14 +254,32 @@ class QFactionUnits(QScrollArea):
for unit in GroundUnitType.for_dcs_type(dcs_unit): for unit in GroundUnitType.for_dcs_type(dcs_unit):
if unit in units: if unit in units:
continue continue
cb.addItem(unit.variant_id, unit) if "Frontline vehicles" in type:
add_unit = QPushButton("+") cb.addItem(unit.variant_id, unit)
add_unit.setStyleSheet("QPushButton{ font-weight: bold; }") elif unit.unit_class.value in type:
add_unit.setFixedWidth(50) cb.addItem(unit.variant_id, unit)
add_unit.clicked.connect(callback) else:
hbox = QHBoxLayout() continue
hbox.addWidget(cb) hbox = self._format(cb, callback)
hbox.addWidget(add_unit) return hbox
def _create_naval_combobox(self, cb: QComboBox, callback: Callable):
for ship_dcs in sorted(ShipUnitType.each_dcs_type(), key=lambda x: x.id):
for ship in ShipUnitType.for_dcs_type(ship_dcs):
if ship in self.faction.naval_units:
continue
cb.addItem(ship.variant_id, ship)
hbox = self._format(cb, callback)
return hbox
def _create_preset_group_combobox(self, cb: QComboBox, callback: Callable):
ForceGroup._load_all()
preset_group_names = {pg.name for pg in self.faction.preset_groups}
for preset_group in ForceGroup._by_name:
if preset_group in preset_group_names:
continue
cb.addItem(preset_group, ForceGroup._by_name[preset_group])
hbox = self._format(cb, callback)
return hbox return hbox
def _on_add_unit(self, units: Set[UnitType], cb: QComboBox): def _on_add_unit(self, units: Set[UnitType], cb: QComboBox):
@ -211,6 +296,13 @@ class QFactionUnits(QScrollArea):
del self.faction.__dict__["all_aircrafts"] del self.faction.__dict__["all_aircrafts"]
self.updateFaction(self.faction) self.updateFaction(self.faction)
def _on_add_preset_group(self, groups: List[ForceGroup], cb: QComboBox):
groups.append(cb.currentData())
if self.faction.__dict__.get("accessible_units"):
# invalidate the cached property
del self.faction.__dict__["accessible_units"]
self.updateFaction(self.faction)
def updateFaction(self, faction: Faction): def updateFaction(self, faction: Faction):
self.faction = faction self.faction = faction
self.content = QWidget() self.content = QWidget()
@ -228,6 +320,22 @@ class QFactionUnits(QScrollArea):
for d in deletes: for d in deletes:
units.remove(d) units.remove(d)
def _format(self, cb: QComboBox, callback: Callable):
add_button = QPushButton("+")
add_button.setStyleSheet("QPushButton{ font-weight: bold; }")
add_button.setFixedWidth(50)
add_button.clicked.connect(callback)
hbox = QHBoxLayout()
hbox.addWidget(cb)
hbox.addWidget(add_button)
if cb.count() == 0:
cb.setEnabled(False)
add_button.setEnabled(False)
else:
cb.setEnabled(True)
add_button.setEnabled(True)
return hbox
class FactionSelection(QtWidgets.QWizardPage): class FactionSelection(QtWidgets.QWizardPage):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -284,6 +392,17 @@ class FactionSelection(QtWidgets.QWizardPage):
if r == "USA 2005": if r == "USA 2005":
self.blueFactionSelect.setCurrentIndex(i) self.blueFactionSelect.setCurrentIndex(i)
self.saveBlueFactionButton = QPushButton("Save as new faction")
self.saveRedFactionButton = QPushButton("Save as new faction")
self.blueGroupLayout.addWidget(self.saveBlueFactionButton, 3, 0, 1, 2)
self.redGroupLayout.addWidget(self.saveRedFactionButton, 3, 0, 1, 2)
self.saveBlueFactionButton.clicked.connect(
lambda: self.show_save_faction_dialog(self.selected_blue_faction)
)
self.saveRedFactionButton.clicked.connect(
lambda: self.show_save_faction_dialog(self.selected_red_faction)
)
# Faction units # Faction units
self.blueFactionUnits = QFactionUnits( self.blueFactionUnits = QFactionUnits(
self.blueFactionSelect.currentData(), self.blueGroupLayout, show_jtac=True self.blueFactionSelect.currentData(), self.blueGroupLayout, show_jtac=True
@ -380,6 +499,15 @@ class FactionSelection(QtWidgets.QWizardPage):
qfu.updateFactionUnits(fac.missiles) qfu.updateFactionUnits(fac.missiles)
return fac return fac
def show_save_faction_dialog(self, faction: Faction):
dialog = QFactionSaver(faction)
dialog.exec()
for r in FACTIONS:
if self.blueFactionSelect.findText(r) == -1:
self.blueFactionSelect.addItem(r, FACTIONS[r])
if self.redFactionSelect.findText(r) == -1:
self.redFactionSelect.addItem(r, FACTIONS[r])
@property @property
def selected_blue_faction(self) -> Faction: def selected_blue_faction(self) -> Faction:
return self._filter_selected_units(self.blueFactionUnits) return self._filter_selected_units(self.blueFactionUnits)
@ -387,3 +515,52 @@ class FactionSelection(QtWidgets.QWizardPage):
@property @property
def selected_red_faction(self) -> Faction: def selected_red_faction(self) -> Faction:
return self._filter_selected_units(self.redFactionUnits) return self._filter_selected_units(self.redFactionUnits)
class QFactionSaver(QDialog):
def __init__(self, faction: Faction):
super().__init__()
self.faction = faction
self.setMinimumWidth(400)
self.setWindowTitle("Save new faction")
self.setWindowIcon(EVENT_ICONS["strike"])
layout = QVBoxLayout()
self.name_label = QLabel("Name:")
self.name_text = QLineEdit()
layout.addWidget(self.name_label)
layout.addWidget(self.name_text)
self.description_label = QLabel("Description:")
self.description_text = QLineEdit()
layout.addWidget(self.description_label)
layout.addWidget(self.description_text)
self.authors_label = QLabel("Author(s):")
self.authors_text = QLineEdit()
layout.addWidget(self.authors_label)
layout.addWidget(self.authors_text)
self.save_button = QPushButton("Save")
self.save_button.clicked.connect(self.save_faction)
layout.addWidget(self.save_button)
self.setLayout(layout)
def save_faction(self) -> None:
self.faction.name = self.name_text.text()
self.faction.description = f"<p>{self.description_text.text()}</p>"
self.faction.authors = self.authors_text.text()
user_faction_path = persistency.factions_dir()
fd = QFileDialog(
caption="Save Faction", directory=str(user_faction_path), filter="*.json"
)
fd.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
if fd.exec_():
json_filename = fd.selectedFiles()[0]
with open(json_filename, "w") as file:
json.dump(self.faction.to_dict(), file, indent=2)
FACTIONS.factions[self.faction.name] = self.faction
self.accept()