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 datetime import timedelta
from game.data.groundunitclass import GroundUnitClass
from game.data.units import UnitClass
from game.utils import Distance, feet, nautical_miles
@dataclass
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:
return self.ratios[unit_class] / sum(self.ratios.values())
except KeyError:
@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 2,
GroundUnitClass.Ifv: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
UnitClass.Tank: 3,
UnitClass.Atgm: 2,
UnitClass.Apc: 2,
UnitClass.Ifv: 3,
UnitClass.Artillery: 1,
UnitClass.SHORAD: 2,
UnitClass.Recon: 1,
}
),
)
@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 4,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 3,
GroundUnitClass.Ifv: 2,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
UnitClass.Tank: 4,
UnitClass.Atgm: 2,
UnitClass.Apc: 3,
UnitClass.Ifv: 2,
UnitClass.Artillery: 1,
UnitClass.SHORAD: 2,
UnitClass.Recon: 1,
}
),
)
@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 3,
GroundUnitClass.Apc: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 3,
GroundUnitClass.Recon: 1,
UnitClass.Tank: 3,
UnitClass.Atgm: 3,
UnitClass.Apc: 3,
UnitClass.Artillery: 1,
UnitClass.SHORAD: 3,
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.unittype import FlyingType
from game.data.units import UnitClass
from game.dcs.unitproperty import UnitProperty
from game.dcs.unittype import UnitType
from game.radio.channels import (
@ -180,19 +181,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator]
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
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@ -309,35 +297,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
state.update(updated.__dict__)
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
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, AircraftType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
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
def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_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
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
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,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.Plane,
)

View File

