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
* **[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
* **[New Game Wizard]** Ability to save an edited faction during new game creation
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -321,6 +321,38 @@ class 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
def ground_units(self) -> Iterator[GroundUnitType]:
yield from self.artillery_units

View File

@ -37,7 +37,7 @@ class FactionLoader:
@classmethod
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(
FACTION_DIRECTORY
) + 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")
def factions_dir() -> Path:
return _create_dir_if_needed(base_path() / "Retribution" / "Factions")
def groups_dir() -> Path:
return _create_dir_if_needed(base_path() / "Retribution" / "Groups")

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals
import json
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.QtCore import Qt
@ -15,14 +17,22 @@ from PySide6.QtWidgets import (
QPushButton,
QComboBox,
QHBoxLayout,
QVBoxLayout,
QLineEdit,
QDialog,
QFileDialog,
)
from game import persistency
from game.armedforces.forcegroup import ForceGroup
from game.ato import FlightType
from game.campaignloader import Campaign
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from game.factions import Faction, FACTIONS
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.newgame.jinja_env import jinja_env
@ -62,69 +72,128 @@ class QFactionUnits(QScrollArea):
self.checkboxes: dict[str, QCheckBox] = {}
grid = QGridLayout()
grid.setColumnStretch(1, 1)
if len(self.faction.aircraft) > 0:
self.add_ac_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_ac_combo,
lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo),
self._aircraft_predicate,
)
grid.addWidget(QLabel("<strong>Aircraft:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox)
if len(self.faction.awacs) > 0:
self.add_awacs_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_awacs_combo,
lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo),
self._awacs_predicate,
)
grid.addWidget(QLabel("<strong>AWACS:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox)
if len(self.faction.tankers) > 0:
self.add_tanker_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_tanker_combo,
lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo),
self._tanker_predicate,
)
grid.addWidget(QLabel("<strong>Tankers:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox)
if len(self.faction.frontline_units) > 0:
self.add_frontline_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_frontline_combo,
lambda: self._on_add_unit(
self.faction.frontline_units, self.add_frontline_combo
),
self.faction.frontline_units,
)
grid.addWidget(QLabel("<strong>Frontlines vehicles:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.frontline_units, counter, grid, hbox
)
if len(self.faction.artillery_units) > 0:
grid.addWidget(QLabel("<strong>Artillery units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.artillery_units, counter, grid)
if len(self.faction.logistics_units) > 0:
grid.addWidget(QLabel("<strong>Logistics units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.logistics_units, counter, grid)
if len(self.faction.infantry_units) > 0:
grid.addWidget(QLabel("<strong>Infantry units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.infantry_units, counter, grid)
if len(self.faction.preset_groups) > 0:
grid.addWidget(QLabel("<strong>Preset groups:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.preset_groups, counter, grid)
if len(self.faction.air_defense_units) > 0:
grid.addWidget(QLabel("<strong>Air defenses:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.air_defense_units, counter, grid
)
if len(self.faction.naval_units) > 0:
grid.addWidget(QLabel("<strong>Naval units:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.naval_units, counter, grid)
if len(self.faction.missiles) > 0:
grid.addWidget(QLabel("<strong>Missile units:</strong>"), counter, 0)
self._add_checkboxes(self.faction.missiles, counter, grid)
self.add_ac_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_ac_combo,
lambda: self._on_add_ac(self.faction.aircraft, self.add_ac_combo),
self._aircraft_predicate,
)
grid.addWidget(QLabel("<strong>Aircraft:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.aircraft, counter, grid, hbox)
self.add_awacs_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_awacs_combo,
lambda: self._on_add_ac(self.faction.awacs, self.add_awacs_combo),
self._awacs_predicate,
)
grid.addWidget(QLabel("<strong>AWACS:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.awacs, counter, grid, hbox)
self.add_tanker_combo = QComboBox()
hbox = self._create_aircraft_combobox(
self.add_tanker_combo,
lambda: self._on_add_ac(self.faction.tankers, self.add_tanker_combo),
self._tanker_predicate,
)
grid.addWidget(QLabel("<strong>Tankers:</strong>"), counter, 0)
counter = self._add_checkboxes(self.faction.tankers, counter, grid, hbox)
self.add_frontline_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_frontline_combo,
lambda: self._on_add_unit(
self.faction.frontline_units, self.add_frontline_combo
),
self.faction.frontline_units,
["Frontline vehicles"],
)
grid.addWidget(QLabel("<strong>Frontlines vehicles:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.frontline_units, counter, grid, hbox
)
self.add_artillery_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_artillery_combo,
lambda: self._on_add_unit(
self.faction.artillery_units, self.add_artillery_combo
),
self.faction.artillery_units,
["Artillery"],
)
grid.addWidget(QLabel("<strong>Artillery units:</strong>"), counter, 0)
counter = self._add_checkboxes(
self.faction.artillery_units, counter, grid, hbox
)
self.add_logistics_combo = QComboBox()
hbox = self._create_unit_combobox(
self.add_logistics_combo,
lambda: self._on_add_unit(
self.faction.logistics_units, self.add_logistics_combo
),
self.faction.logistics_units,
["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:
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 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
predicate(ac)
add_ac = QPushButton("+")
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)
hbox = self._format(cb, callback)
return hbox
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):
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):
if unit in units:
continue
cb.addItem(unit.variant_id, unit)
add_unit = QPushButton("+")
add_unit.setStyleSheet("QPushButton{ font-weight: bold; }")
add_unit.setFixedWidth(50)
add_unit.clicked.connect(callback)
hbox = QHBoxLayout()
hbox.addWidget(cb)
hbox.addWidget(add_unit)
if "Frontline vehicles" in type:
cb.addItem(unit.variant_id, unit)
elif unit.unit_class.value in type:
cb.addItem(unit.variant_id, unit)
else:
continue
hbox = self._format(cb, callback)
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
def _on_add_unit(self, units: Set[UnitType], cb: QComboBox):
@ -211,6 +296,13 @@ class QFactionUnits(QScrollArea):
del self.faction.__dict__["all_aircrafts"]
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):
self.faction = faction
self.content = QWidget()
@ -228,6 +320,22 @@ class QFactionUnits(QScrollArea):
for d in deletes:
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):
def __init__(self, parent=None):
@ -284,6 +392,17 @@ class FactionSelection(QtWidgets.QWizardPage):
if r == "USA 2005":
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
self.blueFactionUnits = QFactionUnits(
self.blueFactionSelect.currentData(), self.blueGroupLayout, show_jtac=True
@ -380,6 +499,15 @@ class FactionSelection(QtWidgets.QWizardPage):
qfu.updateFactionUnits(fac.missiles)
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
def selected_blue_faction(self) -> Faction:
return self._filter_selected_units(self.blueFactionUnits)
@ -387,3 +515,52 @@ class FactionSelection(QtWidgets.QWizardPage):
@property
def selected_red_faction(self) -> Faction:
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()