Decoupling and generalization of templates

Improvement for factions and templates which will allow decoupling of the templates from the actual units
- Implement UnitGroup class which matches unit_types and possible templates as the needed abstraction layer for decoupling.
- Refactor UnitType, Add ShipUnitType and all ships we currently use
- Remove serialized template.json and migrated to multiple yaml templates (one for each template) and multiple .miz
- Reorganized a lot of templates and started with generalization of many types (AAA, Flak, SHORAD, Navy)
- Fixed a lot of bugs from the previous reworks (group name generation, strike targets...)
- Reorganized the faction file completly. removed redundant lists, added presets for complex groups / families of units like sams
- Reworked the building template handling. Some templates are unused like "village"
- Reworked how groups from templates can be merged again for the dcs group creation (e.g. the skynet plugin requires them to be in the same group)
- Allow to define alternative tasks
This commit is contained in:
RndName 2022-01-29 00:42:58 +01:00
parent daf4704fe7
commit 60c8c80480
27 changed files with 1481 additions and 958 deletions

View File

@ -1,15 +1,15 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.utils import Distance, feet, nautical_miles from game.utils import Distance, feet, nautical_miles
@dataclass @dataclass
class GroundUnitProcurementRatios: class GroundUnitProcurementRatios:
ratios: dict[GroundUnitClass, float] ratios: dict[UnitClass, float]
def for_unit_class(self, unit_class: GroundUnitClass) -> float: def for_unit_class(self, unit_class: UnitClass) -> float:
try: try:
return self.ratios[unit_class] / sum(self.ratios.values()) return self.ratios[unit_class] / sum(self.ratios.values())
except KeyError: except KeyError:
@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(60), sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
GroundUnitClass.Tank: 3, UnitClass.Tank: 3,
GroundUnitClass.Atgm: 2, UnitClass.Atgm: 2,
GroundUnitClass.Apc: 2, UnitClass.Apc: 2,
GroundUnitClass.Ifv: 3, UnitClass.Ifv: 3,
GroundUnitClass.Artillery: 1, UnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2, UnitClass.SHORAD: 2,
GroundUnitClass.Recon: 1, UnitClass.Recon: 1,
} }
), ),
) )
@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(40), sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
GroundUnitClass.Tank: 4, UnitClass.Tank: 4,
GroundUnitClass.Atgm: 2, UnitClass.Atgm: 2,
GroundUnitClass.Apc: 3, UnitClass.Apc: 3,
GroundUnitClass.Ifv: 2, UnitClass.Ifv: 2,
GroundUnitClass.Artillery: 1, UnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2, UnitClass.SHORAD: 2,
GroundUnitClass.Recon: 1, UnitClass.Recon: 1,
} }
), ),
) )
@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(10), sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios( ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{ {
GroundUnitClass.Tank: 3, UnitClass.Tank: 3,
GroundUnitClass.Atgm: 3, UnitClass.Atgm: 3,
GroundUnitClass.Apc: 3, UnitClass.Apc: 3,
GroundUnitClass.Artillery: 1, UnitClass.Artillery: 1,
GroundUnitClass.Shorads: 3, UnitClass.SHORAD: 3,
GroundUnitClass.Recon: 1, UnitClass.Recon: 1,
} }
), ),
) )

View File

@ -1,17 +0,0 @@
from __future__ import annotations
from enum import unique, Enum
@unique
class GroundUnitClass(Enum):
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
Shorads = "SHORADS"
Manpads = "MANPADS"

63
game/data/groups.py Normal file
View File

@ -0,0 +1,63 @@
from enum import Enum
class GroupRole(Enum):
Unknow = "Unknown"
AntiAir = "AntiAir"
Building = "Building"
Naval = "Naval"
GroundForce = "GroundForce"
Defenses = "Defenses"
Air = "Air"
class GroupTask(Enum):
EWR = "EarlyWarningRadar"
AAA = "AAA"
SHORAD = "SHORAD"
MERAD = "MERAD"
LORAD = "LORAD"
AircraftCarrier = "AircraftCarrier"
HelicopterCarrier = "HelicopterCarrier"
Navy = "Navy"
BaseDefense = "BaseDefense" # Ground
FrontLine = "FrontLine"
Air = "Air"
Missile = "Missile"
Coastal = "Coastal"
Factory = "Factory"
Ammo = "Ammo"
Oil = "Oil"
FOB = "FOB"
StrikeTarget = "StrikeTarget"
Comms = "Comms"
Power = "Power"
ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = {
GroupRole.Unknow: [], # No Tasking
GroupRole.AntiAir: [
GroupTask.EWR,
GroupTask.AAA,
GroupTask.SHORAD,
GroupTask.MERAD,
GroupTask.LORAD,
],
GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine],
GroupRole.Naval: [
GroupTask.AircraftCarrier,
GroupTask.HelicopterCarrier,
GroupTask.Navy,
],
GroupRole.Building: [
GroupTask.Factory,
GroupTask.Ammo,
GroupTask.Oil,
GroupTask.FOB,
GroupTask.StrikeTarget,
GroupTask.Comms,
GroupTask.Power,
],
GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal],
GroupRole.Air: [GroupTask.Air],
}

40
game/data/units.py Normal file
View File

@ -0,0 +1,40 @@
from __future__ import annotations
from enum import unique, Enum
from game.data.groups import GroupRole, GroupTask
@unique
class UnitClass(Enum):
Unknown = "Unknown"
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
AAA = "AAA"
SHORAD = "SHORAD"
Manpad = "Manpad"
SR = "SearchRadar"
STR = "SearchTrackRadar"
LowAltSR = "LowAltSearchRadar"
TR = "TrackRadar"
LN = "Launcher"
EWR = "EarlyWarningRadar"
TELAR = "TELAR"
Missile = "Missile"
AircraftCarrier = "AircraftCarrier"
HelicopterCarrier = "HelicopterCarrier"
Destroyer = "Destroyer"
Cruiser = "Cruiser"
Submarine = "Submarine"
LandingShip = "LandingShip"
Boat = "Boat"
Plane = "Plane"
def to_dict(self) -> str:
return self.value

View File

@ -12,6 +12,7 @@ from dcs.helicopters import helicopter_map
from dcs.planes import plane_map from dcs.planes import plane_map
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.data.units import UnitClass
from game.dcs.unitproperty import UnitProperty from game.dcs.unitproperty import UnitProperty
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
from game.radio.channels import ( from game.radio.channels import (
@ -180,19 +181,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator] channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer] channel_namer: Type[ChannelNamer]
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@property @property
def flyable(self) -> bool: def flyable(self) -> bool:
return self.dcs_unit_type.flyable return self.dcs_unit_type.flyable
@ -309,35 +297,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
state.update(updated.__dict__) state.update(updated.__dict__)
self.__dict__.update(state) self.__dict__.update(state)
@classmethod
def register(cls, aircraft_type: AircraftType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod @classmethod
def named(cls, name: str) -> AircraftType: def named(cls, name: str) -> AircraftType:
if not cls._loaded: if not cls._loaded:
cls._load_all() cls._load_all()
return cls._by_name[name] unit = cls._by_name[name]
assert isinstance(unit, AircraftType)
return unit
@classmethod @classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]: def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded: if not cls._loaded:
cls._load_all() cls._load_all()
yield from cls._by_unit_type[dcs_unit_type] for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, AircraftType)
yield unit
@staticmethod @staticmethod
def _each_unit_type() -> Iterator[Type[FlyingType]]: def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_map.values() yield from helicopter_map.values()
yield from plane_map.values() yield from plane_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod @classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]: def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml" data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
@ -417,4 +397,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer, channel_namer=radio_config.channel_namer,
kneeboard_units=units, kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False), utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.Plane,
) )

View File

