Raffson ff2ec07d83
Move constants from laypout.py to theathergroup.py
Should fix the "FIXED_POS_ARG not defined" error
2024-10-12 16:35:04 +02:00

337 lines
12 KiB
Python

from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Iterator, Type, Optional
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.theater.iadsnetwork.iadsrole import IadsRole
from game.theater.presetlocation import PresetLocation
from game.theater.theatergroundobject import (
IadsBuildingGroundObject,
SamGroundObject,
EwrGroundObject,
BuildingGroundObject,
MissileSiteGroundObject,
ShipGroundObject,
CarrierGroundObject,
LhaGroundObject,
CoastalSiteGroundObject,
VehicleGroupGroundObject,
IadsGroundObject,
)
from game.theater.theatergroup import TheaterUnit
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 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
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)
# 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)
sub_task: Optional[GroupTask] = None
# Defines if this groupTemplate is required or not
optional: bool = False
# Should this be filled by accessible units if optional or not
fill: bool = True
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) + 1))
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,
fixed_pos: bool,
fixed_hdg: bool,
) -> list[TheaterUnit]:
"""Generate units of the given unit type and amount for the TgoLayoutGroup"""
if amount > len(self.layout_units):
raise LayoutException(
f"{self.name} has incorrect unit_count for {unit_type.id}"
)
return [
TheaterUnit.from_template(
i,
unit_type,
self.layout_units[i],
go,
fixed_pos,
fixed_hdg,
)
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
# All TgoGroups this layout has.
self.groups: list[TgoLayoutGroup] = []
# 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 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_unit_groups
if not group.optional
)
except LayoutException:
return False
def create_ground_object(
self,
name: str,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> 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_unit_groups(self) -> Iterator[TgoLayoutUnitGroup]:
for group in self.groups:
yield from group.unit_groups
class AntiAirLayout(TgoLayout):
def create_ground_object(
self,
name: str,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> IadsGroundObject:
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
return EwrGroundObject(name, location, control_point)
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
return SamGroundObject(name, location, control_point, task)
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,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> BuildingGroundObject:
iads_role = IadsRole.for_category(self.category)
tgo_type = (
IadsBuildingGroundObject if iads_role.participate else BuildingGroundObject
)
return tgo_type(
name,
self.category,
location,
control_point,
task,
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,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> TheaterGroundObject:
if GroupTask.NAVY in self.tasks:
return ShipGroundObject(name, location, control_point)
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
return CarrierGroundObject(name, location, control_point)
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
return LhaGroundObject(name, location, control_point)
raise NotImplementedError
class DefensesLayout(TgoLayout):
def create_ground_object(
self,
name: str,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> TheaterGroundObject:
if GroupTask.MISSILE in self.tasks:
return MissileSiteGroundObject(name, location, control_point)
elif GroupTask.COASTAL in self.tasks:
return CoastalSiteGroundObject(name, location, control_point)
raise NotImplementedError
class GroundForceLayout(TgoLayout):
def create_ground_object(
self,
name: str,
location: PresetLocation,
control_point: ControlPoint,
task: Optional[GroupTask],
) -> TheaterGroundObject:
return VehicleGroupGroundObject(name, location, control_point, task)