mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add handling to insert the layout group at the correct index according to the order used in the layout yaml
298 lines
11 KiB
Python
298 lines
11 KiB
Python
from __future__ import annotations
|
|
from collections import defaultdict
|
|
|
|
import logging
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Iterator, Type
|
|
|
|
from dcs import Point
|
|
from dcs.unit import Unit
|
|
from dcs.unittype import UnitType as DcsUnitType
|
|
|
|
from game.data.groups import GroupRole, GroupTask
|
|
from game.data.units import UnitClass
|
|
from game.point_with_heading import PointWithHeading
|
|
from game.theater.theatergroundobject import (
|
|
SamGroundObject,
|
|
EwrGroundObject,
|
|
BuildingGroundObject,
|
|
MissileSiteGroundObject,
|
|
ShipGroundObject,
|
|
CarrierGroundObject,
|
|
LhaGroundObject,
|
|
CoastalSiteGroundObject,
|
|
VehicleGroupGroundObject,
|
|
IadsGroundObject,
|
|
)
|
|
from game.theater.theatergroup import TheaterUnit
|
|
from game.utils import Heading
|
|
|
|
if TYPE_CHECKING:
|
|
from game.factions.faction import Faction
|
|
from game.theater.theatergroundobject import TheaterGroundObject
|
|
from game.theater.controlpoint import ControlPoint
|
|
|
|
|
|
class LayoutException(Exception):
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class LayoutUnit:
|
|
"""The Position and Orientation of a single unit within the GroupLayout"""
|
|
|
|
name: str
|
|
position: Point
|
|
heading: int
|
|
|
|
@staticmethod
|
|
def from_unit(unit: Unit) -> LayoutUnit:
|
|
"""Creates a LayoutUnit from a DCS Unit"""
|
|
return LayoutUnit(
|
|
unit.name,
|
|
unit.position,
|
|
int(unit.heading),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TgoLayoutGroup:
|
|
"""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
|
|
TgoLayoutGroup. Each TgoLayoutGroup will generate only a single type of unit.
|
|
|
|
The merging of multiple TgoLayoutGroups to a single DCS group is defined in the
|
|
TgoLayout with a dict which uses the Dcs group name as key and the corresponding
|
|
TgoLayoutGroups as values.
|
|
|
|
Each TgoLayoutGroup will be filled with a single type of unit when generated. The
|
|
types compatible with the position can either be specified precisely (with
|
|
unit_types) or generically (with unit_classes). If neither list specifies units
|
|
that can be fulfilled by the faction, fallback_classes will be used. This allows
|
|
the early-warning radar template, which prefers units that are defined as early
|
|
warning radars like the 55G6, but to fall back to any radar usable by the faction
|
|
if EWRs are not available.
|
|
|
|
A TgoLayoutGroup may be optional. Factions or ForceGroups that are not able to
|
|
provide an actual unit for the TgoLayoutGroup will still be able to use the layout;
|
|
the optional TgoLayoutGroup will be omitted.
|
|
"""
|
|
|
|
name: str
|
|
layout_units: list[LayoutUnit]
|
|
|
|
# Define the amount of units to be created. This can be a fixed int or a random
|
|
# choice from a range of two ints. If the list is empty it will use the whole group
|
|
# size / all available LayoutUnits
|
|
unit_count: list[int] = field(default_factory=list)
|
|
|
|
# defintion which unit types are supported
|
|
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
|
|
unit_classes: list[UnitClass] = field(default_factory=list)
|
|
fallback_classes: list[UnitClass] = field(default_factory=list)
|
|
|
|
# Defines if this groupTemplate is required or not
|
|
optional: bool = False
|
|
|
|
def possible_types_for_faction(self, faction: Faction) -> list[Type[DcsUnitType]]:
|
|
"""Determine the possible dcs unit types for the TgoLayoutGroup and the given faction"""
|
|
unit_types = [t for t in self.unit_types if faction.has_access_to_dcs_type(t)]
|
|
|
|
alternative_types = []
|
|
for accessible_unit in faction.accessible_units:
|
|
if accessible_unit.unit_class in self.unit_classes:
|
|
unit_types.append(accessible_unit.dcs_unit_type)
|
|
if accessible_unit.unit_class in self.fallback_classes:
|
|
alternative_types.append(accessible_unit.dcs_unit_type)
|
|
|
|
if not unit_types and not alternative_types and not self.optional:
|
|
raise LayoutException(f"{self.name} not usable by faction {faction.name}")
|
|
|
|
return unit_types or alternative_types
|
|
|
|
@property
|
|
def group_size(self) -> int:
|
|
"""The amount of units to be generated. If unit_count is defined in the layout this will be randomized accordingly. Otherwise this will be the maximum size."""
|
|
if self.unit_count:
|
|
if len(self.unit_count) == 1:
|
|
return self.unit_count[0]
|
|
return random.choice(range(min(self.unit_count), max(self.unit_count)))
|
|
return self.max_size
|
|
|
|
@property
|
|
def max_size(self) -> int:
|
|
return len(self.layout_units)
|
|
|
|
def generate_units(
|
|
self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int
|
|
) -> list[TheaterUnit]:
|
|
"""Generate units of the given unit type and amount for the TgoLayoutGroup"""
|
|
return [
|
|
TheaterUnit.from_template(i, unit_type, self.layout_units[i], go)
|
|
for i in range(amount)
|
|
]
|
|
|
|
|
|
class TgoLayout:
|
|
"""TgoLayout defines how a TheaterGroundObject will be generated from a ForceGroup. This defines the positioning, orientation, type and amount of the actual units
|
|
|
|
Each TgoLayout is defined in resources/layouts with a .yaml file which has all the
|
|
information about the Layout next to a .miz file which gives information about the
|
|
actual position (x, y) and orientation (heading) of the units. The layout file also
|
|
defines the structure of the DCS group (or groups) that will be spawned in the
|
|
mission. Complex groups like SAMs protected by point-defense require specific
|
|
grouping when used with plugins like Skynet. One group would define the main
|
|
battery (the search and track radars, launchers, C2 units, etc), another would
|
|
define PD units, and others could define SHORADs or resupply units.
|
|
|
|
Each group (representing a DCS group) is further divided into TgoLayoutGroups. As
|
|
a TgoLayoutGroup only represents a single dcs unit type the logical dcs group of multiple unit types will be created with the usage of a dict which has the DCS Group name as key and a list of TgoLayoutGroups which will be merged into this single dcs group.
|
|
|
|
As the TgoLayout will be used to create a TheaterGroundObject for a ForceGroup,
|
|
specialized classes inherit from this base class. For example there is a special
|
|
AiDefenseLayout which will be used to create the SamGroundObject from it.
|
|
"""
|
|
|
|
def __init__(self, name: str, description: str = "") -> None:
|
|
self.name = name
|
|
self.description = description
|
|
self.tasks: list[GroupTask] = [] # The supported
|
|
|
|
# Mapping of group name and LayoutGroups for a specific TgoGroup
|
|
# A Group can have multiple TgoLayoutGroups which get merged together
|
|
self.groups: dict[str, list[TgoLayoutGroup]] = defaultdict(list)
|
|
|
|
# A generic layout will be used to create generic ForceGroups during the
|
|
# campaign initialization. For each generic layout a new Group will be created.
|
|
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:
|
|
# Special handling for Buildings
|
|
if (
|
|
isinstance(self, BuildingLayout)
|
|
and self.category not in faction.building_set
|
|
):
|
|
return False
|
|
|
|
# Check if faction has at least 1 possible unit for non-optional groups
|
|
try:
|
|
return all(
|
|
len(group.possible_types_for_faction(faction)) > 0
|
|
for group in self.all_groups
|
|
if not group.optional
|
|
)
|
|
except LayoutException:
|
|
return False
|
|
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> TheaterGroundObject:
|
|
"""Create the TheaterGroundObject for the TgoLayout
|
|
|
|
This function has to be implemented by the inheriting class to create
|
|
a specific TGO like SamGroundObject or BuildingGroundObject
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def all_groups(self) -> Iterator[TgoLayoutGroup]:
|
|
for groups in self.groups.values():
|
|
yield from groups
|
|
|
|
|
|
class AntiAirLayout(TgoLayout):
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> IadsGroundObject:
|
|
|
|
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
|
return EwrGroundObject(name, position, position.heading, control_point)
|
|
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
|
return SamGroundObject(name, position, position.heading, control_point)
|
|
raise RuntimeError(
|
|
f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})"
|
|
)
|
|
|
|
|
|
class BuildingLayout(TgoLayout):
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> BuildingGroundObject:
|
|
return BuildingGroundObject(
|
|
name,
|
|
self.category,
|
|
position,
|
|
Heading.from_degrees(0),
|
|
control_point,
|
|
self.category == "fob",
|
|
)
|
|
|
|
@property
|
|
def category(self) -> str:
|
|
for task in self.tasks:
|
|
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
|
|
return task.description.lower()
|
|
raise RuntimeError(f"Building Template {self.name} has no building category")
|
|
|
|
|
|
class NavalLayout(TgoLayout):
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> TheaterGroundObject:
|
|
if GroupTask.NAVY in self.tasks:
|
|
return ShipGroundObject(name, position, control_point)
|
|
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
|
|
return CarrierGroundObject(name, control_point)
|
|
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
|
|
return LhaGroundObject(name, control_point)
|
|
raise NotImplementedError
|
|
|
|
|
|
class DefensesLayout(TgoLayout):
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> TheaterGroundObject:
|
|
if GroupTask.MISSILE in self.tasks:
|
|
return MissileSiteGroundObject(
|
|
name, position, position.heading, control_point
|
|
)
|
|
elif GroupTask.COASTAL in self.tasks:
|
|
return CoastalSiteGroundObject(
|
|
name, position, control_point, position.heading
|
|
)
|
|
raise NotImplementedError
|
|
|
|
|
|
class GroundForceLayout(TgoLayout):
|
|
def create_ground_object(
|
|
self,
|
|
name: str,
|
|
position: PointWithHeading,
|
|
control_point: ControlPoint,
|
|
) -> TheaterGroundObject:
|
|
return VehicleGroupGroundObject(name, position, position.heading, control_point)
|