@ -1,65 +1,42 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator from typing import Type, Optional, Iterator
import yaml import yaml
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.dcs.unittype import UnitType from game.dcs.unittype import UnitType
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundUnitType(UnitType[Type[VehicleType]]): class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int spawn_weight: int
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[Type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, aircraft_type: GroundUnitType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod @classmethod
def named(cls, name: str) -> GroundUnitType: def named(cls, name: str) -> GroundUnitType:
if not cls._loaded: if not cls._loaded:
cls._load_all() cls._load_all()
return cls._by_name[name] unit = cls._by_name[name]
assert isinstance(unit, GroundUnitType)
return unit
@classmethod @classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]: def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded: if not cls._loaded:
cls._load_all() cls._load_all()
yield from cls._by_unit_type[dcs_unit_type] for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, GroundUnitType)
yield unit
@staticmethod @staticmethod
def _each_unit_type() -> Iterator[Type[VehicleType]]: def _each_unit_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values() yield from vehicle_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod @classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]: def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml" data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
@ -78,9 +55,8 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
introduction = "No data." introduction = "No data."
class_name = data.get("class") class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None # TODO Exception handling for missing classes
if class_name is not None: unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
unit_class = GroundUnitClass(class_name)
for variant in data.get("variants", [vehicle.id]): for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType( yield GroundUnitType(

74
game/dcs/shipunittype.py Normal file
View File

@ -0,0 +1,74 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
import yaml
from dcs.ships import ship_map
from dcs.unittype import VehicleType, ShipType
from dcs.vehicles import vehicle_map
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class ShipUnitType(UnitType[Type[ShipType]]):
@classmethod
def named(cls, name: str) -> ShipUnitType:
if not cls._loaded:
cls._load_all()
unit = cls._by_name[name]
assert isinstance(unit, ShipUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[ShipType]) -> Iterator[ShipUnitType]:
if not cls._loaded:
cls._load_all()
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, ShipUnitType)
yield unit
@staticmethod
def _each_unit_type() -> Iterator[Type[ShipType]]:
yield from ship_map.values()
@classmethod
def _each_variant_of(cls, ship: Type[ShipType]) -> Iterator[ShipUnitType]:
data_path = Path("resources/units/ships") / f"{ship.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {ship.id}; it will not be available")
return
with data_path.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
class_name = data.get("class")
unit_class = UnitClass(class_name)
for variant in data.get("variants", [ship.id]):
yield ShipUnitType(
dcs_unit_type=ship,
unit_class=unit_class,
name=variant,
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
role=data.get("role", "No data."),
price=data.get("price", 1),
)

157
game/dcs/unitgroup.py Normal file
View File

@ -0,0 +1,157 @@
from __future__ import annotations
import copy
import itertools
import random
from dataclasses import dataclass, field
from pathlib import Path
from typing import ClassVar, TYPE_CHECKING, Any, Iterator
import yaml
from game.data.groups import GroupRole, GroupTask
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from game.point_with_heading import PointWithHeading
from gen.templates import GroundObjectTemplate
if TYPE_CHECKING:
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject, ControlPoint
@dataclass
class UnitGroup:
name: str
ground_units: list[GroundUnitType]
ship_units: list[ShipUnitType]
statics: list[str]
role: GroupRole
tasks: list[GroupTask] = field(default_factory=list)
template_names: list[str] = field(default_factory=list)
_by_name: ClassVar[dict[str, UnitGroup]] = {}
_by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {}
_loaded: bool = False
_templates: list[GroundObjectTemplate] = field(default_factory=list)
def __str__(self) -> str:
return self.name
def update_from_unit_group(self, unit_group: UnitGroup) -> None:
# Update tasking and templates
self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks])
self._templates.extend(
[
template
for template in unit_group.templates
if template not in self.templates
]
)
@property
def templates(self) -> list[GroundObjectTemplate]:
return self._templates
def add_template(self, faction_template: GroundObjectTemplate) -> None:
template = copy.deepcopy(faction_template)
updated_groups = []
for group in template.groups:
unit_types = list(
itertools.chain(
[u.dcs_id for u in self.ground_units if group.can_use_unit(u)],
[s.dcs_id for s in self.ship_units if group.can_use_unit(s)],
[s for s in self.statics if group.can_use_unit_type(s)],
)
)
if unit_types:
group.set_possible_types(unit_types)
updated_groups.append(group)
template.groups = updated_groups
self._templates.append(template)
def load_templates(self, faction: Faction) -> None:
self._templates = []
if self.template_names:
# Preferred templates
for template_name in self.template_names:
template = faction.templates.by_name(template_name)
if template:
self.add_template(template)
if not self._templates:
# Find all matching templates if no preferred set or available
for template in list(
faction.templates.for_role_and_tasks(self.role, self.tasks)
):
if any(self.has_unit_type(unit) for unit in template.units):
self.add_template(template)
def set_templates(self, templates: list[GroundObjectTemplate]) -> None:
self._templates = templates
def has_unit_type(self, unit_type: UnitType[Any]) -> bool:
return unit_type in self.ground_units or unit_type in self.ship_units
@property
def unit_types(self) -> Iterator[str]:
for unit in self.ground_units:
yield unit.dcs_id
for ship in self.ship_units:
yield ship.dcs_id
for static in self.statics:
yield static
@classmethod
def named(cls, name: str) -> UnitGroup:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
def generate(
self,
name: str,
position: PointWithHeading,
control_point: ControlPoint,
game: Game,
) -> TheaterGroundObject:
template = random.choice(self.templates)
return template.generate(name, position, control_point, game)
@classmethod
def _load_all(cls) -> None:
for file in Path("resources/units/unit_groups").glob("*.yaml"):
if not file.is_file():
continue
with file.open(encoding="utf-8") as data_file:
data = yaml.safe_load(data_file)
group_role = GroupRole(data.get("role"))
group_tasks = [GroupTask(n) for n in data.get("tasks", [])]
ground_units = [
GroundUnitType.named(n) for n in data.get("ground_units", [])
]
ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])]
unit_group = UnitGroup(
name=data.get("name"),
ground_units=ground_units,
ship_units=ship_units,
statics=data.get("statics", []),
role=group_role,
tasks=group_tasks,
template_names=data.get("templates", []),
)
cls._by_name[unit_group.name] = unit_group
if group_role in cls._by_role:
cls._by_role[group_role].append(unit_group)
else:
cls._by_role[group_role] = [unit_group]
cls._loaded = True

View File