@ -1,65 +1,42 @@
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
from typing import Type, Optional, Iterator
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass
from game.data.units import UnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
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
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, GroundUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
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
def _each_unit_type() -> Iterator[Type[VehicleType]]:
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
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
@ -78,9 +55,8 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
introduction = "No data."
class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None
if class_name is not None:
unit_class = GroundUnitClass(class_name)
# TODO Exception handling for missing classes
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
for variant in data.get("variants", [vehicle.id]):
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 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 game.data.units import UnitClass
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
class UnitType(ABC, Generic[DcsUnitTypeT]):
dcs_unit_type: DcsUnitTypeT
name: str
description: str
@ -17,10 +23,47 @@ class UnitType(Generic[DcsUnitTypeT]):
manufacturer: str
role: str
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:
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
def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False)

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import copy
import itertools
import logging
import random
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType, UnitType
from dcs.unittype import ShipType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
@ -22,10 +23,19 @@ from game.data.doctrine import (
COLDWAR_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.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:
from game.theater.start_generator import ModSettings
@ -70,50 +80,26 @@ class Faction:
# Logistics units used
logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list)
# Possible Air Defence units, Like EWRs
air_defense_units: List[GroundUnitType] = field(default_factory=list)
# Possible EWR generators for this faction.
ewrs: List[GroundUnitType] = field(default_factory=list)
# A list of all supported sets of units
preset_groups: list[UnitGroup] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
missiles: List[GroundUnitType] = field(default_factory=list)
# Required mods or asset packs
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
carrier_names: List[str] = field(default_factory=list)
# Possible helicopter carrier names
helicopter_carrier_names: List[str] = field(default_factory=list)
# Navy group generators
navy_generators: List[str] = 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)
# Available Naval Units
naval_units: List[ShipUnitType] = field(default_factory=list)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
@ -124,7 +110,7 @@ class Faction:
# 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)
# List of default livery overrides
@ -142,64 +128,183 @@ class Faction:
# All possible templates which can be generated by the faction
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:
return getattr(self, item)
def has_access_to_unit_type(self, unit_type: str) -> bool:
# Supports all GroundUnit lists and AirDefenses
for unit in self.ground_units:
if unit_type == unit.dcs_id:
return True
return unit_type in self.air_defenses
@property
def accessible_units(self) -> Iterator[UnitType[Any]]:
yield from self._accessible_units
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
if vehicle.unit_class is unit_class:
return True
@property
def air_defenses(self) -> list[str]:
"""Returns the Air Defense types"""
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
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
# This loads all faction possible sam templates and the default ones
# 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.
# For example it can be possible to define the unit_types and check if all
# requirements for the template are fulfilled.
for category, template in all_templates.templates:
if (
(
category == TemplateCategory.AirDefence
and (
# Check if faction has the template name or ALL required
# unit_types in the list air_defenses. For legacy reasons this
# allows both and also the EWR template
template.name in self.air_defenses
or all(
self.has_access_to_unit_type(required_unit)
for required_unit in template.required_units
)
or template.template_type == "EWR"
)
)
or template.name in self.navy_generators
or template.name in self.missiles
or template.name in self.coastal_defenses
or (
template.template_type
in self.building_set + ["fob", "ammo", "factory"]
)
or (template.template_type == "carrier" and self.aircraft_carrier)
or (template.template_type == "lha" and self.helicopter_carrier)
or category == TemplateCategory.Armor
):
# 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)
# Initialize all randomizers
for group_template in faction_template.groups:
if group_template.randomizer:
group_template.randomizer.init_randomization_for_faction(self)
self.templates.add_template(category, faction_template)
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
return any(unit.unit_class is unit_class for unit in self.accessible_units)
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
self._accessible_units = []
all_units: Iterator[UnitType[Any]] = itertools.chain(
self.ground_units,
self.infantry_units,
self.air_defense_units,
self.naval_units,
self.missiles,
(
ground_unit
for preset_group in self.preset_groups
for ground_unit in preset_group.ground_units
),
(
ship_unit
for preset_group in self.preset_groups
for ship_unit in preset_group.ship_units
),
)
for unit in all_units:
if unit not in self._accessible_units:
self._accessible_units.append(unit)
def initialize(
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
) -> None:
# Apply the mod settings
self._apply_mod_settings(mod_settings)
# Load all accessible units and store them for performant later usage
self._load_accessible_units(all_templates)
# Load all faction compatible templates
self._load_templates(all_templates)
# Load Unit Groups
self._load_unit_groups()
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
if not unit_group.templates:
unit_group.load_templates(self)
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
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@ -240,34 +345,30 @@ class Faction:
faction.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", [])
# Compatibility for older factions. All air defenses now belong to a
# single group and the generator decides what belongs where.
faction.air_defenses.extend(json.get("sams", []))
faction.air_defenses.extend(json.get("shorads", []))
faction.preset_groups = [
UnitGroup.named(n) for n in json.get("preset_groups", [])
]
faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("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.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.naval_units = [
ShipUnitType.named(n) for n in json.get("naval_units", [])
]
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
faction.jtac_unit = AircraftType.named(jtac_name)
else:
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
doctrine = json.get("doctrine", "modern")
@ -304,6 +405,7 @@ class Faction:
# Templates
faction.templates = GroundObjectTemplates()
return faction
@property
@ -312,14 +414,49 @@ class Faction:
yield from self.frontline_units
yield from self.logistics_units
def infantry_with_class(
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
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
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
@ -379,24 +516,23 @@ class Faction:
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA-10B/S-300PS Battery")
self.remove_air_defenses("SA-12/S-300V Battery")
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
self.remove_air_defenses("SA-23/S-300VM Battery")
self.remove_air_defenses("SA-17 Grizzly Battery")
self.remove_air_defenses("KS-19 AAA Site")
return self
self.remove_presets("SA-10B/S-300PS")
self.remove_presets("SA-12/S-300V")
self.remove_presets("SA-20/S-300PMU-1")
self.remove_presets("SA-20B/S-300PMU-2")
self.remove_presets("SA-23/S-300VM")
self.remove_presets("SA-17")
self.remove_presets("KS-19")
def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_air_defenses(self, name: str) -> None:
for i in self.air_defenses:
if i == name:
self.air_defenses.remove(i)
def remove_presets(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:
self.preset_groups.remove(pg)
def remove_vehicle(self, name: str) -> None:
for i in self.frontline_units:

View File

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

View File

@ -20,9 +20,11 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return
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:
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
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import logging
import random
from dataclasses import dataclass
from datetime import datetime
from typing import List
from typing import List, Optional
from game import Game
from game.factions.faction import Faction
@ -18,7 +18,7 @@ from game.theater.theatergroundobject import (
)
from game.utils import Heading
from game.version import VERSION
from gen.templates import GroundObjectTemplates, TemplateCategory, GroundObjectTemplate
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
from gen.naming import namegen
from . import (
ConflictTheater,
@ -28,6 +28,9 @@ from . import (
OffMapSpawn,
)
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 ..settings import Settings
@ -68,15 +71,15 @@ class GameGenerator:
generator_settings: GeneratorSettings,
mod_settings: ModSettings,
) -> None:
self.player = player.apply_mod_settings(mod_settings)
self.enemy = enemy.apply_mod_settings(mod_settings)
self.player = player
self.enemy = enemy
self.theater = theater
self.air_wing_config = air_wing_config
self.settings = settings
self.generator_settings = generator_settings
with logged_duration(f"Initializing templates"):
self.load_templates()
with logged_duration(f"Initializing faction and templates"):
self.initialize_factions(mod_settings)
def generate(self) -> Game:
with logged_duration("TGO population"):
@ -123,12 +126,11 @@ class GameGenerator:
for cp in to_remove:
self.theater.controlpoints.remove(cp)
def load_templates(self) -> None:
templates = GroundObjectTemplates.from_json(
"resources/templates/templates.json"
)
self.player.load_templates(templates)
self.enemy.load_templates(templates)
def initialize_factions(self, mod_settings: ModSettings) -> None:
with logged_duration("Loading Templates from mapping"):
templates = GroundObjectTemplates.from_folder("resources/templates/")
self.player.initialize(templates, mod_settings)
self.enemy.initialize(templates, mod_settings)
class ControlPointGroundObjectGenerator:
@ -152,21 +154,23 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
self.generate_navy()
return True
def generate_random_from_templates(
self, templates: list[GroundObjectTemplate], position: PointWithHeading
def generate_random_ground_object(
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:
try:
template = random.choice(templates)
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(),
position,
self.control_point,
@ -176,23 +180,23 @@ class ControlPointGroundObjectGenerator:
except NotImplementedError:
logging.error("Template Generator not implemented yet")
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:
skip_player_navy = self.generator_settings.no_player_navy
if self.control_point.captured and skip_player_navy:
return
skip_enemy_navy = self.generator_settings.no_enemy_navy
if not self.control_point.captured and skip_enemy_navy:
return
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "ship")
)
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):
@ -213,12 +217,14 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "carrier")
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.AircraftCarrier
)
self.generate_random_from_templates(
templates,
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
return False
self.generate_ground_object_from_group(
unit_group,
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),
@ -240,12 +246,14 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
)
return False
templates = list(
self.faction.templates.for_category(TemplateCategory.Naval, "lha")
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Naval, GroupTask.HelicopterCarrier
)
self.generate_random_from_templates(
templates,
if not unit_group:
logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
return False
self.generate_ground_object_from_group(
unit_group,
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),
@ -280,110 +288,83 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.generate_offshore_strike_targets()
self.generate_factories()
self.generate_ammunition_depots()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
self.generate_missile_sites()
self.generate_coastal_sites()
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:
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:
presets = self.control_point.preset_locations
for position in presets.long_range_sams:
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],
)
aa_tasking = [GroupTask.AAA]
for position in presets.aaa:
self.generate_aa_at(
position,
[AirDefenseRange.AAA],
)
self.generate_aa_at(position, aa_tasking)
aa_tasking.insert(0, GroupTask.SHORAD)
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:
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:
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(
self,
template_category: TemplateCategory,
building_category: str,
group_task: GroupTask,
position: PointWithHeading,
) -> None:
templates = list(
self.faction.templates.for_category(template_category, building_category)
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.Building, group_task
)
if templates:
self.generate_random_from_templates(templates, position)
else:
if not unit_group:
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:
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:
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(
self, position: PointWithHeading, ranges: list[AirDefenseRange]
self, position: PointWithHeading, tasks: list[GroupTask]
) -> None:
templates = []
for aa_range in ranges:
for template in self.faction.templates.for_category(
TemplateCategory.AirDefence, aa_range.name
):
templates.append(template)
if len(templates) > 0:
for task in tasks:
unit_group = self.faction.random_group_for_role_and_task(
GroupRole.AntiAir, task
)
if unit_group:
# Only take next (smaller) aa_range when no template available for the
# most requested range. Otherwise break the loop and continue
break
self.generate_ground_object_from_group(unit_group, position)
return
if templates:
self.generate_random_from_templates(templates, position)
else:
logging.error(
f"{self.faction_name} has no access to SAM Templates ({', '.join([range.name for range in ranges])})"
)
logging.error(
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
)
def generate_scenery_sites(self) -> None:
presets = self.control_point.preset_locations
@ -423,38 +404,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
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:
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:
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:
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:
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:
category = random.choice(building_set)
self.generate_building_at(TemplateCategory.Building, category, position)
self.generate_building_at(GroupTask.StrikeTarget, position)
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:
self.generate_building_at(TemplateCategory.Building, "oil", position)
self.generate_building_at(GroupTask.Oil, position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
@ -466,8 +441,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate_fob(self) -> None:
self.generate_building_at(
TemplateCategory.Building,
"fob",
GroupTask.FOB,
PointWithHeading.from_point(
self.control_point.position, self.control_point.heading
),

View File

@ -8,6 +8,7 @@ from dataclasses import dataclass
from enum import Enum
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
from dcs.unittype import VehicleType, ShipType
from dcs.vehicles import vehicle_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 ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..dcs.groundunittype import GroundUnitType
from ..dcs.shipunittype import ShipUnitType
from ..dcs.unittype import UnitType
from ..point_with_heading import PointWithHeading
from ..utils import Distance, Heading, meters
@ -90,11 +92,13 @@ class GroundUnit:
_unit_type: Optional[UnitType[Any]] = None
@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(
id,
t.name,
t.type,
unit_type,
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
go,
)
@ -103,20 +107,16 @@ class GroundUnit:
def unit_type(self) -> Optional[UnitType[Any]]:
if not self._unit_type:
try:
if self.type in vehicle_map:
vehicle_type = db.vehicle_type_from_name(self.type)
self._unit_type = next(GroundUnitType.for_dcs_type(vehicle_type))
elif self.type in ship_map:
ship_type = db.ship_type_from_name(self.type)
# TODO Allow handling of Ships. This requires extension of UnitType
return None
elif (static_type := db.static_type_from_name(self.type)) is not None:
# TODO Allow handling of Statics
return None
else:
return None
unit_type: Optional[UnitType[Any]] = None
dcs_type = db.unit_type_from_name(self.type)
if dcs_type and issubclass(dcs_type, VehicleType):
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
elif dcs_type and issubclass(dcs_type, ShipType):
unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
self._unit_type = unit_type
except StopIteration:
return None
logging.error(f"No UnitType for {self.type}")
pass
return self._unit_type
def kill(self) -> None:
@ -153,36 +153,15 @@ class GroundGroup:
id: int,
g: GroupTemplate,
go: TheaterGroundObject,
randomization: bool = True,
) -> 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(
id,
g.name,
PointWithHeading.from_point(go.position, go.heading),
units,
g.generate_units(go),
go,
)
tgo_group.static_group = g.static
return tgo_group
@property

View File

@ -1305,7 +1305,7 @@ class FlightPlanBuilder:
for group in location.groups:
if group.units:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group)
StrikeTarget(f"{group.group_name} at {location.name}", group)
)
elif isinstance(location, Convoy):
targets.append(StrikeTarget(location.name, location))
@ -1318,7 +1318,7 @@ class FlightPlanBuilder:
@staticmethod
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:
"""Generates an anti-ship flight plan.

View File

@ -5,7 +5,7 @@ import random
from enum import Enum
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.theater import ControlPoint
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
for unit_type in self.cp.base.armor:
unit_class = unit_type.unit_class
if unit_class is GroundUnitClass.Tank:
if unit_class is UnitClass.Tank:
collection = self.tank_groups
role = CombatGroupRole.TANK
elif unit_class is GroundUnitClass.Apc:
elif unit_class is UnitClass.Apc:
collection = self.apc_group
role = CombatGroupRole.APC
elif unit_class is GroundUnitClass.Artillery:
elif unit_class is UnitClass.Artillery:
collection = self.art_group
role = CombatGroupRole.ARTILLERY
elif unit_class is GroundUnitClass.Ifv:
elif unit_class is UnitClass.Ifv:
collection = self.ifv_group
role = CombatGroupRole.IFV
elif unit_class is GroundUnitClass.Logistics:
elif unit_class is UnitClass.Logistics:
collection = self.logi_groups
role = CombatGroupRole.LOGI
elif unit_class is GroundUnitClass.Atgm:
elif unit_class is UnitClass.Atgm:
collection = self.atgm_group
role = CombatGroupRole.ATGM
elif unit_class is GroundUnitClass.Shorads:
elif unit_class is UnitClass.SHORAD:
collection = self.shorad_groups
role = CombatGroupRole.SHORAD
elif unit_class is GroundUnitClass.Recon:
elif unit_class is UnitClass.Recon:
collection = self.recon_groups
role = CombatGroupRole.RECON
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.data.groups import GroupRole, ROLE_TASKINGS, GroupTask
from game.point_with_heading import PointWithHeading
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import (
@ -26,7 +27,6 @@ from game.theater.theatergroundobject import (
GroundGroup,
)
from gen.templates import (
TemplateCategory,
GroundObjectTemplate,
GroupTemplate,
)
@ -41,7 +41,9 @@ class QGroundObjectGroupTemplate(QGroupBox):
# If the group is not randomizable: Just view labels instead of edit fields
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_layout = QGridLayout()
@ -51,12 +53,12 @@ class QGroundObjectGroupTemplate(QGroupBox):
self.unit_selector = QComboBox()
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)
if self.group_template.randomizer:
# Group can be randomized
for unit in self.group_template.randomizer.possible_ground_units:
if self.group_template.can_be_modified:
# Group can be modified (more than 1 possible unit_type for the group)
for unit in self.group_template.possible_units:
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
self.group_layout.addWidget(
self.unit_selector, 0, 0, alignment=Qt.AlignRight
@ -66,17 +68,21 @@ class QGroundObjectGroupTemplate(QGroupBox):
)
self.amount_selector.setMinimum(1)
self.amount_selector.setMaximum(len(self.group_template.units))
self.amount_selector.setValue(self.group_template.randomizer.unit_count)
self.amount_selector.setMaximum(self.group_template.max_size)
self.amount_selector.setValue(self.group_template.size)
self.on_group_changed()
else:
# Group can not be randomized so just show the group info
group_info = QVBoxLayout()
for unit_type, count in self.group_template.unit_types_count.items():
group_info.addWidget(
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft
)
try:
unit_name = next(self.group_template.possible_units)
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.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)
def on_group_changed(self) -> None:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
count = self.amount_selector.value() if self.group_selector.isChecked() else 0
self.group_template.randomizer.count = count
self.group_template.randomizer.force_type(unit_type.dcs_id)
self.group_template.set_enabled(self.group_selector.isChecked())
if self.group_template.can_be_modified:
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
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)
@ -143,8 +150,6 @@ class QGroundObjectTemplateLayout(QGroupBox):
def update_price(self) -> None:
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]")
def buy_group(self):
@ -216,30 +221,40 @@ class QGroundObjectBuyMenu(QDialog):
self.mainLayout = QGridLayout()
self.setLayout(self.mainLayout)
self.unit_group_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
template_sub_category = None
tasks = []
if isinstance(ground_object, SamGroundObject):
template_category = TemplateCategory.AirDefence
role = GroupRole.AntiAir
elif isinstance(ground_object, VehicleGroupGroundObject):
template_category = TemplateCategory.Armor
role = GroupRole.GroundForce
elif isinstance(ground_object, EwrGroundObject):
template_category = TemplateCategory.AirDefence
template_sub_category = "EWR"
role = GroupRole.AntiAir
tasks.append(GroupTask.EWR)
else:
raise RuntimeError
for template in game.blue.faction.templates.for_category(
template_category, template_sub_category
):
self.template_selector.addItem(template.name, userData=template)
if not tasks:
tasks = ROLE_TASKINGS[role]
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.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft)
template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
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)
@ -250,10 +265,22 @@ class QGroundObjectBuyMenu(QDialog):
self.setLayout(self.mainLayout)
# 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):
template = self.template_selector.itemData(
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.config import REWARDS
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.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
@ -181,8 +181,11 @@ class QGroundObjectMenu(QDialog):
return
for u in self.ground_object.units:
# Hack: Unknown variant.
unit_type = next(GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type]))
total_value += unit_type.price
if u.type in vehicles.vehicle_map:
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:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value

View File

@ -52,18 +52,9 @@
"sams": [
"HawkGenerator"
],
"aircraft_carrier": [
"Stennis"
],
"helicopter_carrier": [
"LHA_Tarawa"
],
"destroyers": [
"PERRY",
"USS_Arleigh_Burke_IIa"
],
"cruisers": [
"TICONDEROG"
"naval_units": [
"LHA-1 Tarawa",
"CVN-74 John C. Stennis"
],
"requirements": {"mod": "Some mod is required"},
"carrier_names": [
@ -79,10 +70,6 @@
"LHA-4 Nassau",
"LHA-5 Peleliu"
],
"navy_generators": [
"OliverHazardPerryGroupGenerator",
"ArleighBurkeGroupGenerator"
],
"has_jtac": true,
"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_M249, faction.infantry_units)
self.assertIn("AvengerGenerator", faction.air_defenses)
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(Stennis.name, faction.naval_units)
self.assertIn(LHA_Tarawa.name, faction.naval_units)
self.assertIn("mod", faction.requirements.keys())
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(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")
def test_load_valid_faction_with_invalid_country(self) -> None: