Implement Template System for ground objects

- Factored out the current generators to use a better approach with Templates build from the dcs mission editor.
- This information is extended with a template-mapping in a json file which allows to logically group together multiple dcs groups and even statics to one template
- The combination of mapping and miz will be serialized to a template.json which is only used for loading.
- Factions now load templates during initialization and hold all the templates they can really use. This is based around the faction file.
- Implemented a template randomizer which allows to add some randomization to templates
- Each Template Group can have 1 randomizer which randomizes unit_type and size based on the mapping definition. Larger groups need to be devided in more fine detailed groups as we can now handle them better due to the change from dcs group types to our own classes.
- Rewritten the ArmorGroup, Naval and EWR template handling

Rework GroundObjectBuyMenu to support templates
This commit is contained in:
RndName
2022-01-19 13:06:19 +01:00
parent d154069877
commit 5febcdd4e4
11 changed files with 1884 additions and 591 deletions

View File

@@ -0,0 +1,259 @@
import logging
from typing import Optional
from PySide2.QtCore import Signal
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
QCheckBox,
)
from game import Game
from game.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import (
VehicleGroupGroundObject,
SamGroundObject,
EwrGroundObject,
GroundGroup,
)
from gen.templates import (
TemplateCategory,
GroundObjectTemplate,
GroupTemplate,
)
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
class QGroundObjectGroupTemplate(QGroupBox):
group_template_changed = Signal(GroupTemplate)
# UI to show one GroupTemplate and configure the TemplateRandomizer for it
# one row: [Required | Unit Selector | Amount | Price]
# If the group is not randomizable: Just view labels instead of edit fields
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
super(QGroundObjectGroupTemplate, self).__init__(str(group_id + 1))
self.group_template = group_template
self.group_layout = QGridLayout()
self.setLayout(self.group_layout)
self.amount_selector = QSpinBox()
self.unit_selector = QComboBox()
self.group_selector = QCheckBox()
self.group_selector.setChecked(True)
self.group_selector.setEnabled(self.group_template.optional)
if self.group_template.randomizer:
# Group can be randomized
for unit in self.group_template.randomizer.possible_ground_units:
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.group_layout.addWidget(
self.unit_selector, 0, 0, alignment=Qt.AlignRight
)
self.group_layout.addWidget(
self.amount_selector, 0, 1, alignment=Qt.AlignRight
)
self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(len(self.group_template.units))
self.amount_selector.setValue(self.group_template.randomizer.unit_count)
self.on_group_changed()
else:
# Group can not be randomized so just show the group info
group_info = QVBoxLayout()
for unit_type, count in self.group_template.unit_types_count.items():
group_info.addWidget(
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft
)
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
self.amount_selector.valueChanged.connect(self.on_group_changed)
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
self.group_selector.stateChanged.connect(self.on_group_changed)
def on_group_changed(self) -> None:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
count = self.amount_selector.value() if self.group_selector.isChecked() else 0
self.group_template.randomizer.count = count
self.group_template.randomizer.force_type(unit_type.dcs_id)
self.group_template_changed.emit(self.group_template)
class QGroundObjectTemplateLayout(QGroupBox):
def __init__(
self,
game: Game,
ground_object: TheaterGroundObject,
template_changed_signal: Signal(GroundObjectTemplate),
current_group_value: int,
):
super(QGroundObjectTemplateLayout, self).__init__("Groups:")
# Connect to the signal to handle template updates
self.game = game
self.ground_object = ground_object
self.template_changed_signal = template_changed_signal
self.template_changed_signal.connect(self.load_for_template)
self.template: Optional[GroundObjectTemplate] = None
self.current_group_value = current_group_value
self.buy_button = QPushButton("Buy")
self.buy_button.clicked.connect(self.buy_group)
self.template_layout = QGridLayout()
self.setLayout(self.template_layout)
self.template_grid = QGridLayout()
self.template_layout.addLayout(self.template_grid, 0, 0, 1, 2)
self.template_layout.addWidget(self.buy_button, 1, 1)
stretch = QVBoxLayout()
stretch.addStretch()
self.template_layout.addLayout(stretch, 2, 0)
def load_for_template(self, template: GroundObjectTemplate) -> None:
self.template = template
# Clean the current grid
for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater()
for g_id, group in enumerate(template.groups):
group_row = QGroundObjectGroupTemplate(g_id, group)
group_row.group_template_changed.connect(self.group_template_changed)
self.template_grid.addWidget(group_row)
self.update_price()
def group_template_changed(self, group_template: GroupTemplate) -> None:
self.update_price()
def update_price(self) -> None:
price = "$" + str(self.template.estimated_price_for(self.ground_object))
if self.template.randomizable:
price = "~" + price
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
def buy_group(self):
if not self.template:
return
groups = self.generate_groups()
price = 0
for group in groups:
for unit in group.units:
if unit.unit_type:
price += unit.unit_type.price
price -= self.current_group_value
if price > self.game.blue.budget:
self.error_money()
self.close()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = groups
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()
def generate_groups(self) -> list[GroundGroup]:
go = self.template.generate(
self.ground_object.name,
PointWithHeading.from_point(
self.ground_object.position, self.ground_object.heading
),
self.ground_object.control_point,
self.game,
)
return go.groups
class QGroundObjectBuyMenu(QDialog):
template_changed_signal = Signal(GroundObjectTemplate)
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
game: Game,
current_group_value: int,
):
super(QGroundObjectBuyMenu, self).__init__(parent)
self.setMinimumWidth(350)
self.setWindowTitle("Buy ground object @ " + ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.template_selector = QComboBox()
self.template_selector.currentIndexChanged.connect(self.template_changed)
# Get the templates and fill the combobox
template_sub_category = None
if isinstance(ground_object, SamGroundObject):
template_category = TemplateCategory.AirDefence
elif isinstance(ground_object, VehicleGroupGroundObject):
template_category = TemplateCategory.Armor
elif isinstance(ground_object, EwrGroundObject):
template_category = TemplateCategory.AirDefence
template_sub_category = "EWR"
else:
raise RuntimeError
for template in game.blue.faction.templates.for_category(
template_category, template_sub_category
):
self.template_selector.addItem(template.name, userData=template)
template_selector_layout = QGridLayout()
template_selector_layout.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.template_selector, 0, 1, alignment=Qt.AlignRight
)
self.mainLayout.addLayout(template_selector_layout, 0, 0)
self.template_layout = QGroundObjectTemplateLayout(
game, ground_object, self.template_changed_signal, current_group_value
)
self.mainLayout.addWidget(self.template_layout, 1, 0)
self.setLayout(self.mainLayout)
# Update UI
self.template_changed()
def template_changed(self):
template = self.template_selector.itemData(
self.template_selector.currentIndex()
)
self.template_changed_signal.emit(template)