@ -1,14 +1,20 @@
from __future__ import annotations
from abc import ABC
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from typing import TypeVar, Generic, Type from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
from dcs.unittype import UnitType as DcsUnitType from dcs.unittype import UnitType as DcsUnitType
from game.data.units import UnitClass
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType]) DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True) @dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]): class UnitType(ABC, Generic[DcsUnitTypeT]):
dcs_unit_type: DcsUnitTypeT dcs_unit_type: DcsUnitTypeT
name: str name: str
description: str description: str
@ -17,10 +23,47 @@ class UnitType(Generic[DcsUnitTypeT]):
manufacturer: str manufacturer: str
role: str role: str
price: int price: int
unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, unit_type: UnitType[Any]) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> UnitType[Any]:
raise NotImplementedError
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@staticmethod
def _each_unit_type() -> Iterator[DcsUnitTypeT]:
raise NotImplementedError
@classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@cached_property @cached_property
def eplrs_capable(self) -> bool: def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False) return getattr(self.dcs_unit_type, "eplrs", False)

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import copy import copy
import itertools import itertools
import logging import logging
import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs import dcs
from dcs.countries import country_dict from dcs.countries import country_dict
from dcs.unittype import ShipType, UnitType from dcs.unittype import ShipType
from game.data.building_data import ( from game.data.building_data import (
WW2_ALLIES_BUILDINGS, WW2_ALLIES_BUILDINGS,
@ -22,10 +23,19 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE, COLDWAR_DOCTRINE,
WWII_DOCTRINE, WWII_DOCTRINE,
) )
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.data.groups import GroupRole, GroupTask
from game import db
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from gen.templates import GroundObjectTemplates, TemplateCategory from game.dcs.shipunittype import ShipUnitType
from game.dcs.unitgroup import UnitGroup
from game.dcs.unittype import UnitType
from gen.templates import (
GroundObjectTemplates,
GroundObjectTemplate,
GroupTemplate,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater.start_generator import ModSettings from game.theater.start_generator import ModSettings
@ -70,50 +80,26 @@ class Faction:
# Logistics units used # Logistics units used
logistics_units: List[GroundUnitType] = field(default_factory=list) logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction # Possible Air Defence units, Like EWRs
air_defenses: List[str] = field(default_factory=list) air_defense_units: List[GroundUnitType] = field(default_factory=list)
# Possible EWR generators for this faction. # A list of all supported sets of units
ewrs: List[GroundUnitType] = field(default_factory=list) preset_groups: list[UnitGroup] = field(default_factory=list)
# Possible Missile site generators for this faction # Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list) missiles: List[GroundUnitType] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
# Required mods or asset packs # Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
# possible helicopter carrier units
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
# Possible carrier names # Possible carrier names
carrier_names: List[str] = field(default_factory=list) carrier_names: List[str] = field(default_factory=list)
# Possible helicopter carrier names # Possible helicopter carrier names
helicopter_carrier_names: List[str] = field(default_factory=list) helicopter_carrier_names: List[str] = field(default_factory=list)
# Navy group generators # Available Naval Units
navy_generators: List[str] = field(default_factory=list) naval_units: List[ShipUnitType] = field(default_factory=list)
# Available destroyers
destroyers: List[Type[ShipType]] = field(default_factory=list)
# Available cruisers
cruisers: List[Type[ShipType]] = field(default_factory=list)
# How many navy group should we try to generate per CP on startup for this faction
navy_group_count: int = field(default=1)
# How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Whether this faction has JTAC access # Whether this faction has JTAC access
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
@ -124,7 +110,7 @@ class Faction:
# doctrine # doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE) doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# List of available buildings for this faction # List of available building templates for this faction
building_set: List[str] = field(default_factory=list) building_set: List[str] = field(default_factory=list)
# List of default livery overrides # List of default livery overrides
@ -142,64 +128,183 @@ class Faction:
# All possible templates which can be generated by the faction # All possible templates which can be generated by the faction
templates: GroundObjectTemplates = field(default=GroundObjectTemplates()) templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
# All available unit_groups
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
# Save all accessible units for performance increase
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
def __getitem__(self, item: str) -> Any: def __getitem__(self, item: str) -> Any:
return getattr(self, item) return getattr(self, item)
def has_access_to_unit_type(self, unit_type: str) -> bool: @property
# Supports all GroundUnit lists and AirDefenses def accessible_units(self) -> Iterator[UnitType[Any]]:
for unit in self.ground_units: yield from self._accessible_units
if unit_type == unit.dcs_id:
return True
return unit_type in self.air_defenses
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool: @property
for vehicle in itertools.chain(self.frontline_units, self.artillery_units): def air_defenses(self) -> list[str]:
if vehicle.unit_class is unit_class: """Returns the Air Defense types"""
return True air_defenses = [a.name for a in self.air_defense_units]
air_defenses.extend(
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
)
return sorted(air_defenses)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# GroundUnits
if any(unit_type == u.dcs_id for u in self.accessible_units):
return True
# Statics
if db.static_type_from_name(unit_type) is not None:
# TODO Improve the statics checking
return True
return False return False
def load_templates(self, all_templates: GroundObjectTemplates) -> None: def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
# This loads all faction possible sam templates and the default ones return any(unit.unit_class is unit_class for unit in self.accessible_units)
# For legacy reasons this allows to check for template names. This can be
# improved in the future to have more control about the possible Templates. def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
# For example it can be possible to define the unit_types and check if all self._accessible_units = []
# requirements for the template are fulfilled. all_units: Iterator[UnitType[Any]] = itertools.chain(
for category, template in all_templates.templates: self.ground_units,
if ( self.infantry_units,
( self.air_defense_units,
category == TemplateCategory.AirDefence self.naval_units,
and ( self.missiles,
# Check if faction has the template name or ALL required (
# unit_types in the list air_defenses. For legacy reasons this ground_unit
# allows both and also the EWR template for preset_group in self.preset_groups
template.name in self.air_defenses for ground_unit in preset_group.ground_units
or all( ),
self.has_access_to_unit_type(required_unit) (
for required_unit in template.required_units ship_unit
) for preset_group in self.preset_groups
or template.template_type == "EWR" for ship_unit in preset_group.ship_units
) ),
) )
or template.name in self.navy_generators for unit in all_units:
or template.name in self.missiles if unit not in self._accessible_units:
or template.name in self.coastal_defenses self._accessible_units.append(unit)
or (
template.template_type def initialize(
in self.building_set + ["fob", "ammo", "factory"] self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
) ) -> None:
or (template.template_type == "carrier" and self.aircraft_carrier) # Apply the mod settings
or (template.template_type == "lha" and self.helicopter_carrier) self._apply_mod_settings(mod_settings)
or category == TemplateCategory.Armor # Load all accessible units and store them for performant later usage
): self._load_accessible_units(all_templates)
# Make a deep copy of a template and add it to the template_list. # Load all faction compatible templates
# This is required to have faction independent templates. Otherwise self._load_templates(all_templates)
# the reference would be the same and changes would affect all. # Load Unit Groups
faction_template = copy.deepcopy(template) self._load_unit_groups()
# Initialize all randomizers
for group_template in faction_template.groups: def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
if group_template.randomizer: if not unit_group.templates:
group_template.randomizer.init_randomization_for_faction(self) unit_group.load_templates(self)
self.templates.add_template(category, faction_template) if not unit_group.templates:
# Empty templates will throw an error on generation
logging.error(
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
)
return
if unit_group.role in self.unit_groups:
for group in self.unit_groups[unit_group.role]:
if merge and all(task in group.tasks for task in unit_group.tasks):
# Update existing group if same tasking
group.update_from_unit_group(unit_group)
return
# Add new Unit_group
self.unit_groups[unit_group.role].append(unit_group)
else:
self.unit_groups[unit_group.role] = [unit_group]
def _load_unit_groups(self) -> None:
# This function will create all the UnitGroups for the faction
# It will create a unit group for each global Template, Building or
# Legacy supported templates (not yet migrated from the generators).
# For every preset_group there will be a separate UnitGroup so no mixed
# UnitGroups will be generated for them. Special groups like complex SAM Systems
self.unit_groups = {}
# Generate UnitGroups for all global templates
for role, template in self.templates.templates:
if template.generic or role == GroupRole.Building:
# Build groups for global templates and buildings
self._add_group_for_template(role, template)
# Add preset groups
for preset_group in self.preset_groups:
# Add as separate group, do not merge with generic groups!
self._add_unit_group(preset_group, False)
def _add_group_for_template(
self, role: GroupRole, template: GroundObjectTemplate
) -> None:
unit_group = UnitGroup(
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
[u for u in template.units if isinstance(u, GroundUnitType)],
[u for u in template.units if isinstance(u, ShipUnitType)],
list(template.statics),
role,
)
unit_group.tasks = template.tasks
unit_group.set_templates([template])
self._add_unit_group(unit_group)
def initialize_group_template(
self, group: GroupTemplate, faction_sensitive: bool = True
) -> bool:
# Sensitive defines if the initialization should check if the unit is available
# to this faction or not. It is disabled for migration only atm.
unit_types = [
t
for t in group.unit_types
if not faction_sensitive or self.has_access_to_unit_type(t)
]
alternative_types = []
for accessible_unit in self.accessible_units:
if accessible_unit.unit_class in group.unit_classes:
unit_types.append(accessible_unit.dcs_id)
if accessible_unit.unit_class in group.alternative_classes:
alternative_types.append(accessible_unit.dcs_id)
if not unit_types and not alternative_types and not group.optional:
raise StopIteration
types = unit_types or alternative_types
group.set_possible_types(types)
return len(types) > 0
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
self.templates = GroundObjectTemplates()
# This loads all templates which are usable by the faction
for role, template in all_templates.templates:
# Make a deep copy of a template and add it to the template_list.
# This is required to have faction independent templates. Otherwise
# the reference would be the same and changes would affect all.
faction_template = copy.deepcopy(template)
try:
faction_template.groups[:] = [
group_template
for group_template in faction_template.groups
if self.initialize_group_template(group_template)
]
if (
role == GroupRole.Building
and GroupTask.StrikeTarget in template.tasks
and faction_template.category not in self.building_set
):
# Special handling for strike targets. Skip if not supported by faction
continue
if faction_template.groups:
self.templates.add_template(role, faction_template)
continue
except StopIteration:
pass
logging.info(f"{self.name} can not use template {template.name}")
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@ -240,34 +345,30 @@ class Faction:
faction.logistics_units = [ faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", []) GroundUnitType.named(n) for n in json.get("logistics_units", [])
] ]
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])] faction.air_defense_units = [
GroundUnitType.named(n) for n in json.get("air_defense_units", [])
]
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
faction.air_defenses = json.get("air_defenses", []) faction.preset_groups = [
# Compatibility for older factions. All air defenses now belong to a UnitGroup.named(n) for n in json.get("preset_groups", [])
# single group and the generator decides what belongs where. ]
faction.air_defenses.extend(json.get("sams", []))
faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", []) faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", [])) faction.naval_units = [
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", [])) ShipUnitType.named(n) for n in json.get("naval_units", [])
faction.destroyers = load_all_ships(json.get("destroyers", [])) ]
faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False) faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None) jtac_name = json.get("jtac_unit", None)
if jtac_name is not None: if jtac_name is not None:
faction.jtac_unit = AircraftType.named(jtac_name) faction.jtac_unit = AircraftType.named(jtac_name)
else: else:
faction.jtac_unit = None faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine # Load doctrine
doctrine = json.get("doctrine", "modern") doctrine = json.get("doctrine", "modern")
@ -304,6 +405,7 @@ class Faction:
# Templates # Templates
faction.templates = GroundObjectTemplates() faction.templates = GroundObjectTemplates()
return faction return faction
@property @property
@ -312,14 +414,49 @@ class Faction:
yield from self.frontline_units yield from self.frontline_units
yield from self.logistics_units yield from self.logistics_units
def infantry_with_class( def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
for unit in self.infantry_units: for unit in self.infantry_units:
if unit.unit_class is unit_class: if unit.unit_class is unit_class:
yield unit yield unit
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction: def groups_for_role_and_task(
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
) -> list[UnitGroup]:
if group_role not in self.unit_groups:
return []
groups = []
for unit_group in self.unit_groups[group_role]:
if not group_task or group_task in unit_group.tasks:
groups.append(unit_group)
return groups
def groups_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> list[UnitGroup]:
groups = []
for task in tasks:
for group in self.groups_for_role_and_task(group_role, task):
if group not in groups:
groups.append(group)
return groups
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_task(
self, group_role: GroupRole, group_task: GroupTask
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_task(group_role, group_task)
return random.choice(unit_groups) if unit_groups else None
def random_group_for_role_and_tasks(
self, group_role: GroupRole, tasks: list[GroupTask]
) -> Optional[UnitGroup]:
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
return random.choice(unit_groups) if unit_groups else None
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
# aircraft # aircraft
if not mod_settings.a4_skyhawk: if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C") self.remove_aircraft("A-4E-C")
@ -379,24 +516,23 @@ class Faction:
self.remove_vehicle("KORNET") self.remove_vehicle("KORNET")
# high digit sams # high digit sams
if not mod_settings.high_digit_sams: if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA-10B/S-300PS Battery") self.remove_presets("SA-10B/S-300PS")
self.remove_air_defenses("SA-12/S-300V Battery") self.remove_presets("SA-12/S-300V")
self.remove_air_defenses("SA-20/S-300PMU-1 Battery") self.remove_presets("SA-20/S-300PMU-1")
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery") self.remove_presets("SA-20B/S-300PMU-2")
self.remove_air_defenses("SA-23/S-300VM Battery") self.remove_presets("SA-23/S-300VM")
self.remove_air_defenses("SA-17 Grizzly Battery") self.remove_presets("SA-17")
self.remove_air_defenses("KS-19 AAA Site") self.remove_presets("KS-19")
return self
def remove_aircraft(self, name: str) -> None: def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts: for i in self.aircrafts:
if i.dcs_unit_type.id == name: if i.dcs_unit_type.id == name:
self.aircrafts.remove(i) self.aircrafts.remove(i)
def remove_air_defenses(self, name: str) -> None: def remove_presets(self, name: str) -> None:
for i in self.air_defenses: for pg in self.preset_groups:
if i == name: if pg.name == name:
self.air_defenses.remove(i) self.preset_groups.remove(pg)
def remove_vehicle(self, name: str) -> None: def remove_vehicle(self, name: str) -> None:
for i in self.frontline_units: for i in self.frontline_units:

View File

@ -15,7 +15,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
target = self.package.target target = self.package.target
if isinstance(target, TheaterGroundObject): if isinstance(target, TheaterGroundObject):
for group in target.groups: for group in target.groups:
group_names.append(group.name) group_names.append(group.group_name)
elif isinstance(target, MultiGroupTransport): elif isinstance(target, MultiGroupTransport):
group_names.append(target.name) group_names.append(target.name)
elif isinstance(target, NavalControlPoint): elif isinstance(target, NavalControlPoint):

View File

@ -20,9 +20,11 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return return
for group in target.groups: for group in target.groups:
miz_group = self.mission.find_group(group.name) miz_group = self.mission.find_group(group.group_name)
if miz_group is None: if miz_group is None:
logging.error(f"Could not find group for DEAD mission {group.name}") logging.error(
f"Could not find group for DEAD mission {group.group_name}"
)
continue continue
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto) task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)

