mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
daf4704fe7
commit
60c8c80480
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@ -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
63
game/data/groups.py
Normal 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
40
game/data/units.py
Normal 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
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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
74
game/dcs/shipunittype.py
Normal 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
157
game/dcs/unitgroup.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
@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 (
|
||||
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,
|
||||
(
|
||||
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
|
||||
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
|
||||
),
|
||||
)
|
||||
or template.template_type == "EWR"
|
||||
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,
|
||||
)
|
||||
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
|
||||
):
|
||||
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)
|
||||
# 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)
|
||||
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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -22,7 +22,6 @@ from typing import (
|
||||
TypeVar,
|
||||
List,
|
||||
Any,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs import Mission, Point, unitgroup
|
||||
@ -110,53 +109,26 @@ 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):
|
||||
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(u)
|
||||
self.add_trigger_zone_for_scenery(unit)
|
||||
continue
|
||||
|
||||
# Only skip dead units after trigger zone for scenery created!
|
||||
if not u.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,
|
||||
)
|
||||
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:
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
if issubclass(unit_type, VehicleType):
|
||||
moving_group = self.m.vehicle_group(
|
||||
@ -168,7 +140,7 @@ class GroundObjectGenerator:
|
||||
)
|
||||
moving_group.units[0].player_can_drive = True
|
||||
self.enable_eplrs(moving_group, unit_type)
|
||||
if issubclass(unit_type, ShipType):
|
||||
elif issubclass(unit_type, ShipType):
|
||||
moving_group = self.m.ship_group(
|
||||
self.country,
|
||||
group_name,
|
||||
@ -176,6 +148,18 @@ class GroundObjectGenerator:
|
||||
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)
|
||||
@ -211,7 +195,7 @@ class GroundObjectGenerator:
|
||||
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,9 +507,6 @@ 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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
if u.unit_type and u.unit_type.unit_class in [
|
||||
UnitClass.AircraftCarrier,
|
||||
UnitClass.HelicopterCarrier,
|
||||
]:
|
||||
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 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()
|
||||
|
||||
@ -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()
|
||||
|
||||
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,109 +288,82 @@ 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()
|
||||
|
||||
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])})"
|
||||
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
|
||||
)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
@ -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
|
||||
),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
808
gen/templates.py
808
gen/templates.py
File diff suppressed because it is too large
Load Diff
@ -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,16 +68,20 @@ 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():
|
||||
try:
|
||||
unit_name = next(self.group_template.possible_units)
|
||||
except StopIteration:
|
||||
unit_name = self.group_template.unit_type
|
||||
group_info.addWidget(
|
||||
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft
|
||||
QLabel(f"{self.group_template.size}x {unit_name}"),
|
||||
alignment=Qt.AlignLeft,
|
||||
)
|
||||
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
|
||||
|
||||
@ -86,10 +92,11 @@ class QGroundObjectGroupTemplate(QGroupBox):
|
||||
self.group_selector.stateChanged.connect(self.on_group_changed)
|
||||
|
||||
def on_group_changed(self) -> None:
|
||||
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())
|
||||
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.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()
|
||||
)
|
||||
if template is not None:
|
||||
self.template_changed_signal.emit(template)
|
||||
|
||||
@ -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,7 +181,10 @@ 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]))
|
||||
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)")
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user