View File

@@ -1,17 +1,12 @@
import logging
from typing import List, Optional
from PySide2.QtGui import Qt
from PySide2.QtWidgets import (
QComboBox,
QDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QSpinBox,
QVBoxLayout,
)
from dcs import Point, vehicles
@@ -19,6 +14,7 @@ from dcs import Point, vehicles
from game import Game
from game.config import REWARDS
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
@@ -27,13 +23,11 @@ from game.theater.theatergroundobject import (
SamGroundObject,
VehicleGroupGroundObject,
)
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
from gen.sam.ewr_group_generator import get_faction_possible_ewrs_generator
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
from qt_ui.windows.groundobject.QGroundObjectBuyMenu import QGroundObjectBuyMenu
class QGroundObjectMenu(QDialog):
@@ -41,17 +35,12 @@ class QGroundObjectMenu(QDialog):
self,
parent,
ground_object: TheaterGroundObject,
buildings: Optional[List[TheaterGroundObject]],
cp: ControlPoint,
game: Game,
):
super().__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
if buildings is None:
self.buildings = []
else:
self.buildings = buildings
self.cp = cp
self.game = game
self.setWindowTitle(
@@ -231,225 +220,7 @@ class QGroundObjectMenu(QDialog):
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(
self, self.ground_object, self.cp, self.game, self.total_value
self.subwindow = QGroundObjectBuyMenu(
self, self.ground_object, self.game, self.total_value
)
self.subwindow.show()
class QBuyGroupForGroundObjectDialog(QDialog):
def __init__(
self,
parent,
ground_object: TheaterGroundObject,
cp: ControlPoint,
game: Game,
current_group_value: int,
):
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.current_group_value = current_group_value
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.buySamButton = QPushButton("Buy")
self.buyArmorButton = QPushButton("Buy")
self.buySamLayout = QGridLayout()
self.buyArmorLayout = QGridLayout()
self.amount = QSpinBox()
self.buyArmorCombo = QComboBox()
self.samCombo = QComboBox()
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
faction = self.game.blue.faction
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
# Pre Generate SAM to get the real price
generator = sam(self.game, self.ground_object)
generator.generate()
self.samCombo.addItem(
generator.name + " [$" + str(generator.price) + "M]", userData=generator
)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
self.buySamLayout.addLayout(stretch, 2, 0)
self.buySamButton.clicked.connect(self.buySam)
# EWRs
buy_ewr_box = QGroupBox("Buy EWR:")
buy_ewr_layout = QGridLayout()
buy_ewr_box.setLayout(buy_ewr_layout)
buy_ewr_layout.addWidget(QLabel("Radar type:"), 0, 0, Qt.AlignLeft)
self.ewr_selector = QComboBox()
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
ewr_types = get_faction_possible_ewrs_generator(faction)
for ewr_type in ewr_types:
# Pre Generate to get the real price
generator = ewr_type(self.game, self.ground_object)
generator.generate()
self.ewr_selector.addItem(
generator.name() + " [$" + str(generator.price) + "M]",
userData=generator,
)
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
self.buy_ewr_button = QPushButton("Buy")
self.buy_ewr_button.clicked.connect(self.buy_ewr)
buy_ewr_layout.addWidget(self.buy_ewr_button, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
buy_ewr_layout.addLayout(stretch, 2, 0)
# Armored units
for unit in set(faction.ground_units):
self.buyArmorCombo.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
self.amount.setMinimum(2)
self.amount.setMaximum(8)
self.amount.setValue(2)
self.amount.valueChanged.connect(self.amountComboChanged)
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft
)
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(
self.buyArmorButton, 2, 1, alignment=Qt.AlignRight
)
stretch2 = QVBoxLayout()
stretch2.addStretch()
self.buyArmorLayout.addLayout(stretch2, 3, 0)
self.buyArmorButton.clicked.connect(self.buyArmor)
# Do layout
self.buySamBox.setLayout(self.buySamLayout)
self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout()
if isinstance(self.ground_object, SamGroundObject):
self.mainLayout.addWidget(self.buySamBox)
elif isinstance(self.ground_object, VehicleGroupGroundObject):
self.mainLayout.addWidget(self.buyArmorBox)
elif isinstance(self.ground_object, EwrGroundObject):
self.mainLayout.addWidget(buy_ewr_box)
self.setLayout(self.mainLayout)
try:
self.samComboChanged(0)
self.armorComboChanged(0)
self.on_ewr_selection_changed(0)
except:
pass
def samComboChanged(self, index):
self.buySamButton.setText(
"Buy [$"
+ str(self.samCombo.itemData(index).price)
+ "M] [-$"
+ str(self.current_group_value)
+ "M]"
)
def on_ewr_selection_changed(self, index):
ewr = self.ewr_selector.itemData(index)
self.buy_ewr_button.setText(
f"Buy [${ewr.price}M][-${self.current_group_value}M]"
)
def armorComboChanged(self, index):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def amountComboChanged(self):
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = unit_type.price * self.amount.value()
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
def buyArmor(self):
logging.info("Buying Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
price = utype.price * self.amount.value() - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
self.close()
return
else:
self.game.blue.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(
self.game, self.ground_object, utype, int(self.amount.value())
)
self.ground_object.groups = [group]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = list(sam_generator.groups)
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def buy_ewr(self):
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
price = ewr_generator.price - self.current_group_value
if price > self.game.blue.budget:
self.error_money()
return
else:
self.game.blue.budget -= price
self.ground_object.groups = [ewr_generator.vg]
# Replan redfor missions
self.game.initialize_turn(for_red=True, for_blue=False)
GameUpdateSignal.get_instance().updateGame(self.game)
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()