View File

@ -20,9 +20,11 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
return return
for group in target.groups: for group in target.groups:
miz_group = self.mission.find_group(group.name) miz_group = self.mission.find_group(group.group_name)
if miz_group is None: if miz_group is None:
logging.error(f"Could not find group for SEAD mission {group.name}") logging.error(
f"Could not find group for SEAD mission {group.group_name}"
)
continue continue
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided) task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided)

View File

@ -28,7 +28,7 @@ from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle, Skill from dcs.unit import Vehicle, Skill
from dcs.unitgroup import VehicleGroup from dcs.unitgroup import VehicleGroup
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
@ -221,7 +221,7 @@ class FlotGenerator:
if self.game.settings.manpads: if self.game.settings.manpads:
# 50% of armored units protected by manpad # 50% of armored units protected by manpad
if random.choice([True, False]): if random.choice([True, False]):
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads)) manpads = list(faction.infantry_with_class(UnitClass.Manpad))
if manpads: if manpads:
u = random.choices( u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads] manpads, weights=[m.spawn_weight for m in manpads]
@ -237,12 +237,10 @@ class FlotGenerator:
) )
return return
possible_infantry_units = set( possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry))
faction.infantry_with_class(GroundUnitClass.Infantry)
)
if self.game.settings.manpads: if self.game.settings.manpads:
possible_infantry_units |= set( possible_infantry_units |= set(
faction.infantry_with_class(GroundUnitClass.Manpads) faction.infantry_with_class(UnitClass.Manpad)
) )
if not possible_infantry_units: if not possible_infantry_units:
return return

View File

