Improve Layout loading and ForceGroup generation

- fix layout not preserving the correct group index
- fix ForceGroup generation merging preset_groups with generics
This commit is contained in:
RndName 2022-05-13 20:25:08 +02:00
parent 72682e4db3
commit 50b82f6383
No known key found for this signature in database
GPG Key ID: 5EF516FD9537F7C0
7 changed files with 115 additions and 84 deletions

View File

@ -35,12 +35,6 @@ class ArmedForces:
"""Initialize the ArmedForces for the given faction. """Initialize the ArmedForces for the given faction.
This will create a ForceGroup for each generic Layout and PresetGroup""" This will create a ForceGroup for each generic Layout and PresetGroup"""
# Initialize with preset_groups from the faction
self.forces = [
preset_group.initialize_for_faction(faction)
for preset_group in faction.preset_groups
]
# Generate ForceGroup for all generic layouts by iterating over # Generate ForceGroup for all generic layouts by iterating over
# all layouts which are usable by the given faction. # all layouts which are usable by the given faction.
for layout in LAYOUTS.layouts: for layout in LAYOUTS.layouts:
@ -48,6 +42,10 @@ class ArmedForces:
# Creates a faction compatible GorceGroup # Creates a faction compatible GorceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction)) self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
# Add all preset groups afterwards to prevent them being merged with generics
for preset_group in faction.preset_groups:
self.forces.append(preset_group.initialize_for_faction(faction))
def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]: def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]:
for force_group in self.forces: for force_group in self.forces:
if group_task in force_group.tasks: if group_task in force_group.tasks:
@ -59,7 +57,7 @@ class ArmedForces:
for group in self.groups_for_task(task): for group in self.groups_for_task(task):
if group not in groups: if group not in groups:
groups.append(group) groups.append(group)
return groups return sorted(groups, key=lambda g: g.name)
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]: def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
unit_groups = list(self.groups_for_task(group_task)) unit_groups = list(self.groups_for_task(group_task))

View File

@ -21,7 +21,7 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
) )
from game.layout import LAYOUTS from game.layout import LAYOUTS
from game.layout.layout import TgoLayout, TgoLayoutGroup from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
from game.utils import escape_string_for_lua from game.utils import escape_string_for_lua
@ -67,10 +67,10 @@ class ForceGroup:
""" """
units: set[UnitType[Any]] = set() units: set[UnitType[Any]] = set()
statics: set[Type[DcsUnitType]] = set() statics: set[Type[DcsUnitType]] = set()
for group in layout.all_groups: for unit_group in layout.all_unit_groups:
if group.optional and not group.fill: if unit_group.optional and not unit_group.fill:
continue continue
for unit_type in group.possible_types_for_faction(faction): for unit_type in unit_group.possible_types_for_faction(faction):
if issubclass(unit_type, VehicleType): if issubclass(unit_type, VehicleType):
units.add(next(GroundUnitType.for_dcs_type(unit_type))) units.add(next(GroundUnitType.for_dcs_type(unit_type)))
elif issubclass(unit_type, ShipType): elif issubclass(unit_type, ShipType):
@ -89,11 +89,11 @@ class ForceGroup:
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
def has_unit_for_layout_group(self, group: TgoLayoutGroup) -> bool: def has_unit_for_layout_group(self, unit_group: TgoLayoutUnitGroup) -> bool:
for unit in self.units: for unit in self.units:
if ( if (
unit.dcs_unit_type in group.unit_types unit.dcs_unit_type in unit_group.unit_types
or unit.unit_class in group.unit_classes or unit.unit_class in unit_group.unit_classes
): ):
return True return True
return False return False
@ -102,9 +102,9 @@ class ForceGroup:
"""Initialize a ForceGroup for the given Faction. """Initialize a ForceGroup for the given Faction.
This adds accessible units to LayoutGroups with the fill property""" This adds accessible units to LayoutGroups with the fill property"""
for layout in self.layouts: for layout in self.layouts:
for group in layout.all_groups: for unit_group in layout.all_unit_groups:
if group.fill and not self.has_unit_for_layout_group(group): if unit_group.fill and not self.has_unit_for_layout_group(unit_group):
for unit_type in group.possible_types_for_faction(faction): for unit_type in unit_group.possible_types_for_faction(faction):
if issubclass(unit_type, VehicleType): if issubclass(unit_type, VehicleType):
self.units.append( self.units.append(
next(GroundUnitType.for_dcs_type(unit_type)) next(GroundUnitType.for_dcs_type(unit_type))
@ -138,38 +138,44 @@ class ForceGroup:
) )
def dcs_unit_types_for_group( def dcs_unit_types_for_group(
self, group: TgoLayoutGroup self, unit_group: TgoLayoutUnitGroup
) -> list[Type[DcsUnitType]]: ) -> list[Type[DcsUnitType]]:
"""Return all available DCS Unit Types which can be used in the given """Return all available DCS Unit Types which can be used in the given
TgoLayoutGroup""" TgoLayoutGroup"""
unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)] unit_types = [
t for t in unit_group.unit_types if self.has_access_to_dcs_type(t)
]
alternative_types = [] alternative_types = []
for accessible_unit in self.units: for accessible_unit in self.units:
if accessible_unit.unit_class in group.unit_classes: if accessible_unit.unit_class in unit_group.unit_classes:
unit_types.append(accessible_unit.dcs_unit_type) unit_types.append(accessible_unit.dcs_unit_type)
if accessible_unit.unit_class in group.fallback_classes: if accessible_unit.unit_class in unit_group.fallback_classes:
alternative_types.append(accessible_unit.dcs_unit_type) alternative_types.append(accessible_unit.dcs_unit_type)
return unit_types or alternative_types return unit_types or alternative_types
def unit_types_for_group(self, group: TgoLayoutGroup) -> Iterator[UnitType[Any]]: def unit_types_for_group(
for dcs_type in self.dcs_unit_types_for_group(group): self, unit_group: TgoLayoutUnitGroup
) -> Iterator[UnitType[Any]]:
for dcs_type in self.dcs_unit_types_for_group(unit_group):
if issubclass(dcs_type, VehicleType): if issubclass(dcs_type, VehicleType):
yield next(GroundUnitType.for_dcs_type(dcs_type)) yield next(GroundUnitType.for_dcs_type(dcs_type))
elif issubclass(dcs_type, ShipType): elif issubclass(dcs_type, ShipType):
yield next(ShipUnitType.for_dcs_type(dcs_type)) yield next(ShipUnitType.for_dcs_type(dcs_type))
def statics_for_group(self, group: TgoLayoutGroup) -> Iterator[Type[DcsUnitType]]: def statics_for_group(
for dcs_type in self.dcs_unit_types_for_group(group): self, unit_group: TgoLayoutUnitGroup
) -> Iterator[Type[DcsUnitType]]:
for dcs_type in self.dcs_unit_types_for_group(unit_group):
if issubclass(dcs_type, StaticType): if issubclass(dcs_type, StaticType):
yield dcs_type yield dcs_type
def random_dcs_unit_type_for_group( def random_dcs_unit_type_for_group(
self, group: TgoLayoutGroup self, unit_group: TgoLayoutUnitGroup
) -> Type[DcsUnitType]: ) -> Type[DcsUnitType]:
"""Return random DCS Unit Type which can be used in the given TgoLayoutGroup""" """Return random DCS Unit Type which can be used in the given TgoLayoutGroup"""
return random.choice(self.dcs_unit_types_for_group(group)) return random.choice(self.dcs_unit_types_for_group(unit_group))
def merge_group(self, new_group: ForceGroup) -> None: def merge_group(self, new_group: ForceGroup) -> None:
"""Merge the group with another similar group.""" """Merge the group with another similar group."""
@ -204,22 +210,22 @@ class ForceGroup:
"""Create a TheaterGroundObject for the given template""" """Create a TheaterGroundObject for the given template"""
go = layout.create_ground_object(name, location, control_point) go = layout.create_ground_object(name, location, control_point)
# Generate all groups using the randomization if it defined # Generate all groups using the randomization if it defined
for group_name, groups in layout.groups.items(): for tgo_group in layout.groups:
for group in groups: for unit_group in tgo_group.unit_groups:
# Choose a random unit_type for the group # Choose a random unit_type for the group
try: try:
unit_type = self.random_dcs_unit_type_for_group(group) unit_type = self.random_dcs_unit_type_for_group(unit_group)
except IndexError: except IndexError:
if group.optional: if unit_group.optional:
# If group is optional it is ok when no unit_type is available # If group is optional it is ok when no unit_type is available
continue continue
# if non-optional this is a error # if non-optional this is a error
raise RuntimeError( raise RuntimeError(
f"No accessible unit for {self.name} - {group.name}" f"No accessible unit for {self.name} - {unit_group.name}"
) )
tgo_group_name = f"{name} ({group_name})" tgo_group_name = f"{name} ({tgo_group.group_name})"
self.create_theater_group_for_tgo( self.create_theater_group_for_tgo(
go, group, tgo_group_name, game, unit_type go, unit_group, tgo_group_name, game, unit_type
) )
return go return go
@ -227,7 +233,7 @@ class ForceGroup:
def create_theater_group_for_tgo( def create_theater_group_for_tgo(
self, self,
ground_object: TheaterGroundObject, ground_object: TheaterGroundObject,
group: TgoLayoutGroup, unit_group: TgoLayoutUnitGroup,
group_name: str, group_name: str,
game: Game, game: Game,
unit_type: Type[DcsUnitType], unit_type: Type[DcsUnitType],
@ -237,12 +243,12 @@ class ForceGroup:
# Random UnitCounter if not forced # Random UnitCounter if not forced
if unit_count is None: if unit_count is None:
# Choose a random group_size based on the layouts unit_count # Choose a random group_size based on the layouts unit_count
unit_count = group.group_size unit_count = unit_group.group_size
if unit_count == 0: if unit_count == 0:
# No units to be created so dont create a theater group for them # No units to be created so dont create a theater group for them
return return
# Generate Units # Generate Units
units = group.generate_units(ground_object, unit_type, unit_count) units = unit_group.generate_units(ground_object, unit_type, unit_count)
# Get or create the TheaterGroup # Get or create the TheaterGroup
ground_group = ground_object.group_by_name(group_name) ground_group = ground_object.group_by_name(group_name)
if ground_group is not None: if ground_group is not None:
@ -261,9 +267,9 @@ class ForceGroup:
): ):
# Recreate the TheaterGroup as IadsGroundGroup # Recreate the TheaterGroup as IadsGroundGroup
ground_group = IadsGroundGroup.from_group(ground_group) ground_group = IadsGroundGroup.from_group(ground_group)
if group.sub_task is not None: if unit_group.sub_task is not None:
# Use the special sub_task of the TheaterGroup # Use the special sub_task of the TheaterGroup
iads_task = group.sub_task iads_task = unit_group.sub_task
else: else:
# Use the primary task of the ForceGroup # Use the primary task of the ForceGroup
iads_task = self.tasks[0] iads_task = self.tasks[0]

