dcs-retribution/gen/templates.py
RndName daf4704fe7 Rotate ground-units as in the campaign miz
This implements a logic to rotate groups which are generated from templates like the origin (ground unit) which was placed by the campaign designer
2022-02-21 20:45:41 +01:00

585 lines
19 KiB
Python

from __future__ import annotations
import copy
import json
import logging
import random
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Iterator, Any, TYPE_CHECKING, Optional, Union
from dcs import Point
from dcs.ships import ship_map
from dcs.unit import Unit
from dcs.unittype import UnitType
from dcs.vehicles import vehicle_map
from game.data.radar_db import UNITS_WITH_RADAR
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import (
SamGroundObject,
EwrGroundObject,
BuildingGroundObject,
GroundGroup,
MissileSiteGroundObject,
ShipGroundObject,
CarrierGroundObject,
LhaGroundObject,
CoastalSiteGroundObject,
VehicleGroupGroundObject,
IadsGroundObject,
)
from game.point_with_heading import PointWithHeading
from game.utils import Heading
from game import db
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
class TemplateEncoder(json.JSONEncoder):
def default(self, obj: Any) -> dict[str, Any]:
if hasattr(obj, "to_json"):
return obj.to_json()
else:
return obj.__dict__
class TemplateCategory(Enum):
AirDefence = "AirDefence" # Has subcategories for the AARange
Building = "Building" # Has subcategories from
Naval = "Naval" # Has subcategories lha, carrier, ship
Armor = "Armor"
Missile = "Missile"
Coastal = "Coastal"
@dataclass
class UnitTemplate:
name: str
type: str
position: Point
heading: int
@staticmethod
def from_unit(unit: Unit) -> UnitTemplate:
return UnitTemplate(
unit.name,
unit.type,
Point(int(unit.position.x), int(unit.position.y)),
int(unit.heading),
)
@staticmethod
def from_dict(d_unit: dict[str, Any]) -> UnitTemplate:
return UnitTemplate(
d_unit["name"],
d_unit["type"],
Point(d_unit["position"]["x"], d_unit["position"]["y"]),
d_unit["heading"],
)
@dataclass
class GroupTemplate:
name: str
units: list[UnitTemplate]
# Is Static group
static: bool = False
# Every group can only have one Randomizer
randomizer: Optional[TemplateRandomizer] = None
# Defines if this groupTemplate is required or not
optional: bool = False
@staticmethod
def from_dict(d_group: dict[str, Any]) -> GroupTemplate:
units = [UnitTemplate.from_dict(unit) for unit in d_group["units"]]
randomizer = (
TemplateRandomizer.from_dict(d_group["randomizer"])
if d_group["randomizer"]
else None
)
return GroupTemplate(
d_group["name"], units, d_group["static"], randomizer, d_group["optional"]
)
@property
def unit_types_count(self) -> dict[str, int]:
units: dict[str, int] = {}
for unit in self.units:
if unit.type in units:
units[unit.type] += 1
else:
units[unit.type] = 1
return units
@dataclass
class TemplateRandomizer:
# Selection of units to apply the randomization.
# If left empty the randomizer will be applied to all unit of the group
units: list[int] = field(default_factory=list)
# Define the amount of random units to be created by the randomizer.
# This can be a fixed int or a random value from a range of two ints as tuple
count: Union[int, list[int]] = field(default=1)
# The randomizer can pick a random unit type from a faction list like
# frontline_units or air_defenses to allow faction sensitive randomization
faction_types: list[str] = field(default_factory=list)
# Only works for vehicle units. Allows to specify the type class of the unit.
# For example this allows to select frontline_units as faction_type and also define
# Shorads as class to only pick AntiAir from the list
type_classes: list[str] = field(default_factory=list)
# Allows to define the exact UnitTypes the randomizer picks from. these have to be
# the dcs_unit_types found in the pydcs arrays
unit_types: list[str] = field(default_factory=list)
# Runtime Attributes
_initialized: bool = False
_possible_types: list[str] = field(default_factory=list)
_random_unit_type: Optional[str] = None
_forced_unit_type: Optional[str] = None
_unit_counter: Optional[int] = None
def to_json(self) -> dict[str, Any]:
d = self.__dict__
# Do not serialize the runtime attributes
d.pop("_initialized", None)
d.pop("_possible_types", None)
d.pop("_random_unit_type", None)
d.pop("_forced_unit_type", None)
d.pop("_unit_counter", None)
return d
@staticmethod
def from_dict(d: dict[str, Any]) -> TemplateRandomizer:
return TemplateRandomizer(
d["units"],
d["count"],
d["faction_types"],
d["type_classes"],
d["unit_types"],
)
@property
def possible_ground_units(self) -> Iterator[GroundUnitType]:
for unit_type in self._possible_types:
if unit_type in vehicle_map:
dcs_unit_type = vehicle_map[unit_type]
try:
yield next(GroundUnitType.for_dcs_type(dcs_unit_type))
except StopIteration:
continue
def force_type(self, type: str) -> None:
self._forced_unit_type = type
def randomize(self) -> None:
self.randomize_unit_type()
self.reset_unit_counter()
def reset_unit_counter(self) -> None:
if isinstance(self.count, list):
count = random.choice(range(self.count[0], self.count[1]))
elif isinstance(self.count, int):
count = self.count
self._unit_counter = count
def init_randomization_for_faction(self, faction: Faction) -> None:
# Initializes the randomization
# This sets the random_unit_type and the random_unit_count
if self._initialized:
return
type_list = []
for faction_type in self.faction_types:
for unit_type in faction[faction_type]:
if isinstance(unit_type, GroundUnitType):
# GroundUnitType
type_list.append(unit_type.dcs_id)
elif issubclass(unit_type, UnitType):
# DCS Unit Type object
type_list.append(unit_type.id)
elif db.unit_type_from_name(unit_type):
# DCS Unit Type as string
type_list.append(unit_type)
else:
raise KeyError
if self.unit_types and self.faction_types:
# If Faction types were defined use unit_types as filter
filtered_type_list = [
unit_type for unit_type in type_list if unit_type in self.unit_types
]
type_list = filtered_type_list
else:
# If faction_types is not defined append the unit_types
for unit_type in self.unit_types:
type_list.append(unit_type)
if self.type_classes:
filtered_type_list = []
for unit_type in type_list:
if unit_type in vehicle_map:
dcs_type = vehicle_map[unit_type]
else:
continue
try:
ground_unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
except (KeyError, StopIteration):
logging.error(f"Unit {unit_type} has no GroundUnitType")
continue
if (
ground_unit_type.unit_class
and ground_unit_type.unit_class.value in self.type_classes
):
filtered_type_list.append(unit_type)
type_list = filtered_type_list
self._possible_types = type_list
if self.randomize_unit_type():
self.reset_unit_counter()
self._initialized = True
@property
def unit_type(self) -> Optional[str]:
return self._random_unit_type
def randomize_unit_type(self) -> bool:
try:
self._random_unit_type = self._forced_unit_type or random.choice(
self._possible_types
)
except IndexError:
logging.warning("Can not initialize randomizer")
return False
return True
@property
def unit_count(self) -> int:
if not self._unit_counter:
self.reset_unit_counter()
return self._unit_counter or 1
def use_unit(self) -> None:
if self._unit_counter is None:
self.reset_unit_counter()
if self._unit_counter and self._unit_counter > 0:
self._unit_counter -= 1
else:
raise IndexError
@property
def unit_range(self) -> list[int]:
if len(self.units) > 1:
return list(range(self.units[0], self.units[1] + 1))
return self.units
class GroundObjectTemplate(ABC):
def __init__(
self, name: str, template_type: str = "", description: str = ""
) -> None:
self.name = name
self.template_type = template_type
self.description = description
self.groups: list[GroupTemplate] = []
@classmethod
def from_dict(cls, d_object: dict[str, Any]) -> GroundObjectTemplate:
template = cls(
d_object["name"], d_object["template_type"], d_object["description"]
)
for d_group in d_object["groups"]:
template.groups.append(GroupTemplate.from_dict(d_group))
return template
def generate(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
randomization: bool = True,
) -> TheaterGroundObject:
# Create the ground_object based on the type
ground_object = self._create_ground_object(name, position, control_point)
# Generate all groups using the randomization if it defined
for g_id, group in enumerate(self.groups):
tgo_group = GroundGroup.from_template(
game.next_group_id(),
group,
ground_object,
randomization,
)
# Set Group Name
tgo_group.name = f"{self.name} {g_id}"
# Assign UniqueID, name and align relative to ground_object
for u_id, unit in enumerate(tgo_group.units):
unit.id = game.next_unit_id()
unit.name = f"{self.name} {g_id}-{u_id}"
unit.position = PointWithHeading.from_point(
Point(
ground_object.position.x + unit.position.x,
ground_object.position.y + unit.position.y,
),
# Align heading to GroundObject defined by the campaign designer
unit.position.heading + ground_object.heading,
)
if (
isinstance(self, AirDefenceTemplate)
and unit.unit_type
and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR
):
# Head Radars towards the center of the conflict
unit.position.heading = (
game.theater.heading_to_conflict_from(unit.position)
or unit.position.heading
)
# Rotate unit around the center to align the orientation of the group
unit.position.rotate(ground_object.position, ground_object.heading)
ground_object.groups.append(tgo_group)
return ground_object
@abstractmethod
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
raise NotImplementedError
@property
def randomizable(self) -> bool:
# Returns True if any group of the template has a randomizer
return any(group_template.randomizer for group_template in self.groups)
def estimated_price_for(self, go: TheaterGroundObject) -> float:
# Price can only be estimated because of randomization
template_price = 0
for g_id, group in enumerate(self.groups):
tgo_group = GroundGroup.from_template(g_id, group, go)
for unit in tgo_group.units:
if unit.type in vehicle_map:
dcs_type = vehicle_map[unit.type]
try:
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
except StopIteration:
continue
template_price = template_price + unit_type.price
return template_price
@property
def size(self) -> int:
return sum([len(group.units) for group in self.groups])
@property
def min_size(self) -> int:
return self._size_for_randomized(True)
@property
def max_size(self) -> int:
return self._size_for_randomized(False)
def _size_for_randomized(self, min_size: bool) -> int:
size = 0
for group in self.groups:
for unit_id, unit in enumerate(group.units):
if group.randomizer and unit_id in group.randomizer.units:
if isinstance(group.randomizer.count, int):
size = size + group.randomizer.count
else:
size = size + group.randomizer.count[0 if min_size else 1]
else:
size = size + 1
return size
@property
def required_units(self) -> list[str]:
"""returns all required unit types by theyre dcs type id"""
# todo take care for randomizer
unit_types = []
for group in self.groups:
# this completly excludes randomized groups
if not group.optional and not group.randomizer:
for unit in group.units:
if unit.type not in unit_types:
unit_types.append(unit.type)
return unit_types
class AirDefenceTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> IadsGroundObject:
if self.template_type == "EWR":
return EwrGroundObject(name, position, position.heading, control_point)
elif self.template_type in ["Long", "Medium", "Short", "AAA"]:
return SamGroundObject(name, position, position.heading, control_point)
raise RuntimeError(
f" No Template Definition for AirDefence with subcategory {self.template_type}"
)
class BuildingTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> BuildingGroundObject:
return BuildingGroundObject(
name,
self.template_type,
position,
Heading.from_degrees(0),
control_point,
self.template_type == "fob",
)
class NavalTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
if self.template_type == "ship":
return ShipGroundObject(name, position, control_point)
elif self.template_type == "carrier":
return CarrierGroundObject(name, control_point)
elif self.template_type == "lha":
return LhaGroundObject(name, control_point)
raise NotImplementedError
class CoastalTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return CoastalSiteGroundObject(name, position, control_point, position.heading)
class ArmorTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return VehicleGroupGroundObject(name, position, position.heading, control_point)
class MissileTemplate(GroundObjectTemplate):
def _create_ground_object(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
) -> TheaterGroundObject:
return MissileSiteGroundObject(name, position, position.heading, control_point)
TEMPLATE_TYPES = {
TemplateCategory.AirDefence: AirDefenceTemplate,
TemplateCategory.Building: BuildingTemplate,
TemplateCategory.Naval: NavalTemplate,
TemplateCategory.Armor: ArmorTemplate,
TemplateCategory.Missile: MissileTemplate,
TemplateCategory.Coastal: CoastalTemplate,
}
class GroundObjectTemplates:
# list of templates per category. e.g. AA or similar
_templates: dict[TemplateCategory, list[GroundObjectTemplate]]
def __init__(self) -> None:
self._templates = {}
@property
def templates(self) -> Iterator[tuple[TemplateCategory, GroundObjectTemplate]]:
for category, templates in self._templates.items():
for template in templates:
yield category, template
@classmethod
def from_json(cls, template_file: str) -> GroundObjectTemplates:
# Rebuild the TemplatesObject from the json dict
obj = GroundObjectTemplates()
with open(template_file, "r") as f:
json_templates: dict[str, list[dict[str, Any]]] = json.load(f)
for category, templates in json_templates.items():
for d_template in templates:
template = TEMPLATE_TYPES[TemplateCategory(category)].from_dict(
d_template
)
obj.add_template(TemplateCategory(category), template)
return obj
def to_json(self) -> dict[str, Any]:
return {
category.value: templates for category, templates in self._templates.items()
}
@property
def all(self) -> Iterator[GroundObjectTemplate]:
for templates in self._templates.values():
yield from templates
def by_name(self, template_name: str) -> Optional[GroundObjectTemplate]:
for template in self.all:
if template.name == template_name:
return template
return None
def by_category_and_name(
self, category: TemplateCategory, template_name: str
) -> Optional[GroundObjectTemplate]:
if category in self._templates:
for template in self._templates[category]:
if template.name == template_name:
return template
return None
def add_template(
self, category: TemplateCategory, template: GroundObjectTemplate
) -> None:
if category not in self._templates:
self._templates[category] = [template]
else:
self._templates[category].append(template)
def for_category(
self, category: TemplateCategory, sub_category: Optional[str] = None
) -> Iterator[GroundObjectTemplate]:
if category not in self._templates:
return None
for template in self._templates[category]:
if not sub_category or template.template_type == sub_category:
yield template