@ -118,7 +118,7 @@ class LuaGenerator:
faction = "BlueAA" if cp.captured else "RedAA" faction = "BlueAA" if cp.captured else "RedAA"
lua_data[faction][g.name] = { lua_data[faction][g.group_name] = {
"name": ground_object.name, "name": ground_object.name,
"range": threat_range.meters, "range": threat_range.meters,
"position": { "position": {

View File

@ -22,7 +22,6 @@ from typing import (
TypeVar, TypeVar,
List, List,
Any, Any,
Union,
) )
from dcs import Mission, Point, unitgroup from dcs import Mission, Point, unitgroup
@ -110,108 +109,93 @@ class GroundObjectGenerator:
logging.warning(f"Found empty group in {self.ground_object}") logging.warning(f"Found empty group in {self.ground_object}")
continue continue
group_name = group.group_name if unique_name else group.name group_name = group.group_name if unique_name else group.name
if group.static_group: moving_group: Optional[MovingGroup[Any]] = None
# Static Group for i, unit in enumerate(group.units):
for i, u in enumerate(group.units): if isinstance(unit, SceneryGroundUnit):
if isinstance(u, SceneryGroundUnit): # Special handling for scenery objects:
# Special handling for scenery objects: # Only create a trigger zone and no "real" dcs unit
# Only create a trigger zone and no "real" dcs unit self.add_trigger_zone_for_scenery(unit)
self.add_trigger_zone_for_scenery(u) continue
continue
# Only skip dead units after trigger zone for scenery created! # Only skip dead units after trigger zone for scenery created!
if not u.alive: if not unit.alive:
continue continue
unit_type = unit_type_from_name(u.type) unit_type = unit_type_from_name(unit.type)
if not unit_type: if not unit_type:
raise RuntimeError( raise RuntimeError(
f"Unit type {u.type} is not a valid dcs unit type" f"Unit type {unit.type} is not a valid dcs unit type"
)
sg = self.m.static_group(
country=self.country,
name=u.unit_name if unique_name else u.name,
_type=unit_type,
position=u.position,
heading=u.position.heading.degrees,
dead=not u.alive,
) )
self._register_ground_unit(u, sg.units[0])
else:
# Moving Group
moving_group: Optional[MovingGroup[Any]] = None
for i, unit in enumerate(group.units):
if not unit.alive:
continue
if unit.type in vehicle_map:
# Vehicle Group
unit_type = vehicle_map[unit.type]
elif unit.type in ship_map:
# Ship Group
unit_type = ship_map[group.units[0].type]
else:
raise RuntimeError(
f"Unit type {unit.type} is not a valid dcs unit type"
)
unit_name = unit.unit_name if unique_name else unit.name unit_name = unit.unit_name if unique_name else unit.name
if moving_group is None: if moving_group is None or group.static_group:
# First unit of the group will create the dcs group # First unit of the group will create the dcs group
if issubclass(unit_type, VehicleType): if issubclass(unit_type, VehicleType):
moving_group = self.m.vehicle_group( moving_group = self.m.vehicle_group(
self.country, self.country,
group_name, group_name,
unit_type, unit_type,
position=unit.position, position=unit.position,
heading=unit.position.heading.degrees, heading=unit.position.heading.degrees,
) )
moving_group.units[0].player_can_drive = True moving_group.units[0].player_can_drive = True
self.enable_eplrs(moving_group, unit_type) self.enable_eplrs(moving_group, unit_type)
if issubclass(unit_type, ShipType): elif issubclass(unit_type, ShipType):
moving_group = self.m.ship_group( moving_group = self.m.ship_group(
self.country, self.country,
group_name, group_name,
unit_type, unit_type,
position=unit.position, position=unit.position,
heading=unit.position.heading.degrees, heading=unit.position.heading.degrees,
) )
if moving_group: elif issubclass(unit_type, StaticType):
moving_group.units[0].name = unit_name static_group = self.m.static_group(
self.set_alarm_state(moving_group) country=self.country,
self._register_ground_unit(unit, moving_group.units[0]) name=unit_name,
else: _type=unit_type,
raise RuntimeError("DCS Group creation failed") position=unit.position,
heading=unit.position.heading.degrees,
dead=not unit.alive,
)
self._register_ground_unit(unit, static_group.units[0])
continue
if moving_group:
moving_group.units[0].name = unit_name
self.set_alarm_state(moving_group)
self._register_ground_unit(unit, moving_group.units[0])
else: else:
# Additional Units in the group raise RuntimeError("DCS Group creation failed")
dcs_unit: Optional[Unit] = None else:
if issubclass(unit_type, VehicleType): # Additional Units in the group
dcs_unit = Vehicle( dcs_unit: Optional[Unit] = None
self.m.next_unit_id(), if issubclass(unit_type, VehicleType):
unit_name, dcs_unit = Vehicle(
unit.type, self.m.next_unit_id(),
) unit_name,
dcs_unit.player_can_drive = True unit.type,
elif issubclass(unit_type, ShipType): )
dcs_unit = Ship( dcs_unit.player_can_drive = True
self.m.next_unit_id(), elif issubclass(unit_type, ShipType):
unit_name, dcs_unit = Ship(
unit_type, self.m.next_unit_id(),
) unit_name,
if dcs_unit: unit_type,
dcs_unit.position = unit.position )
dcs_unit.heading = unit.position.heading.degrees if dcs_unit:
moving_group.add_unit(dcs_unit) dcs_unit.position = unit.position
self._register_ground_unit(unit, dcs_unit) dcs_unit.heading = unit.position.heading.degrees
else: moving_group.add_unit(dcs_unit)
raise RuntimeError("DCS Unit creation failed") self._register_ground_unit(unit, dcs_unit)
else:
raise RuntimeError("DCS Unit creation failed")
@staticmethod @staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
if unit_type.eplrs: if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None: def set_alarm_state(self, group: MovingGroup[Any]) -> None:
if self.game.settings.perf_red_alert_state: if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2)) group.points[0].tasks.append(OptAlarmState(2))
else: else:
@ -287,7 +271,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
# TODO : Should be pre-planned ? # TODO : Should be pre-planned ?
# TODO : Add delay to task to spread fire task over mission duration ? # TODO : Add delay to task to spread fire task over mission duration ?
for group in self.ground_object.groups: for group in self.ground_object.groups:
vg = self.m.find_group(group.name) vg = self.m.find_group(group.group_name)
if vg is not None: if vg is not None:
targets = self.possible_missile_targets() targets = self.possible_missile_targets()
if targets: if targets:
@ -327,7 +311,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
""" """
site_range = 0 site_range = 0
for group in self.ground_object.groups: for group in self.ground_object.groups:
vg = self.m.find_group(group.name) vg = self.m.find_group(group.group_name)
if vg is not None: if vg is not None:
for u in vg.units: for u in vg.units:
if u.type in vehicle_map: if u.type in vehicle_map:
@ -383,7 +367,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
ship_group = self.m.ship_group( ship_group = self.m.ship_group(
self.country, self.country,
group.name, group.group_name if unique_name else group.name,
unit_type, unit_type,
position=group.units[0].position, position=group.units[0].position,
heading=group.units[0].position.heading.degrees, heading=group.units[0].position.heading.degrees,
@ -523,23 +507,20 @@ class CarrierGenerator(GenericCarrierGenerator):
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
# TODO: Assign these properly. # TODO: Assign these properly.
if self.control_point.name == "Carrier Strike Group 8": return random.choice(
return "TRU" [
else: "STE",
return random.choice( "CVN",
[ "CVH",
"STE", "CCV",
"CVN", "ACC",
"CVH", "ARC",
"CCV", "GER",
"ACC", "ABR",
"ARC", "LIN",
"GER", "TRU",
"ABR", ]
"LIN", )
"TRU",
]
)
class LhaGenerator(GenericCarrierGenerator): class LhaGenerator(GenericCarrierGenerator):

View File

@ -6,7 +6,7 @@ from dataclasses import dataclass
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game.config import RUNWAY_REPAIR_COST from game.config import RUNWAY_REPAIR_COST
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
@ -116,7 +116,7 @@ class ProcurementAi:
return budget return budget
def affordable_ground_unit_of_class( def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass self, budget: float, unit_class: UnitClass
) -> Optional[GroundUnitType]: ) -> Optional[GroundUnitType]:
faction_units = set(self.faction.frontline_units) | set( faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units self.faction.artillery_units
@ -154,10 +154,10 @@ class ProcurementAi:
return budget return budget
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass: def most_needed_unit_class(self, cp: ControlPoint) -> UnitClass:
worst_balanced: Optional[GroundUnitClass] = None worst_balanced: Optional[UnitClass] = None
worst_fulfillment = math.inf worst_fulfillment = math.inf
for unit_class in GroundUnitClass: for unit_class in UnitClass:
if not self.faction.has_access_to_unit_class(unit_class): if not self.faction.has_access_to_unit_class(unit_class):
continue continue
@ -176,7 +176,7 @@ class ProcurementAi:
worst_fulfillment = fulfillment worst_fulfillment = fulfillment
worst_balanced = unit_class worst_balanced = unit_class
if worst_balanced is None: if worst_balanced is None:
return GroundUnitClass.Tank return UnitClass.Tank
return worst_balanced return worst_balanced
@staticmethod @staticmethod
@ -300,7 +300,7 @@ class ProcurementAi:
return understaffed return understaffed
def cost_ratio_of_ground_unit( def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass self, control_point: ControlPoint, unit_class: UnitClass
) -> float: ) -> float:
allocations = control_point.allocated_ground_units( allocations = control_point.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers self.game.coalition_for(self.is_player).transfers

View File

@ -48,6 +48,7 @@ from .theatergroundobject import (
GroundUnit, GroundUnit,
) )
from ..ato.starttype import StartType from ..ato.starttype import StartType
from ..data.units import UnitClass
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles from ..utils import nautical_miles
@ -521,20 +522,13 @@ class ControlPoint(MissionTarget, ABC):
ControlPointType.LHA_GROUP, ControlPointType.LHA_GROUP,
]: ]:
for g in self.ground_objects: for g in self.ground_objects:
if isinstance(g, CarrierGroundObject): for group in g.groups:
for group in g.groups: for u in group.units:
for u in group.units: if u.unit_type and u.unit_type.unit_class in [
if unit_type_from_name(u.type) in [ UnitClass.AircraftCarrier,
Forrestal, UnitClass.HelicopterCarrier,
Stennis, ]:
KUZNECOW, return group.group_name
]:
return group.name
elif isinstance(g, LhaGroundObject):
for group in g.groups:
for u in group.units:
if unit_type_from_name(u.type) in [LHA_Tarawa]:
return group.name
return None return None
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
@ -668,6 +662,7 @@ class ControlPoint(MissionTarget, ABC):
self._retreat_squadron(game, squadron) self._retreat_squadron(game, squadron)
def depopulate_uncapturable_tgos(self) -> None: def depopulate_uncapturable_tgos(self) -> None:
# TODO Rework this.
for tgo in self.connected_objectives: for tgo in self.connected_objectives:
if not tgo.capturable: if not tgo.capturable:
tgo.clear() tgo.clear()

View File