View File

@ -1,4 +1,4 @@
from .layout import TgoLayout, TgoLayoutGroup from .layout import TgoLayout, TgoLayoutGroup, TgoLayoutUnitGroup
from .layoutloader import LayoutLoader from .layoutloader import LayoutLoader
LAYOUTS = LayoutLoader() LAYOUTS = LayoutLoader()

View File

@ -60,6 +60,21 @@ class LayoutUnit:
@dataclass @dataclass
class TgoLayoutGroup: class TgoLayoutGroup:
"""The layout of a group which will generate a DCS group later. The TgoLayoutGroup has one or many TgoLayoutUnitGroup which represents a set of unit of the same type. Therefore the TgoLayoutGroup is a logical grouping of different unit_types. A TgoLayout can have one or many TgoLayoutGroup"""
# The group name which will be used later as the DCS group name
group_name: str
# The index of the group within the TgoLayout. Used to preserve that the order of
# the groups generated match the order defined in the layout yaml.
group_index: int
# List of all connected TgoLayoutUnitGroup
unit_groups: list[TgoLayoutUnitGroup] = field(default_factory=list)
@dataclass
class TgoLayoutUnitGroup:
"""The layout of a single type of unit within the TgoLayout """The layout of a single type of unit within the TgoLayout
Each DCS group that is spawned in the mission is composed of one or more Each DCS group that is spawned in the mission is composed of one or more
@ -95,6 +110,9 @@ class TgoLayoutGroup:
unit_classes: list[UnitClass] = field(default_factory=list) unit_classes: list[UnitClass] = field(default_factory=list)
fallback_classes: list[UnitClass] = field(default_factory=list) fallback_classes: list[UnitClass] = field(default_factory=list)
# The index of the TgoLayoutGroup within the Layout
unit_index: int = field(default_factory=int)
# Allows a group to have a special SubTask (PointDefence for example) # Allows a group to have a special SubTask (PointDefence for example)
sub_task: Optional[GroupTask] = None sub_task: Optional[GroupTask] = None
@ -172,21 +190,13 @@ class TgoLayout:
self.description = description self.description = description
self.tasks: list[GroupTask] = [] # The supported self.tasks: list[GroupTask] = [] # The supported
# Mapping of group name and LayoutGroups for a specific TgoGroup # All TgoGroups this layout has.
# A Group can have multiple TgoLayoutGroups which get merged together self.groups: list[TgoLayoutGroup] = []
self.groups: dict[str, list[TgoLayoutGroup]] = defaultdict(list)
# A generic layout will be used to create generic ForceGroups during the # A generic layout will be used to create generic ForceGroups during the
# campaign initialization. For each generic layout a new Group will be created. # campaign initialization. For each generic layout a new Group will be created.
self.generic: bool = False self.generic: bool = False
def add_layout_group(
self, name: str, group: TgoLayoutGroup, index: int = 0
) -> None:
"""Adds the layout group to the group dict at the given index"""
# If the index is greater than the actual len it will add it after the last item
self.groups[name].insert(min(len(self.groups[name]), index), group)
def usable_by_faction(self, faction: Faction) -> bool: def usable_by_faction(self, faction: Faction) -> bool:
# Special handling for Buildings # Special handling for Buildings
if ( if (
@ -199,7 +209,7 @@ class TgoLayout:
try: try:
return all( return all(
len(group.possible_types_for_faction(faction)) > 0 len(group.possible_types_for_faction(faction)) > 0
for group in self.all_groups for group in self.all_unit_groups
if not group.optional if not group.optional
) )
except LayoutException: except LayoutException:
@ -219,9 +229,9 @@ class TgoLayout:
raise NotImplementedError raise NotImplementedError
@property @property
def all_groups(self) -> Iterator[TgoLayoutGroup]: def all_unit_groups(self) -> Iterator[TgoLayoutUnitGroup]:
for groups in self.groups.values(): for group in self.groups:
yield from groups yield from group.unit_groups
class AntiAirLayout(TgoLayout): class AntiAirLayout(TgoLayout):

View File

@ -18,6 +18,7 @@ from game.data.groups import GroupRole
from game.layout.layout import ( from game.layout.layout import (
TgoLayout, TgoLayout,
TgoLayoutGroup, TgoLayoutGroup,
TgoLayoutUnitGroup,
LayoutUnit, LayoutUnit,
AntiAirLayout, AntiAirLayout,
BuildingLayout, BuildingLayout,
@ -29,10 +30,10 @@ from game.layout.layoutmapping import LayoutMapping
from game.profiling import logged_duration from game.profiling import logged_duration
from game.version import VERSION from game.version import VERSION
TEMPLATE_DIR = "resources/layouts/" LAYOUT_DIR = "resources/layouts/"
TEMPLATE_DUMP = "Liberation/layouts.p" LAYOUT_DUMP = "Liberation/layouts.p"
TEMPLATE_TYPES = { LAYOUT_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout, GroupRole.AIR_DEFENSE: AntiAirLayout,
GroupRole.BUILDING: BuildingLayout, GroupRole.BUILDING: BuildingLayout,
GroupRole.NAVAL: NavalLayout, GroupRole.NAVAL: NavalLayout,
@ -62,7 +63,7 @@ class LayoutLoader:
"""This will load all pre-loaded layouts from a pickle file. """This will load all pre-loaded layouts from a pickle file.
If pickle can not be loaded it will import and dump the layouts""" If pickle can not be loaded it will import and dump the layouts"""
# We use a pickle for performance reasons. Importing takes many seconds # We use a pickle for performance reasons. Importing takes many seconds
file = Path(persistency.base_path()) / TEMPLATE_DUMP file = Path(persistency.base_path()) / LAYOUT_DUMP
if file.is_file(): if file.is_file():
# Load from pickle if existing # Load from pickle if existing
with file.open("rb") as f: with file.open("rb") as f:
@ -82,7 +83,7 @@ class LayoutLoader:
self._layouts = {} self._layouts = {}
mappings: dict[str, list[LayoutMapping]] = defaultdict(list) mappings: dict[str, list[LayoutMapping]] = defaultdict(list)
with logged_duration("Parsing mapping yamls"): with logged_duration("Parsing mapping yamls"):
for file in Path(TEMPLATE_DIR).rglob("*.yaml"): for file in Path(LAYOUT_DIR).rglob("*.yaml"):
if not file.is_file(): if not file.is_file():
raise RuntimeError(f"{file.name} is not a file") raise RuntimeError(f"{file.name} is not a file")
with file.open("r", encoding="utf-8") as f: with file.open("r", encoding="utf-8") as f:
@ -95,11 +96,17 @@ class LayoutLoader:
with ThreadPoolExecutor() as exe: with ThreadPoolExecutor() as exe:
exe.map(self._load_from_miz, mappings.keys(), mappings.values()) exe.map(self._load_from_miz, mappings.keys(), mappings.values())
# Sort al the LayoutGroups with the correct index
for layout in self._layouts.values():
layout.groups.sort(key=lambda g: g.group_index)
for group in layout.groups:
group.unit_groups.sort(key=lambda ug: ug.unit_index)
logging.info(f"Imported {len(self._layouts)} layouts") logging.info(f"Imported {len(self._layouts)} layouts")
self._dump_templates() self._dump_templates()
def _dump_templates(self) -> None: def _dump_templates(self) -> None:
file = Path(persistency.base_path()) / TEMPLATE_DUMP file = Path(persistency.base_path()) / LAYOUT_DUMP
dump = (VERSION, self._layouts) dump = (VERSION, self._layouts)
with file.open("wb") as fdata: with file.open("wb") as fdata:
pickle.dump(dump, fdata) pickle.dump(dump, fdata)
@ -127,7 +134,7 @@ class LayoutLoader:
): ):
try: try:
g_id, group_name, group_mapping = mapping.group_for_name( g_id, u_id, group_name, group_mapping = mapping.group_for_name(
dcs_group.name dcs_group.name
) )
except KeyError: except KeyError:
@ -143,40 +150,46 @@ class LayoutLoader:
layout = self._layouts.get(mapping.name, None) layout = self._layouts.get(mapping.name, None)
if layout is None: if layout is None:
# Create a new template # Create a new template
layout = TEMPLATE_TYPES[mapping.primary_role]( layout = LAYOUT_TYPES[mapping.primary_role](
mapping.name, mapping.description mapping.name, mapping.description
) )
layout.generic = mapping.generic layout.generic = mapping.generic
layout.tasks = mapping.tasks layout.tasks = mapping.tasks
self._layouts[layout.name] = layout self._layouts[layout.name] = layout
for i, unit in enumerate(dcs_group.units): for i, unit in enumerate(dcs_group.units):
group_layout = None unit_group = None
for group in layout.all_groups: for _unit_group in layout.all_unit_groups:
if group.name == group_mapping.name: if _unit_group.name == group_mapping.name:
# We already have a layoutgroup for this dcs_group # We already have a layoutgroup for this dcs_group
group_layout = group unit_group = _unit_group
if not group_layout: if not unit_group:
group_layout = TgoLayoutGroup( unit_group = TgoLayoutUnitGroup(
group_mapping.name, group_mapping.name,
[], [],
group_mapping.unit_count, group_mapping.unit_count,
group_mapping.unit_types, group_mapping.unit_types,
group_mapping.unit_classes, group_mapping.unit_classes,
group_mapping.fallback_classes, group_mapping.fallback_classes,
u_id,
) )
group_layout.optional = group_mapping.optional unit_group.optional = group_mapping.optional
group_layout.fill = group_mapping.fill unit_group.fill = group_mapping.fill
group_layout.sub_task = group_mapping.sub_task unit_group.sub_task = group_mapping.sub_task
# Add the group at the correct index tgo_group = None
layout.add_layout_group(group_name, group_layout, g_id) for _tgo_group in layout.groups:
if _tgo_group.group_name == group_name:
tgo_group = _tgo_group
if tgo_group is None:
tgo_group = TgoLayoutGroup(group_name, g_id)
layout.groups.append(tgo_group)
tgo_group.unit_groups.append(unit_group)
layout_unit = LayoutUnit.from_unit(unit) layout_unit = LayoutUnit.from_unit(unit)
if i == 0 and layout.name not in template_position: if i == 0 and layout.name not in template_position:
template_position[layout.name] = unit.position template_position[layout.name] = unit.position
layout_unit.position = ( layout_unit.position = (
layout_unit.position - template_position[layout.name] layout_unit.position - template_position[layout.name]
) )
group_layout.layout_units.append(layout_unit) unit_group.layout_units.append(layout_unit)
def by_name(self, name: str) -> TgoLayout: def by_name(self, name: str) -> TgoLayout:
self.initialize() self.initialize()

View File

@ -128,9 +128,11 @@ class LayoutMapping:
layout_file, layout_file,
) )
def group_for_name(self, name: str) -> tuple[int, str, GroupLayoutMapping]: def group_for_name(self, name: str) -> tuple[int, int, str, GroupLayoutMapping]:
g_id = 0
for group_name, group_mappings in self.groups.items(): for group_name, group_mappings in self.groups.items():
for g_id, group_mapping in enumerate(group_mappings): for u_id, group_mapping in enumerate(group_mappings):
if group_mapping.name == name or name in group_mapping.statics: if group_mapping.name == name or name in group_mapping.statics:
return g_id, group_name, group_mapping return g_id, u_id, group_name, group_mapping
g_id += 1
raise KeyError raise KeyError

View File

@ -25,7 +25,7 @@ from game.data.groups import GroupRole, GroupTask
from game.layout.layout import ( from game.layout.layout import (
LayoutException, LayoutException,
TgoLayout, TgoLayout,
TgoLayoutGroup, TgoLayoutUnitGroup,
) )
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
@ -38,7 +38,7 @@ from qt_ui.uiconstants import EVENT_ICONS
@dataclass @dataclass
class QTgoLayoutGroup: class QTgoLayoutGroup:
layout: TgoLayoutGroup layout: TgoLayoutUnitGroup
dcs_unit_type: Type[UnitType] dcs_unit_type: Type[UnitType]
amount: int amount: int
unit_price: int unit_price: int
@ -63,7 +63,7 @@ class QTgoLayout:
class QTgoLayoutGroupRow(QWidget): class QTgoLayoutGroupRow(QWidget):
group_template_changed = Signal() group_template_changed = Signal()
def __init__(self, force_group: ForceGroup, group: TgoLayoutGroup) -> None: def __init__(self, force_group: ForceGroup, group: TgoLayoutUnitGroup) -> None:
super().__init__() super().__init__()
self.grid_layout = QGridLayout() self.grid_layout = QGridLayout()
self.setLayout(self.grid_layout) self.setLayout(self.grid_layout)
@ -170,8 +170,10 @@ class QGroundObjectTemplateLayout(QGroupBox):
self.layout_model.groups = defaultdict(list) self.layout_model.groups = defaultdict(list)
for id in range(self.template_grid.count()): for id in range(self.template_grid.count()):
self.template_grid.itemAt(id).widget().deleteLater() self.template_grid.itemAt(id).widget().deleteLater()
for group_name, groups in self.layout_model.layout.groups.items(): for group in self.layout_model.layout.groups:
self.add_theater_group(group_name, self.layout_model.force_group, groups) self.add_theater_group(
group.group_name, self.layout_model.force_group, group.unit_groups
)
self.group_template_changed() self.group_template_changed()
@property @property
@ -183,7 +185,7 @@ class QGroundObjectTemplateLayout(QGroupBox):
return self.cost <= self.game.blue.budget return self.cost <= self.game.blue.budget
def add_theater_group( def add_theater_group(
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutGroup] self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutUnitGroup]
) -> None: ) -> None:
group_box = QGroupBox(group_name) group_box = QGroupBox(group_name)
vbox_layout = QVBoxLayout() vbox_layout = QVBoxLayout()