@ -4,7 +4,7 @@ import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import List from typing import List, Optional
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
@ -18,7 +18,7 @@ from game.theater.theatergroundobject import (
) )
from game.utils import Heading from game.utils import Heading
from game.version import VERSION from game.version import VERSION
from gen.templates import GroundObjectTemplates, TemplateCategory, GroundObjectTemplate from gen.templates import GroundObjectTemplates, GroundObjectTemplate
from gen.naming import namegen from gen.naming import namegen
from . import ( from . import (
ConflictTheater, ConflictTheater,
@ -28,6 +28,9 @@ from . import (
OffMapSpawn, OffMapSpawn,
) )
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
from ..data.units import UnitClass
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS
from ..dcs.unitgroup import UnitGroup
from ..profiling import logged_duration from ..profiling import logged_duration
from ..settings import Settings from ..settings import Settings
@ -68,15 +71,15 @@ class GameGenerator:
generator_settings: GeneratorSettings, generator_settings: GeneratorSettings,
mod_settings: ModSettings, mod_settings: ModSettings,
) -> None: ) -> None:
self.player = player.apply_mod_settings(mod_settings) self.player = player
self.enemy = enemy.apply_mod_settings(mod_settings) self.enemy = enemy
self.theater = theater self.theater = theater
self.air_wing_config = air_wing_config self.air_wing_config = air_wing_config
self.settings = settings self.settings = settings
self.generator_settings = generator_settings self.generator_settings = generator_settings
with logged_duration(f"Initializing templates"): with logged_duration(f"Initializing faction and templates"):
self.load_templates() self.initialize_factions(mod_settings)
def generate(self) -> Game: def generate(self) -> Game:
with logged_duration("TGO population"): with logged_duration("TGO population"):
@ -123,12 +126,11 @@ class GameGenerator:
for cp in to_remove: for cp in to_remove:
self.theater.controlpoints.remove(cp) self.theater.controlpoints.remove(cp)
def load_templates(self) -> None: def initialize_factions(self, mod_settings: ModSettings) -> None:
templates = GroundObjectTemplates.from_json( with logged_duration("Loading Templates from mapping"):
"resources/templates/templates.json" templates = GroundObjectTemplates.from_folder("resources/templates/")
) self.player.initialize(templates, mod_settings)
self.player.load_templates(templates) self.enemy.initialize(templates, mod_settings)
self.enemy.load_templates(templates)
class ControlPointGroundObjectGenerator: class ControlPointGroundObjectGenerator:
@ -152,21 +154,23 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool: def generate(self) -> bool:
self.control_point.connected_objectives = [] self.control_point.connected_objectives = []
if self.faction.navy_generators: self.generate_navy()
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
return True return True
def generate_random_from_templates( def generate_random_ground_object(
self, templates: list[GroundObjectTemplate], position: PointWithHeading self, unit_groups: list[UnitGroup], position: PointWithHeading
) -> None:
self.generate_ground_object_from_group(random.choice(unit_groups), position)
def generate_ground_object_from_group(
self, unit_group: UnitGroup, position: PointWithHeading
) -> None: ) -> None:
try: try:
template = random.choice(templates)
with logged_duration( with logged_duration(
f"Ground Object generation from template {template.name}" f"Ground Object generation for unit_group "
f"{unit_group.name} ({unit_group.role.value})"
): ):
ground_object = template.generate( ground_object = unit_group.generate(
namegen.random_objective_name(), namegen.random_objective_name(),
position, position,
self.control_point, self.control_point,
@ -176,23 +180,23 @@ class ControlPointGroundObjectGenerator:
except NotImplementedError: except NotImplementedError:
logging.error("Template Generator not implemented yet") logging.error("Template Generator not implemented yet")
except IndexError: except IndexError:
logging.error(f"No templates to generate object") logging.error(f"No templates to generate object from {unit_group.name}")
def generate_navy(self) -> None: def generate_navy(self) -> None:
skip_player_navy = self.generator_settings.no_player_navy skip_player_navy = self.generator_settings.no_player_navy
if self.control_point.captured and skip_player_navy: if self.control_point.captured and skip_player_navy:
return return
skip_enemy_navy = self.generator_settings.no_enemy_navy skip_enemy_navy = self.generator_settings.no_enemy_navy
if not self.control_point.captured and skip_enemy_navy: if not self.control_point.captured and skip_enemy_navy:
return return
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "ship")
)
for position in self.control_point.preset_locations.ships: for position in self.control_point.preset_locations.ships:
self.generate_random_from_templates(templates, position) unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.Navy
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Navy")
return
self.generate_ground_object_from_group(unit_group, position)
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator): class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
@ -213,12 +217,14 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
) )
return False return False
templates = list( unit_group = self.faction.random_group_for_role_and_task(
self.faction.templates.for_category(TemplateCategory.Naval, "carrier") GroupRole.Naval, GroupTask.AircraftCarrier
) )
if not unit_group:
self.generate_random_from_templates( logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
templates, return False
self.generate_ground_object_from_group(
unit_group,
PointWithHeading.from_point( PointWithHeading.from_point(
self.control_point.position, self.control_point.heading self.control_point.position, self.control_point.heading
), ),
@ -240,12 +246,14 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
) )
return False return False
templates = list( unit_group = self.faction.random_group_for_role_and_task(
self.faction.templates.for_category(TemplateCategory.Naval, "lha") GroupRole.Naval, GroupTask.HelicopterCarrier
) )
if not unit_group:
self.generate_random_from_templates( logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
templates, return False
self.generate_ground_object_from_group(
unit_group,
PointWithHeading.from_point( PointWithHeading.from_point(
self.control_point.position, self.control_point.heading self.control_point.position, self.control_point.heading
), ),
@ -280,110 +288,83 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_offshore_strike_targets() self.generate_offshore_strike_targets()
self.generate_factories() self.generate_factories()
self.generate_ammunition_depots() self.generate_ammunition_depots()
self.generate_missile_sites()
if self.faction.missiles: self.generate_coastal_sites()
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
def generate_armor_groups(self) -> None: def generate_armor_groups(self) -> None:
templates = list(self.faction.templates.for_category(TemplateCategory.Armor))
if not templates:
logging.error(f"{self.faction_name} has no access to Armor templates")
return
for position in self.control_point.preset_locations.armor_groups: for position in self.control_point.preset_locations.armor_groups:
self.generate_random_from_templates(templates, position) unit_group = self.faction.random_group_for_role_and_tasks(
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
)
if not unit_group:
logging.error(f"{self.faction_name} has no templates for Armor Groups")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_aa(self) -> None: def generate_aa(self) -> None:
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.long_range_sams: aa_tasking = [GroupTask.AAA]
self.generate_aa_at(
position,
[
AirDefenseRange.Long,
AirDefenseRange.Medium,
AirDefenseRange.Short,
AirDefenseRange.AAA,
],
)
for position in presets.medium_range_sams:
self.generate_aa_at(
position,
[
AirDefenseRange.Medium,
AirDefenseRange.Short,
AirDefenseRange.AAA,
],
)
for position in presets.short_range_sams:
self.generate_aa_at(
position,
[AirDefenseRange.Short, AirDefenseRange.AAA],
)
for position in presets.aaa: for position in presets.aaa:
self.generate_aa_at( self.generate_aa_at(position, aa_tasking)
position, aa_tasking.insert(0, GroupTask.SHORAD)
[AirDefenseRange.AAA], for position in presets.short_range_sams:
) self.generate_aa_at(position, aa_tasking)
aa_tasking.insert(0, GroupTask.MERAD)
for position in presets.medium_range_sams:
self.generate_aa_at(position, aa_tasking)
aa_tasking.insert(0, GroupTask.LORAD)
for position in presets.long_range_sams:
self.generate_aa_at(position, aa_tasking)
def generate_ewrs(self) -> None: def generate_ewrs(self) -> None:
templates = list(
self.faction.templates.for_category(TemplateCategory.AirDefence, "EWR")
)
if not templates:
logging.error(f"{self.faction_name} has no access to EWR templates")
return
for position in self.control_point.preset_locations.ewrs: for position in self.control_point.preset_locations.ewrs:
self.generate_random_from_templates(templates, position) unit_group = self.faction.random_group_for_role_and_task(
GroupRole.AntiAir, GroupTask.EWR
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for EWR")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_building_at( def generate_building_at(
self, self,
template_category: TemplateCategory, group_task: GroupTask,
building_category: str,
position: PointWithHeading, position: PointWithHeading,
) -> None: ) -> None:
templates = list( unit_group = self.faction.random_group_for_role_and_task(
self.faction.templates.for_category(template_category, building_category) GroupRole.Building, group_task
) )
if templates: if not unit_group:
self.generate_random_from_templates(templates, position)
else:
logging.error( logging.error(
f"{self.faction_name} has no access to Building type {building_category}" f"{self.faction_name} has no access to Building ({group_task.value})"
) )
return
self.generate_ground_object_from_group(unit_group, position)
def generate_ammunition_depots(self) -> None: def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots: for position in self.control_point.preset_locations.ammunition_depots:
self.generate_building_at(TemplateCategory.Building, "ammo", position) self.generate_building_at(GroupTask.Ammo, position)
def generate_factories(self) -> None: def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories: for position in self.control_point.preset_locations.factories:
self.generate_building_at(TemplateCategory.Building, "factory", position) self.generate_building_at(GroupTask.Factory, position)
def generate_aa_at( def generate_aa_at(
self, position: PointWithHeading, ranges: list[AirDefenseRange] self, position: PointWithHeading, tasks: list[GroupTask]
) -> None: ) -> None:
for task in tasks:
templates = [] unit_group = self.faction.random_group_for_role_and_task(
for aa_range in ranges: GroupRole.AntiAir, task
for template in self.faction.templates.for_category( )
TemplateCategory.AirDefence, aa_range.name if unit_group:
):
templates.append(template)
if len(templates) > 0:
# Only take next (smaller) aa_range when no template available for the # Only take next (smaller) aa_range when no template available for the
# most requested range. Otherwise break the loop and continue # most requested range. Otherwise break the loop and continue
break self.generate_ground_object_from_group(unit_group, position)
return
if templates: logging.error(
self.generate_random_from_templates(templates, position) f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
else: )
logging.error(
f"{self.faction_name} has no access to SAM Templates ({', '.join([range.name for range in ranges])})"
)
def generate_scenery_sites(self) -> None: def generate_scenery_sites(self) -> None:
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
@ -423,38 +404,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None: def generate_missile_sites(self) -> None:
templates = list(self.faction.templates.for_category(TemplateCategory.Missile))
if not templates:
logging.error(f"{self.faction_name} has no access to Missile templates")
return
for position in self.control_point.preset_locations.missile_sites: for position in self.control_point.preset_locations.missile_sites:
self.generate_random_from_templates(templates, position) unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Defenses, GroupTask.Missile
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Missile")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_coastal_sites(self) -> None: def generate_coastal_sites(self) -> None:
templates = list(self.faction.templates.for_category(TemplateCategory.Coastal))
if not templates:
logging.error(f"{self.faction_name} has no access to Coastal templates")
return
for position in self.control_point.preset_locations.coastal_defenses: for position in self.control_point.preset_locations.coastal_defenses:
self.generate_random_from_templates(templates, position) unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Defenses, GroupTask.Coastal
)
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for Coastal")
return
self.generate_ground_object_from_group(unit_group, position)
def generate_strike_targets(self) -> None: def generate_strike_targets(self) -> None:
building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set:
logging.error(f"{self.faction_name} has no buildings defined")
return
for position in self.control_point.preset_locations.strike_locations: for position in self.control_point.preset_locations.strike_locations:
category = random.choice(building_set) self.generate_building_at(GroupTask.StrikeTarget, position)
self.generate_building_at(TemplateCategory.Building, category, position)
def generate_offshore_strike_targets(self) -> None: def generate_offshore_strike_targets(self) -> None:
if "oil" not in self.faction.building_set:
logging.error(
f"{self.faction_name} does not support offshore strike targets"
)
return
for position in self.control_point.preset_locations.offshore_strike_locations: for position in self.control_point.preset_locations.offshore_strike_locations:
self.generate_building_at(TemplateCategory.Building, "oil", position) self.generate_building_at(GroupTask.Oil, position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
@ -466,8 +441,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate_fob(self) -> None: def generate_fob(self) -> None:
self.generate_building_at( self.generate_building_at(
TemplateCategory.Building, GroupTask.FOB,
"fob",
PointWithHeading.from_point( PointWithHeading.from_point(
self.control_point.position, self.control_point.heading self.control_point.position, self.control_point.heading
), ),

View File

@ -8,6 +8,7 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
from dcs.unittype import VehicleType, ShipType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from dcs.ships import ship_map from dcs.ships import ship_map
@ -17,6 +18,7 @@ from dcs.triggers import TriggerZone
from game.dcs.helpers import unit_type_from_name from game.dcs.helpers import unit_type_from_name
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..dcs.groundunittype import GroundUnitType from ..dcs.groundunittype import GroundUnitType
from ..dcs.shipunittype import ShipUnitType
from ..dcs.unittype import UnitType from ..dcs.unittype import UnitType
from ..point_with_heading import PointWithHeading from ..point_with_heading import PointWithHeading
from ..utils import Distance, Heading, meters from ..utils import Distance, Heading, meters
@ -90,11 +92,13 @@ class GroundUnit:
_unit_type: Optional[UnitType[Any]] = None _unit_type: Optional[UnitType[Any]] = None
@staticmethod @staticmethod
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit: def from_template(
id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject
) -> GroundUnit:
return GroundUnit( return GroundUnit(
id, id,
t.name, t.name,
t.type, unit_type,
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)), PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
go, go,
) )
@ -103,20 +107,16 @@ class GroundUnit:
def unit_type(self) -> Optional[UnitType[Any]]: def unit_type(self) -> Optional[UnitType[Any]]:
if not self._unit_type: if not self._unit_type:
try: try:
if self.type in vehicle_map: unit_type: Optional[UnitType[Any]] = None
vehicle_type = db.vehicle_type_from_name(self.type) dcs_type = db.unit_type_from_name(self.type)
self._unit_type = next(GroundUnitType.for_dcs_type(vehicle_type)) if dcs_type and issubclass(dcs_type, VehicleType):
elif self.type in ship_map: unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
ship_type = db.ship_type_from_name(self.type) elif dcs_type and issubclass(dcs_type, ShipType):
# TODO Allow handling of Ships. This requires extension of UnitType unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
return None self._unit_type = unit_type
elif (static_type := db.static_type_from_name(self.type)) is not None:
# TODO Allow handling of Statics
return None
else:
return None
except StopIteration: except StopIteration:
return None logging.error(f"No UnitType for {self.type}")
pass
return self._unit_type return self._unit_type
def kill(self) -> None: def kill(self) -> None:
@ -153,36 +153,15 @@ class GroundGroup:
id: int, id: int,
g: GroupTemplate, g: GroupTemplate,
go: TheaterGroundObject, go: TheaterGroundObject,
randomization: bool = True,
) -> GroundGroup: ) -> GroundGroup:
units = []
if g.randomizer:
g.randomizer.randomize()
for u_id, unit in enumerate(g.units):
tgo_unit = GroundUnit.from_template(u_id, unit, go)
if randomization and g.randomizer:
if g.randomizer.unit_type:
tgo_unit.type = g.randomizer.unit_type
try:
# Check if unit can be assigned
g.randomizer.use_unit()
except IndexError:
# Do not generate the unit as no more units are available
continue
units.append(tgo_unit)
tgo_group = GroundGroup( tgo_group = GroundGroup(
id, id,
g.name, g.name,
PointWithHeading.from_point(go.position, go.heading), PointWithHeading.from_point(go.position, go.heading),
units, g.generate_units(go),
go, go,
) )
tgo_group.static_group = g.static tgo_group.static_group = g.static
return tgo_group return tgo_group
@property @property

View File

@ -1305,7 +1305,7 @@ class FlightPlanBuilder:
for group in location.groups: for group in location.groups:
if group.units: if group.units:
targets.append( targets.append(
StrikeTarget(f"{group.name} at {location.name}", group) StrikeTarget(f"{group.group_name} at {location.name}", group)
) )
elif isinstance(location, Convoy): elif isinstance(location, Convoy):
targets.append(StrikeTarget(location.name, location)) targets.append(StrikeTarget(location.name, location))
@ -1318,7 +1318,7 @@ class FlightPlanBuilder:
@staticmethod @staticmethod
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]: def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups] return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan: def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan. """Generates an anti-ship flight plan.

View File

@ -5,7 +5,7 @@ import random
from enum import Enum from enum import Enum
from typing import Dict, List, TYPE_CHECKING from typing import Dict, List, TYPE_CHECKING
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
@ -100,28 +100,28 @@ class GroundPlanner:
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor: for unit_type in self.cp.base.armor:
unit_class = unit_type.unit_class unit_class = unit_type.unit_class
if unit_class is GroundUnitClass.Tank: if unit_class is UnitClass.Tank:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif unit_class is GroundUnitClass.Apc: elif unit_class is UnitClass.Apc:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif unit_class is GroundUnitClass.Artillery: elif unit_class is UnitClass.Artillery:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif unit_class is GroundUnitClass.Ifv: elif unit_class is UnitClass.Ifv:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif unit_class is GroundUnitClass.Logistics: elif unit_class is UnitClass.Logistics:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif unit_class is GroundUnitClass.Atgm: elif unit_class is UnitClass.Atgm:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif unit_class is GroundUnitClass.Shorads: elif unit_class is UnitClass.SHORAD:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
elif unit_class is GroundUnitClass.Recon: elif unit_class is UnitClass.Recon:
collection = self.recon_groups collection = self.recon_groups
role = CombatGroupRole.RECON role = CombatGroupRole.RECON
else: else:

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ from PySide2.QtWidgets import (
) )
from game import Game from game import Game
from game.data.groups import GroupRole, ROLE_TASKINGS, GroupTask
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
@ -26,7 +27,6 @@ from game.theater.theatergroundobject import (
GroundGroup, GroundGroup,
) )
from gen.templates import ( from gen.templates import (
TemplateCategory,
GroundObjectTemplate, GroundObjectTemplate,
GroupTemplate, GroupTemplate,
) )
@ -41,7 +41,9 @@ class QGroundObjectGroupTemplate(QGroupBox):
# If the group is not randomizable: Just view labels instead of edit fields # If the group is not randomizable: Just view labels instead of edit fields
def __init__(self, group_id: int, group_template: GroupTemplate) -> None: def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
super(QGroundObjectGroupTemplate, self).__init__(str(group_id + 1)) super(QGroundObjectGroupTemplate, self).__init__(
f"{group_id + 1}: {group_template.name}"
)
self.group_template = group_template self.group_template = group_template
self.group_layout = QGridLayout() self.group_layout = QGridLayout()
@ -51,12 +53,12 @@ class QGroundObjectGroupTemplate(QGroupBox):
self.unit_selector = QComboBox() self.unit_selector = QComboBox()
self.group_selector = QCheckBox() self.group_selector = QCheckBox()
self.group_selector.setChecked(True) self.group_selector.setChecked(self.group_template.should_be_generated)
self.group_selector.setEnabled(self.group_template.optional) self.group_selector.setEnabled(self.group_template.optional)
if self.group_template.randomizer: if self.group_template.can_be_modified:
# Group can be randomized # Group can be modified (more than 1 possible unit_type for the group)
for unit in self.group_template.randomizer.possible_ground_units: for unit in self.group_template.possible_units:
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit) self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.group_layout.addWidget( self.group_layout.addWidget(
self.unit_selector, 0, 0, alignment=Qt.AlignRight self.unit_selector, 0, 0, alignment=Qt.AlignRight
@ -66,17 +68,21 @@ class QGroundObjectGroupTemplate(QGroupBox):
) )
self.amount_selector.setMinimum(1) self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(len(self.group_template.units)) self.amount_selector.setMaximum(self.group_template.max_size)
self.amount_selector.setValue(self.group_template.randomizer.unit_count) self.amount_selector.setValue(self.group_template.size)
self.on_group_changed() self.on_group_changed()
else: else:
# Group can not be randomized so just show the group info # Group can not be randomized so just show the group info
group_info = QVBoxLayout() group_info = QVBoxLayout()
for unit_type, count in self.group_template.unit_types_count.items(): try:
group_info.addWidget( unit_name = next(self.group_template.possible_units)
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft except StopIteration:
) unit_name = self.group_template.unit_type
group_info.addWidget(
QLabel(f"{self.group_template.size}x {unit_name}"),
alignment=Qt.AlignLeft,
)
self.group_layout.addLayout(group_info, 0, 0, 1, 2) self.group_layout.addLayout(group_info, 0, 0, 1, 2)
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight) self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
@ -86,10 +92,11 @@ class QGroundObjectGroupTemplate(QGroupBox):
self.group_selector.stateChanged.connect(self.on_group_changed) self.group_selector.stateChanged.connect(self.on_group_changed)
def on_group_changed(self) -> None: def on_group_changed(self) -> None:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex()) self.group_template.set_enabled(self.group_selector.isChecked())
count = self.amount_selector.value() if self.group_selector.isChecked() else 0 if self.group_template.can_be_modified:
self.group_template.randomizer.count = count unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
self.group_template.randomizer.force_type(unit_type.dcs_id) self.group_template.unit_count = [self.amount_selector.value()]
self.group_template.set_unit_type(unit_type.dcs_id)
self.group_template_changed.emit(self.group_template) self.group_template_changed.emit(self.group_template)
@ -143,8 +150,6 @@ class QGroundObjectTemplateLayout(QGroupBox):
def update_price(self) -> None: def update_price(self) -> None:
price = "$" + str(self.template.estimated_price_for(self.ground_object)) 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]") self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
def buy_group(self): def buy_group(self):
@ -216,30 +221,40 @@ class QGroundObjectBuyMenu(QDialog):
self.mainLayout = QGridLayout() self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
self.unit_group_selector = QComboBox()
self.template_selector = QComboBox() self.template_selector = QComboBox()
self.template_selector.currentIndexChanged.connect(self.template_changed) self.template_selector.setEnabled(False)
# Get the templates and fill the combobox # Get the templates and fill the combobox
template_sub_category = None template_sub_category = None
tasks = []
if isinstance(ground_object, SamGroundObject): if isinstance(ground_object, SamGroundObject):
template_category = TemplateCategory.AirDefence role = GroupRole.AntiAir
elif isinstance(ground_object, VehicleGroupGroundObject): elif isinstance(ground_object, VehicleGroupGroundObject):
template_category = TemplateCategory.Armor role = GroupRole.GroundForce
elif isinstance(ground_object, EwrGroundObject): elif isinstance(ground_object, EwrGroundObject):
template_category = TemplateCategory.AirDefence role = GroupRole.AntiAir
template_sub_category = "EWR" tasks.append(GroupTask.EWR)
else: else:
raise RuntimeError raise RuntimeError
for template in game.blue.faction.templates.for_category( if not tasks:
template_category, template_sub_category tasks = ROLE_TASKINGS[role]
):
self.template_selector.addItem(template.name, userData=template) for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks):
self.unit_group_selector.addItem(unit_group.name, userData=unit_group)
self.template_selector.currentIndexChanged.connect(self.template_changed)
self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed)
template_selector_layout = QGridLayout() template_selector_layout = QGridLayout()
template_selector_layout.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft) template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget( template_selector_layout.addWidget(
self.template_selector, 0, 1, alignment=Qt.AlignRight self.unit_group_selector, 0, 1, alignment=Qt.AlignRight
)
template_selector_layout.addWidget(QLabel("Template :"), 1, 0, Qt.AlignLeft)
template_selector_layout.addWidget(
self.template_selector, 1, 1, alignment=Qt.AlignRight
) )
self.mainLayout.addLayout(template_selector_layout, 0, 0) self.mainLayout.addLayout(template_selector_layout, 0, 0)
@ -250,10 +265,22 @@ class QGroundObjectBuyMenu(QDialog):
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
# Update UI # Update UI
self.template_changed() self.unit_group_changed()
def unit_group_changed(self) -> None:
unit_group = self.unit_group_selector.itemData(
self.unit_group_selector.currentIndex()
)
self.template_selector.clear()
if unit_group.templates:
for template in unit_group.templates:
self.template_selector.addItem(template.name, userData=template)
# Enable if more than one template is available
self.template_selector.setEnabled(len(unit_group.templates) > 1)
def template_changed(self): def template_changed(self):
template = self.template_selector.itemData( template = self.template_selector.itemData(
self.template_selector.currentIndex() self.template_selector.currentIndex()
) )
self.template_changed_signal.emit(template) if template is not None:
self.template_changed_signal.emit(template)

View File

@ -14,7 +14,7 @@ from dcs import Point, vehicles
from game import Game from game import Game
from game.config import REWARDS from game.config import REWARDS
from game.data.building_data import FORTIFICATION_BUILDINGS from game.data.building_data import FORTIFICATION_BUILDINGS
from game.data.groundunitclass import GroundUnitClass from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
@ -181,8 +181,11 @@ class QGroundObjectMenu(QDialog):
return return
for u in self.ground_object.units: for u in self.ground_object.units:
# Hack: Unknown variant. # Hack: Unknown variant.
unit_type = next(GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])) if u.type in vehicles.vehicle_map:
total_value += unit_type.price unit_type = next(
GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])
)
total_value += unit_type.price
if self.sell_all_button is not None: if self.sell_all_button is not None:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)") self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value self.total_value = total_value

View File

@ -52,18 +52,9 @@
"sams": [ "sams": [
"HawkGenerator" "HawkGenerator"
], ],
"aircraft_carrier": [ "naval_units": [
"Stennis" "LHA-1 Tarawa",
], "CVN-74 John C. Stennis"
"helicopter_carrier": [
"LHA_Tarawa"
],
"destroyers": [
"PERRY",
"USS_Arleigh_Burke_IIa"
],
"cruisers": [
"TICONDEROG"
], ],
"requirements": {"mod": "Some mod is required"}, "requirements": {"mod": "Some mod is required"},
"carrier_names": [ "carrier_names": [
@ -79,10 +70,6 @@
"LHA-4 Nassau", "LHA-4 Nassau",
"LHA-5 Peleliu" "LHA-5 Peleliu"
], ],
"navy_generators": [
"OliverHazardPerryGroupGenerator",
"ArleighBurkeGroupGenerator"
],
"has_jtac": true, "has_jtac": true,
"jtac_unit": "MQ_9_Reaper" "jtac_unit": "MQ_9_Reaper"
} }

View File

@ -95,15 +95,8 @@ class TestFactionLoader(unittest.TestCase):
self.assertIn(Infantry.Soldier_M4, faction.infantry_units) self.assertIn(Infantry.Soldier_M4, faction.infantry_units)
self.assertIn(Infantry.Soldier_M249, faction.infantry_units) self.assertIn(Infantry.Soldier_M249, faction.infantry_units)
self.assertIn("AvengerGenerator", faction.air_defenses) self.assertIn(Stennis.name, faction.naval_units)
self.assertIn(LHA_Tarawa.name, faction.naval_units)
self.assertIn("HawkGenerator", faction.air_defenses)
self.assertIn(Stennis, faction.aircraft_carrier)
self.assertIn(LHA_Tarawa, faction.helicopter_carrier)
self.assertIn(PERRY, faction.destroyers)
self.assertIn(USS_Arleigh_Burke_IIa, faction.destroyers)
self.assertIn(TICONDEROG, faction.cruisers)
self.assertIn("mod", faction.requirements.keys()) self.assertIn("mod", faction.requirements.keys())
self.assertIn("Some mod is required", faction.requirements.values()) self.assertIn("Some mod is required", faction.requirements.values())
@ -111,9 +104,6 @@ class TestFactionLoader(unittest.TestCase):
self.assertEqual(4, len(faction.carrier_names)) self.assertEqual(4, len(faction.carrier_names))
self.assertEqual(5, len(faction.helicopter_carrier_names)) self.assertEqual(5, len(faction.helicopter_carrier_names))
self.assertIn("OliverHazardPerryGroupGenerator", faction.navy_generators)
self.assertIn("ArleighBurkeGroupGenerator", faction.navy_generators)
@pytest.mark.skip(reason="Faction unit names in the json files are outdated") @pytest.mark.skip(reason="Faction unit names in the json files are outdated")
def test_load_valid_faction_with_invalid_country(self) -> None: def test_load_valid_faction_with_invalid_country(self) -> None: