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 dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.utils import Distance, feet, nautical_miles
|
from game.utils import Distance, feet, nautical_miles
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GroundUnitProcurementRatios:
|
class GroundUnitProcurementRatios:
|
||||||
ratios: dict[GroundUnitClass, float]
|
ratios: dict[UnitClass, float]
|
||||||
|
|
||||||
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
|
def for_unit_class(self, unit_class: UnitClass) -> float:
|
||||||
try:
|
try:
|
||||||
return self.ratios[unit_class] / sum(self.ratios.values())
|
return self.ratios[unit_class] / sum(self.ratios.values())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(60),
|
sweep_distance=nautical_miles(60),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
GroundUnitClass.Tank: 3,
|
UnitClass.Tank: 3,
|
||||||
GroundUnitClass.Atgm: 2,
|
UnitClass.Atgm: 2,
|
||||||
GroundUnitClass.Apc: 2,
|
UnitClass.Apc: 2,
|
||||||
GroundUnitClass.Ifv: 3,
|
UnitClass.Ifv: 3,
|
||||||
GroundUnitClass.Artillery: 1,
|
UnitClass.Artillery: 1,
|
||||||
GroundUnitClass.Shorads: 2,
|
UnitClass.SHORAD: 2,
|
||||||
GroundUnitClass.Recon: 1,
|
UnitClass.Recon: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(40),
|
sweep_distance=nautical_miles(40),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
GroundUnitClass.Tank: 4,
|
UnitClass.Tank: 4,
|
||||||
GroundUnitClass.Atgm: 2,
|
UnitClass.Atgm: 2,
|
||||||
GroundUnitClass.Apc: 3,
|
UnitClass.Apc: 3,
|
||||||
GroundUnitClass.Ifv: 2,
|
UnitClass.Ifv: 2,
|
||||||
GroundUnitClass.Artillery: 1,
|
UnitClass.Artillery: 1,
|
||||||
GroundUnitClass.Shorads: 2,
|
UnitClass.SHORAD: 2,
|
||||||
GroundUnitClass.Recon: 1,
|
UnitClass.Recon: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(10),
|
sweep_distance=nautical_miles(10),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
GroundUnitClass.Tank: 3,
|
UnitClass.Tank: 3,
|
||||||
GroundUnitClass.Atgm: 3,
|
UnitClass.Atgm: 3,
|
||||||
GroundUnitClass.Apc: 3,
|
UnitClass.Apc: 3,
|
||||||
GroundUnitClass.Artillery: 1,
|
UnitClass.Artillery: 1,
|
||||||
GroundUnitClass.Shorads: 3,
|
UnitClass.SHORAD: 3,
|
||||||
GroundUnitClass.Recon: 1,
|
UnitClass.Recon: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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.planes import plane_map
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
|
from game.data.units import UnitClass
|
||||||
from game.dcs.unitproperty import UnitProperty
|
from game.dcs.unitproperty import UnitProperty
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
from game.radio.channels import (
|
from game.radio.channels import (
|
||||||
@ -180,19 +181,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
channel_allocator: Optional[RadioChannelAllocator]
|
channel_allocator: Optional[RadioChannelAllocator]
|
||||||
channel_namer: Type[ChannelNamer]
|
channel_namer: Type[ChannelNamer]
|
||||||
|
|
||||||
_by_name: ClassVar[dict[str, AircraftType]] = {}
|
|
||||||
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
|
|
||||||
list
|
|
||||||
)
|
|
||||||
_loaded: ClassVar[bool] = False
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dcs_id(self) -> str:
|
|
||||||
return self.dcs_unit_type.id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def flyable(self) -> bool:
|
def flyable(self) -> bool:
|
||||||
return self.dcs_unit_type.flyable
|
return self.dcs_unit_type.flyable
|
||||||
@ -309,35 +297,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
state.update(updated.__dict__)
|
state.update(updated.__dict__)
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register(cls, aircraft_type: AircraftType) -> None:
|
|
||||||
cls._by_name[aircraft_type.name] = aircraft_type
|
|
||||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def named(cls, name: str) -> AircraftType:
|
def named(cls, name: str) -> AircraftType:
|
||||||
if not cls._loaded:
|
if not cls._loaded:
|
||||||
cls._load_all()
|
cls._load_all()
|
||||||
return cls._by_name[name]
|
unit = cls._by_name[name]
|
||||||
|
assert isinstance(unit, AircraftType)
|
||||||
|
return unit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||||
if not cls._loaded:
|
if not cls._loaded:
|
||||||
cls._load_all()
|
cls._load_all()
|
||||||
yield from cls._by_unit_type[dcs_unit_type]
|
for unit in cls._by_unit_type[dcs_unit_type]:
|
||||||
|
assert isinstance(unit, AircraftType)
|
||||||
|
yield unit
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _each_unit_type() -> Iterator[Type[FlyingType]]:
|
def _each_unit_type() -> Iterator[Type[FlyingType]]:
|
||||||
yield from helicopter_map.values()
|
yield from helicopter_map.values()
|
||||||
yield from plane_map.values()
|
yield from plane_map.values()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load_all(cls) -> None:
|
|
||||||
for unit_type in cls._each_unit_type():
|
|
||||||
for data in cls._each_variant_of(unit_type):
|
|
||||||
cls.register(data)
|
|
||||||
cls._loaded = True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||||
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
||||||
@ -417,4 +397,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
channel_namer=radio_config.channel_namer,
|
channel_namer=radio_config.channel_namer,
|
||||||
kneeboard_units=units,
|
kneeboard_units=units,
|
||||||
utc_kneeboard=data.get("utc_kneeboard", False),
|
utc_kneeboard=data.get("utc_kneeboard", False),
|
||||||
|
unit_class=UnitClass.Plane,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,65 +1,42 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Type, Optional, ClassVar, Iterator
|
from typing import Type, Optional, Iterator
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
from dcs.vehicles import vehicle_map
|
from dcs.vehicles import vehicle_map
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundUnitType(UnitType[Type[VehicleType]]):
|
class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||||
unit_class: Optional[GroundUnitClass]
|
|
||||||
spawn_weight: int
|
spawn_weight: int
|
||||||
|
|
||||||
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
|
|
||||||
_by_unit_type: ClassVar[
|
|
||||||
dict[Type[VehicleType], list[GroundUnitType]]
|
|
||||||
] = defaultdict(list)
|
|
||||||
_loaded: ClassVar[bool] = False
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dcs_id(self) -> str:
|
|
||||||
return self.dcs_unit_type.id
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register(cls, aircraft_type: GroundUnitType) -> None:
|
|
||||||
cls._by_name[aircraft_type.name] = aircraft_type
|
|
||||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def named(cls, name: str) -> GroundUnitType:
|
def named(cls, name: str) -> GroundUnitType:
|
||||||
if not cls._loaded:
|
if not cls._loaded:
|
||||||
cls._load_all()
|
cls._load_all()
|
||||||
return cls._by_name[name]
|
unit = cls._by_name[name]
|
||||||
|
assert isinstance(unit, GroundUnitType)
|
||||||
|
return unit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||||
if not cls._loaded:
|
if not cls._loaded:
|
||||||
cls._load_all()
|
cls._load_all()
|
||||||
yield from cls._by_unit_type[dcs_unit_type]
|
for unit in cls._by_unit_type[dcs_unit_type]:
|
||||||
|
assert isinstance(unit, GroundUnitType)
|
||||||
|
yield unit
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _each_unit_type() -> Iterator[Type[VehicleType]]:
|
def _each_unit_type() -> Iterator[Type[VehicleType]]:
|
||||||
yield from vehicle_map.values()
|
yield from vehicle_map.values()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load_all(cls) -> None:
|
|
||||||
for unit_type in cls._each_unit_type():
|
|
||||||
for data in cls._each_variant_of(unit_type):
|
|
||||||
cls.register(data)
|
|
||||||
cls._loaded = True
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||||
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
|
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
|
||||||
@ -78,9 +55,8 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
|
|||||||
introduction = "No data."
|
introduction = "No data."
|
||||||
|
|
||||||
class_name = data.get("class")
|
class_name = data.get("class")
|
||||||
unit_class: Optional[GroundUnitClass] = None
|
# TODO Exception handling for missing classes
|
||||||
if class_name is not None:
|
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
|
||||||
unit_class = GroundUnitClass(class_name)
|
|
||||||
|
|
||||||
for variant in data.get("variants", [vehicle.id]):
|
for variant in data.get("variants", [vehicle.id]):
|
||||||
yield GroundUnitType(
|
yield GroundUnitType(
|
||||||
|
|||||||
74
game/dcs/shipunittype.py
Normal file
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 dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TypeVar, Generic, Type
|
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
|
||||||
|
|
||||||
from dcs.unittype import UnitType as DcsUnitType
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
|
from game.data.units import UnitClass
|
||||||
|
|
||||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UnitType(Generic[DcsUnitTypeT]):
|
class UnitType(ABC, Generic[DcsUnitTypeT]):
|
||||||
dcs_unit_type: DcsUnitTypeT
|
dcs_unit_type: DcsUnitTypeT
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
@ -17,10 +23,47 @@ class UnitType(Generic[DcsUnitTypeT]):
|
|||||||
manufacturer: str
|
manufacturer: str
|
||||||
role: str
|
role: str
|
||||||
price: int
|
price: int
|
||||||
|
unit_class: UnitClass
|
||||||
|
|
||||||
|
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
|
||||||
|
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
|
||||||
|
_loaded: ClassVar[bool] = False
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dcs_id(self) -> str:
|
||||||
|
return self.dcs_unit_type.id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, unit_type: UnitType[Any]) -> None:
|
||||||
|
cls._by_name[unit_type.name] = unit_type
|
||||||
|
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def named(cls, name: str) -> UnitType[Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _each_unit_type() -> Iterator[DcsUnitTypeT]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _load_all(cls) -> None:
|
||||||
|
for unit_type in cls._each_unit_type():
|
||||||
|
for data in cls._each_variant_of(unit_type):
|
||||||
|
cls.register(data)
|
||||||
|
cls._loaded = True
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def eplrs_capable(self) -> bool:
|
def eplrs_capable(self) -> bool:
|
||||||
return getattr(self.dcs_unit_type, "eplrs", False)
|
return getattr(self.dcs_unit_type, "eplrs", False)
|
||||||
|
|||||||
@ -3,12 +3,13 @@ from __future__ import annotations
|
|||||||
import copy
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
import dcs
|
import dcs
|
||||||
from dcs.countries import country_dict
|
from dcs.countries import country_dict
|
||||||
from dcs.unittype import ShipType, UnitType
|
from dcs.unittype import ShipType
|
||||||
|
|
||||||
from game.data.building_data import (
|
from game.data.building_data import (
|
||||||
WW2_ALLIES_BUILDINGS,
|
WW2_ALLIES_BUILDINGS,
|
||||||
@ -22,10 +23,19 @@ from game.data.doctrine import (
|
|||||||
COLDWAR_DOCTRINE,
|
COLDWAR_DOCTRINE,
|
||||||
WWII_DOCTRINE,
|
WWII_DOCTRINE,
|
||||||
)
|
)
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game import db
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from gen.templates import GroundObjectTemplates, TemplateCategory
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
|
from game.dcs.unitgroup import UnitGroup
|
||||||
|
from game.dcs.unittype import UnitType
|
||||||
|
from gen.templates import (
|
||||||
|
GroundObjectTemplates,
|
||||||
|
GroundObjectTemplate,
|
||||||
|
GroupTemplate,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.theater.start_generator import ModSettings
|
from game.theater.start_generator import ModSettings
|
||||||
@ -70,50 +80,26 @@ class Faction:
|
|||||||
# Logistics units used
|
# Logistics units used
|
||||||
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible SAMS site generators for this faction
|
# Possible Air Defence units, Like EWRs
|
||||||
air_defenses: List[str] = field(default_factory=list)
|
air_defense_units: List[GroundUnitType] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible EWR generators for this faction.
|
# A list of all supported sets of units
|
||||||
ewrs: List[GroundUnitType] = field(default_factory=list)
|
preset_groups: list[UnitGroup] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible Missile site generators for this faction
|
# Possible Missile site generators for this faction
|
||||||
missiles: List[str] = field(default_factory=list)
|
missiles: List[GroundUnitType] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible costal site generators for this faction
|
|
||||||
coastal_defenses: List[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Required mods or asset packs
|
# Required mods or asset packs
|
||||||
requirements: Dict[str, str] = field(default_factory=dict)
|
requirements: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
# possible aircraft carrier units
|
|
||||||
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
|
|
||||||
|
|
||||||
# possible helicopter carrier units
|
|
||||||
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Possible carrier names
|
# Possible carrier names
|
||||||
carrier_names: List[str] = field(default_factory=list)
|
carrier_names: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible helicopter carrier names
|
# Possible helicopter carrier names
|
||||||
helicopter_carrier_names: List[str] = field(default_factory=list)
|
helicopter_carrier_names: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# Navy group generators
|
# Available Naval Units
|
||||||
navy_generators: List[str] = field(default_factory=list)
|
naval_units: List[ShipUnitType] = field(default_factory=list)
|
||||||
|
|
||||||
# Available destroyers
|
|
||||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Available cruisers
|
|
||||||
cruisers: List[Type[ShipType]] = field(default_factory=list)
|
|
||||||
|
|
||||||
# How many navy group should we try to generate per CP on startup for this faction
|
|
||||||
navy_group_count: int = field(default=1)
|
|
||||||
|
|
||||||
# How many missiles group should we try to generate per CP on startup for this faction
|
|
||||||
missiles_group_count: int = field(default=1)
|
|
||||||
|
|
||||||
# How many coastal group should we try to generate per CP on startup for this faction
|
|
||||||
coastal_group_count: int = field(default=1)
|
|
||||||
|
|
||||||
# Whether this faction has JTAC access
|
# Whether this faction has JTAC access
|
||||||
has_jtac: bool = field(default=False)
|
has_jtac: bool = field(default=False)
|
||||||
@ -124,7 +110,7 @@ class Faction:
|
|||||||
# doctrine
|
# doctrine
|
||||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||||
|
|
||||||
# List of available buildings for this faction
|
# List of available building templates for this faction
|
||||||
building_set: List[str] = field(default_factory=list)
|
building_set: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# List of default livery overrides
|
# List of default livery overrides
|
||||||
@ -142,64 +128,183 @@ class Faction:
|
|||||||
# All possible templates which can be generated by the faction
|
# All possible templates which can be generated by the faction
|
||||||
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
||||||
|
|
||||||
|
# All available unit_groups
|
||||||
|
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
# Save all accessible units for performance increase
|
||||||
|
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
|
||||||
|
|
||||||
def __getitem__(self, item: str) -> Any:
|
def __getitem__(self, item: str) -> Any:
|
||||||
return getattr(self, item)
|
return getattr(self, item)
|
||||||
|
|
||||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
@property
|
||||||
# Supports all GroundUnit lists and AirDefenses
|
def accessible_units(self) -> Iterator[UnitType[Any]]:
|
||||||
for unit in self.ground_units:
|
yield from self._accessible_units
|
||||||
if unit_type == unit.dcs_id:
|
|
||||||
return True
|
|
||||||
return unit_type in self.air_defenses
|
|
||||||
|
|
||||||
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
|
@property
|
||||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
def air_defenses(self) -> list[str]:
|
||||||
if vehicle.unit_class is unit_class:
|
"""Returns the Air Defense types"""
|
||||||
return True
|
air_defenses = [a.name for a in self.air_defense_units]
|
||||||
|
air_defenses.extend(
|
||||||
|
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
|
||||||
|
)
|
||||||
|
return sorted(air_defenses)
|
||||||
|
|
||||||
|
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
||||||
|
# GroundUnits
|
||||||
|
if any(unit_type == u.dcs_id for u in self.accessible_units):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Statics
|
||||||
|
if db.static_type_from_name(unit_type) is not None:
|
||||||
|
# TODO Improve the statics checking
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
|
||||||
# This loads all faction possible sam templates and the default ones
|
return any(unit.unit_class is unit_class for unit in self.accessible_units)
|
||||||
# For legacy reasons this allows to check for template names. This can be
|
|
||||||
# improved in the future to have more control about the possible Templates.
|
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
|
||||||
# For example it can be possible to define the unit_types and check if all
|
self._accessible_units = []
|
||||||
# requirements for the template are fulfilled.
|
all_units: Iterator[UnitType[Any]] = itertools.chain(
|
||||||
for category, template in all_templates.templates:
|
self.ground_units,
|
||||||
if (
|
self.infantry_units,
|
||||||
(
|
self.air_defense_units,
|
||||||
category == TemplateCategory.AirDefence
|
self.naval_units,
|
||||||
and (
|
self.missiles,
|
||||||
# Check if faction has the template name or ALL required
|
(
|
||||||
# unit_types in the list air_defenses. For legacy reasons this
|
ground_unit
|
||||||
# allows both and also the EWR template
|
for preset_group in self.preset_groups
|
||||||
template.name in self.air_defenses
|
for ground_unit in preset_group.ground_units
|
||||||
or all(
|
),
|
||||||
self.has_access_to_unit_type(required_unit)
|
(
|
||||||
for required_unit in template.required_units
|
ship_unit
|
||||||
)
|
for preset_group in self.preset_groups
|
||||||
or template.template_type == "EWR"
|
for ship_unit in preset_group.ship_units
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
or template.name in self.navy_generators
|
for unit in all_units:
|
||||||
or template.name in self.missiles
|
if unit not in self._accessible_units:
|
||||||
or template.name in self.coastal_defenses
|
self._accessible_units.append(unit)
|
||||||
or (
|
|
||||||
template.template_type
|
def initialize(
|
||||||
in self.building_set + ["fob", "ammo", "factory"]
|
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
|
||||||
)
|
) -> None:
|
||||||
or (template.template_type == "carrier" and self.aircraft_carrier)
|
# Apply the mod settings
|
||||||
or (template.template_type == "lha" and self.helicopter_carrier)
|
self._apply_mod_settings(mod_settings)
|
||||||
or category == TemplateCategory.Armor
|
# Load all accessible units and store them for performant later usage
|
||||||
):
|
self._load_accessible_units(all_templates)
|
||||||
# Make a deep copy of a template and add it to the template_list.
|
# Load all faction compatible templates
|
||||||
# This is required to have faction independent templates. Otherwise
|
self._load_templates(all_templates)
|
||||||
# the reference would be the same and changes would affect all.
|
# Load Unit Groups
|
||||||
faction_template = copy.deepcopy(template)
|
self._load_unit_groups()
|
||||||
# Initialize all randomizers
|
|
||||||
for group_template in faction_template.groups:
|
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
|
||||||
if group_template.randomizer:
|
if not unit_group.templates:
|
||||||
group_template.randomizer.init_randomization_for_faction(self)
|
unit_group.load_templates(self)
|
||||||
self.templates.add_template(category, faction_template)
|
if not unit_group.templates:
|
||||||
|
# Empty templates will throw an error on generation
|
||||||
|
logging.error(
|
||||||
|
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if unit_group.role in self.unit_groups:
|
||||||
|
for group in self.unit_groups[unit_group.role]:
|
||||||
|
if merge and all(task in group.tasks for task in unit_group.tasks):
|
||||||
|
# Update existing group if same tasking
|
||||||
|
group.update_from_unit_group(unit_group)
|
||||||
|
return
|
||||||
|
# Add new Unit_group
|
||||||
|
self.unit_groups[unit_group.role].append(unit_group)
|
||||||
|
else:
|
||||||
|
self.unit_groups[unit_group.role] = [unit_group]
|
||||||
|
|
||||||
|
def _load_unit_groups(self) -> None:
|
||||||
|
# This function will create all the UnitGroups for the faction
|
||||||
|
# It will create a unit group for each global Template, Building or
|
||||||
|
# Legacy supported templates (not yet migrated from the generators).
|
||||||
|
# For every preset_group there will be a separate UnitGroup so no mixed
|
||||||
|
# UnitGroups will be generated for them. Special groups like complex SAM Systems
|
||||||
|
self.unit_groups = {}
|
||||||
|
|
||||||
|
# Generate UnitGroups for all global templates
|
||||||
|
for role, template in self.templates.templates:
|
||||||
|
if template.generic or role == GroupRole.Building:
|
||||||
|
# Build groups for global templates and buildings
|
||||||
|
self._add_group_for_template(role, template)
|
||||||
|
|
||||||
|
# Add preset groups
|
||||||
|
for preset_group in self.preset_groups:
|
||||||
|
# Add as separate group, do not merge with generic groups!
|
||||||
|
self._add_unit_group(preset_group, False)
|
||||||
|
|
||||||
|
def _add_group_for_template(
|
||||||
|
self, role: GroupRole, template: GroundObjectTemplate
|
||||||
|
) -> None:
|
||||||
|
unit_group = UnitGroup(
|
||||||
|
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
|
||||||
|
[u for u in template.units if isinstance(u, GroundUnitType)],
|
||||||
|
[u for u in template.units if isinstance(u, ShipUnitType)],
|
||||||
|
list(template.statics),
|
||||||
|
role,
|
||||||
|
)
|
||||||
|
unit_group.tasks = template.tasks
|
||||||
|
unit_group.set_templates([template])
|
||||||
|
self._add_unit_group(unit_group)
|
||||||
|
|
||||||
|
def initialize_group_template(
|
||||||
|
self, group: GroupTemplate, faction_sensitive: bool = True
|
||||||
|
) -> bool:
|
||||||
|
# Sensitive defines if the initialization should check if the unit is available
|
||||||
|
# to this faction or not. It is disabled for migration only atm.
|
||||||
|
unit_types = [
|
||||||
|
t
|
||||||
|
for t in group.unit_types
|
||||||
|
if not faction_sensitive or self.has_access_to_unit_type(t)
|
||||||
|
]
|
||||||
|
|
||||||
|
alternative_types = []
|
||||||
|
for accessible_unit in self.accessible_units:
|
||||||
|
if accessible_unit.unit_class in group.unit_classes:
|
||||||
|
unit_types.append(accessible_unit.dcs_id)
|
||||||
|
if accessible_unit.unit_class in group.alternative_classes:
|
||||||
|
alternative_types.append(accessible_unit.dcs_id)
|
||||||
|
|
||||||
|
if not unit_types and not alternative_types and not group.optional:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
types = unit_types or alternative_types
|
||||||
|
group.set_possible_types(types)
|
||||||
|
return len(types) > 0
|
||||||
|
|
||||||
|
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
||||||
|
self.templates = GroundObjectTemplates()
|
||||||
|
# This loads all templates which are usable by the faction
|
||||||
|
for role, template in all_templates.templates:
|
||||||
|
# Make a deep copy of a template and add it to the template_list.
|
||||||
|
# This is required to have faction independent templates. Otherwise
|
||||||
|
# the reference would be the same and changes would affect all.
|
||||||
|
faction_template = copy.deepcopy(template)
|
||||||
|
try:
|
||||||
|
faction_template.groups[:] = [
|
||||||
|
group_template
|
||||||
|
for group_template in faction_template.groups
|
||||||
|
if self.initialize_group_template(group_template)
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
role == GroupRole.Building
|
||||||
|
and GroupTask.StrikeTarget in template.tasks
|
||||||
|
and faction_template.category not in self.building_set
|
||||||
|
):
|
||||||
|
# Special handling for strike targets. Skip if not supported by faction
|
||||||
|
continue
|
||||||
|
if faction_template.groups:
|
||||||
|
self.templates.add_template(role, faction_template)
|
||||||
|
continue
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.info(f"{self.name} can not use template {template.name}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||||
@ -240,34 +345,30 @@ class Faction:
|
|||||||
faction.logistics_units = [
|
faction.logistics_units = [
|
||||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||||
]
|
]
|
||||||
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])]
|
faction.air_defense_units = [
|
||||||
|
GroundUnitType.named(n) for n in json.get("air_defense_units", [])
|
||||||
|
]
|
||||||
|
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
|
||||||
|
|
||||||
faction.air_defenses = json.get("air_defenses", [])
|
faction.preset_groups = [
|
||||||
# Compatibility for older factions. All air defenses now belong to a
|
UnitGroup.named(n) for n in json.get("preset_groups", [])
|
||||||
# single group and the generator decides what belongs where.
|
]
|
||||||
faction.air_defenses.extend(json.get("sams", []))
|
|
||||||
faction.air_defenses.extend(json.get("shorads", []))
|
|
||||||
|
|
||||||
faction.missiles = json.get("missiles", [])
|
|
||||||
faction.coastal_defenses = json.get("coastal_defenses", [])
|
|
||||||
faction.requirements = json.get("requirements", {})
|
faction.requirements = json.get("requirements", {})
|
||||||
|
|
||||||
faction.carrier_names = json.get("carrier_names", [])
|
faction.carrier_names = json.get("carrier_names", [])
|
||||||
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||||
faction.navy_generators = json.get("navy_generators", [])
|
|
||||||
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
|
faction.naval_units = [
|
||||||
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
|
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
||||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
]
|
||||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
|
||||||
faction.has_jtac = json.get("has_jtac", False)
|
faction.has_jtac = json.get("has_jtac", False)
|
||||||
jtac_name = json.get("jtac_unit", None)
|
jtac_name = json.get("jtac_unit", None)
|
||||||
if jtac_name is not None:
|
if jtac_name is not None:
|
||||||
faction.jtac_unit = AircraftType.named(jtac_name)
|
faction.jtac_unit = AircraftType.named(jtac_name)
|
||||||
else:
|
else:
|
||||||
faction.jtac_unit = None
|
faction.jtac_unit = None
|
||||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
|
||||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
|
||||||
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
|
||||||
|
|
||||||
# Load doctrine
|
# Load doctrine
|
||||||
doctrine = json.get("doctrine", "modern")
|
doctrine = json.get("doctrine", "modern")
|
||||||
@ -304,6 +405,7 @@ class Faction:
|
|||||||
|
|
||||||
# Templates
|
# Templates
|
||||||
faction.templates = GroundObjectTemplates()
|
faction.templates = GroundObjectTemplates()
|
||||||
|
|
||||||
return faction
|
return faction
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -312,14 +414,49 @@ class Faction:
|
|||||||
yield from self.frontline_units
|
yield from self.frontline_units
|
||||||
yield from self.logistics_units
|
yield from self.logistics_units
|
||||||
|
|
||||||
def infantry_with_class(
|
def infantry_with_class(self, unit_class: UnitClass) -> Iterator[GroundUnitType]:
|
||||||
self, unit_class: GroundUnitClass
|
|
||||||
) -> Iterator[GroundUnitType]:
|
|
||||||
for unit in self.infantry_units:
|
for unit in self.infantry_units:
|
||||||
if unit.unit_class is unit_class:
|
if unit.unit_class is unit_class:
|
||||||
yield unit
|
yield unit
|
||||||
|
|
||||||
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
|
def groups_for_role_and_task(
|
||||||
|
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
|
||||||
|
) -> list[UnitGroup]:
|
||||||
|
if group_role not in self.unit_groups:
|
||||||
|
return []
|
||||||
|
groups = []
|
||||||
|
for unit_group in self.unit_groups[group_role]:
|
||||||
|
if not group_task or group_task in unit_group.tasks:
|
||||||
|
groups.append(unit_group)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def groups_for_role_and_tasks(
|
||||||
|
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||||
|
) -> list[UnitGroup]:
|
||||||
|
groups = []
|
||||||
|
for task in tasks:
|
||||||
|
for group in self.groups_for_role_and_task(group_role, task):
|
||||||
|
if group not in groups:
|
||||||
|
groups.append(group)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
|
||||||
|
unit_groups = self.groups_for_role_and_task(group_role)
|
||||||
|
return random.choice(unit_groups) if unit_groups else None
|
||||||
|
|
||||||
|
def random_group_for_role_and_task(
|
||||||
|
self, group_role: GroupRole, group_task: GroupTask
|
||||||
|
) -> Optional[UnitGroup]:
|
||||||
|
unit_groups = self.groups_for_role_and_task(group_role, group_task)
|
||||||
|
return random.choice(unit_groups) if unit_groups else None
|
||||||
|
|
||||||
|
def random_group_for_role_and_tasks(
|
||||||
|
self, group_role: GroupRole, tasks: list[GroupTask]
|
||||||
|
) -> Optional[UnitGroup]:
|
||||||
|
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
|
||||||
|
return random.choice(unit_groups) if unit_groups else None
|
||||||
|
|
||||||
|
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||||
# aircraft
|
# aircraft
|
||||||
if not mod_settings.a4_skyhawk:
|
if not mod_settings.a4_skyhawk:
|
||||||
self.remove_aircraft("A-4E-C")
|
self.remove_aircraft("A-4E-C")
|
||||||
@ -379,24 +516,23 @@ class Faction:
|
|||||||
self.remove_vehicle("KORNET")
|
self.remove_vehicle("KORNET")
|
||||||
# high digit sams
|
# high digit sams
|
||||||
if not mod_settings.high_digit_sams:
|
if not mod_settings.high_digit_sams:
|
||||||
self.remove_air_defenses("SA-10B/S-300PS Battery")
|
self.remove_presets("SA-10B/S-300PS")
|
||||||
self.remove_air_defenses("SA-12/S-300V Battery")
|
self.remove_presets("SA-12/S-300V")
|
||||||
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
|
self.remove_presets("SA-20/S-300PMU-1")
|
||||||
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
|
self.remove_presets("SA-20B/S-300PMU-2")
|
||||||
self.remove_air_defenses("SA-23/S-300VM Battery")
|
self.remove_presets("SA-23/S-300VM")
|
||||||
self.remove_air_defenses("SA-17 Grizzly Battery")
|
self.remove_presets("SA-17")
|
||||||
self.remove_air_defenses("KS-19 AAA Site")
|
self.remove_presets("KS-19")
|
||||||
return self
|
|
||||||
|
|
||||||
def remove_aircraft(self, name: str) -> None:
|
def remove_aircraft(self, name: str) -> None:
|
||||||
for i in self.aircrafts:
|
for i in self.aircrafts:
|
||||||
if i.dcs_unit_type.id == name:
|
if i.dcs_unit_type.id == name:
|
||||||
self.aircrafts.remove(i)
|
self.aircrafts.remove(i)
|
||||||
|
|
||||||
def remove_air_defenses(self, name: str) -> None:
|
def remove_presets(self, name: str) -> None:
|
||||||
for i in self.air_defenses:
|
for pg in self.preset_groups:
|
||||||
if i == name:
|
if pg.name == name:
|
||||||
self.air_defenses.remove(i)
|
self.preset_groups.remove(pg)
|
||||||
|
|
||||||
def remove_vehicle(self, name: str) -> None:
|
def remove_vehicle(self, name: str) -> None:
|
||||||
for i in self.frontline_units:
|
for i in self.frontline_units:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
|||||||
target = self.package.target
|
target = self.package.target
|
||||||
if isinstance(target, TheaterGroundObject):
|
if isinstance(target, TheaterGroundObject):
|
||||||
for group in target.groups:
|
for group in target.groups:
|
||||||
group_names.append(group.name)
|
group_names.append(group.group_name)
|
||||||
elif isinstance(target, MultiGroupTransport):
|
elif isinstance(target, MultiGroupTransport):
|
||||||
group_names.append(target.name)
|
group_names.append(target.name)
|
||||||
elif isinstance(target, NavalControlPoint):
|
elif isinstance(target, NavalControlPoint):
|
||||||
|
|||||||
@ -20,9 +20,11 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for group in target.groups:
|
for group in target.groups:
|
||||||
miz_group = self.mission.find_group(group.name)
|
miz_group = self.mission.find_group(group.group_name)
|
||||||
if miz_group is None:
|
if miz_group is None:
|
||||||
logging.error(f"Could not find group for DEAD mission {group.name}")
|
logging.error(
|
||||||
|
f"Could not find group for DEAD mission {group.group_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
|
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
|
||||||
|
|||||||
@ -20,9 +20,11 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for group in target.groups:
|
for group in target.groups:
|
||||||
miz_group = self.mission.find_group(group.name)
|
miz_group = self.mission.find_group(group.group_name)
|
||||||
if miz_group is None:
|
if miz_group is None:
|
||||||
logging.error(f"Could not find group for SEAD mission {group.name}")
|
logging.error(
|
||||||
|
f"Could not find group for SEAD mission {group.group_name}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided)
|
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided)
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from dcs.triggers import Event, TriggerOnce
|
|||||||
from dcs.unit import Vehicle, Skill
|
from dcs.unit import Vehicle, Skill
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import ControlPoint
|
||||||
@ -221,7 +221,7 @@ class FlotGenerator:
|
|||||||
if self.game.settings.manpads:
|
if self.game.settings.manpads:
|
||||||
# 50% of armored units protected by manpad
|
# 50% of armored units protected by manpad
|
||||||
if random.choice([True, False]):
|
if random.choice([True, False]):
|
||||||
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads))
|
manpads = list(faction.infantry_with_class(UnitClass.Manpad))
|
||||||
if manpads:
|
if manpads:
|
||||||
u = random.choices(
|
u = random.choices(
|
||||||
manpads, weights=[m.spawn_weight for m in manpads]
|
manpads, weights=[m.spawn_weight for m in manpads]
|
||||||
@ -237,12 +237,10 @@ class FlotGenerator:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
possible_infantry_units = set(
|
possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry))
|
||||||
faction.infantry_with_class(GroundUnitClass.Infantry)
|
|
||||||
)
|
|
||||||
if self.game.settings.manpads:
|
if self.game.settings.manpads:
|
||||||
possible_infantry_units |= set(
|
possible_infantry_units |= set(
|
||||||
faction.infantry_with_class(GroundUnitClass.Manpads)
|
faction.infantry_with_class(UnitClass.Manpad)
|
||||||
)
|
)
|
||||||
if not possible_infantry_units:
|
if not possible_infantry_units:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -118,7 +118,7 @@ class LuaGenerator:
|
|||||||
|
|
||||||
faction = "BlueAA" if cp.captured else "RedAA"
|
faction = "BlueAA" if cp.captured else "RedAA"
|
||||||
|
|
||||||
lua_data[faction][g.name] = {
|
lua_data[faction][g.group_name] = {
|
||||||
"name": ground_object.name,
|
"name": ground_object.name,
|
||||||
"range": threat_range.meters,
|
"range": threat_range.meters,
|
||||||
"position": {
|
"position": {
|
||||||
|
|||||||
@ -22,7 +22,6 @@ from typing import (
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
List,
|
List,
|
||||||
Any,
|
Any,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from dcs import Mission, Point, unitgroup
|
from dcs import Mission, Point, unitgroup
|
||||||
@ -110,108 +109,93 @@ class GroundObjectGenerator:
|
|||||||
logging.warning(f"Found empty group in {self.ground_object}")
|
logging.warning(f"Found empty group in {self.ground_object}")
|
||||||
continue
|
continue
|
||||||
group_name = group.group_name if unique_name else group.name
|
group_name = group.group_name if unique_name else group.name
|
||||||
if group.static_group:
|
moving_group: Optional[MovingGroup[Any]] = None
|
||||||
# Static Group
|
for i, unit in enumerate(group.units):
|
||||||
for i, u in enumerate(group.units):
|
if isinstance(unit, SceneryGroundUnit):
|
||||||
if isinstance(u, SceneryGroundUnit):
|
# Special handling for scenery objects:
|
||||||
# Special handling for scenery objects:
|
# Only create a trigger zone and no "real" dcs unit
|
||||||
# Only create a trigger zone and no "real" dcs unit
|
self.add_trigger_zone_for_scenery(unit)
|
||||||
self.add_trigger_zone_for_scenery(u)
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
# Only skip dead units after trigger zone for scenery created!
|
# Only skip dead units after trigger zone for scenery created!
|
||||||
if not u.alive:
|
if not unit.alive:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
unit_type = unit_type_from_name(u.type)
|
unit_type = unit_type_from_name(unit.type)
|
||||||
if not unit_type:
|
if not unit_type:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Unit type {u.type} is not a valid dcs unit type"
|
f"Unit type {unit.type} is not a valid dcs unit type"
|
||||||
)
|
|
||||||
|
|
||||||
sg = self.m.static_group(
|
|
||||||
country=self.country,
|
|
||||||
name=u.unit_name if unique_name else u.name,
|
|
||||||
_type=unit_type,
|
|
||||||
position=u.position,
|
|
||||||
heading=u.position.heading.degrees,
|
|
||||||
dead=not u.alive,
|
|
||||||
)
|
)
|
||||||
self._register_ground_unit(u, sg.units[0])
|
|
||||||
else:
|
|
||||||
# Moving Group
|
|
||||||
moving_group: Optional[MovingGroup[Any]] = None
|
|
||||||
for i, unit in enumerate(group.units):
|
|
||||||
if not unit.alive:
|
|
||||||
continue
|
|
||||||
if unit.type in vehicle_map:
|
|
||||||
# Vehicle Group
|
|
||||||
unit_type = vehicle_map[unit.type]
|
|
||||||
elif unit.type in ship_map:
|
|
||||||
# Ship Group
|
|
||||||
unit_type = ship_map[group.units[0].type]
|
|
||||||
else:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"Unit type {unit.type} is not a valid dcs unit type"
|
|
||||||
)
|
|
||||||
|
|
||||||
unit_name = unit.unit_name if unique_name else unit.name
|
unit_name = unit.unit_name if unique_name else unit.name
|
||||||
if moving_group is None:
|
if moving_group is None or group.static_group:
|
||||||
# First unit of the group will create the dcs group
|
# First unit of the group will create the dcs group
|
||||||
if issubclass(unit_type, VehicleType):
|
if issubclass(unit_type, VehicleType):
|
||||||
moving_group = self.m.vehicle_group(
|
moving_group = self.m.vehicle_group(
|
||||||
self.country,
|
self.country,
|
||||||
group_name,
|
group_name,
|
||||||
unit_type,
|
unit_type,
|
||||||
position=unit.position,
|
position=unit.position,
|
||||||
heading=unit.position.heading.degrees,
|
heading=unit.position.heading.degrees,
|
||||||
)
|
)
|
||||||
moving_group.units[0].player_can_drive = True
|
moving_group.units[0].player_can_drive = True
|
||||||
self.enable_eplrs(moving_group, unit_type)
|
self.enable_eplrs(moving_group, unit_type)
|
||||||
if issubclass(unit_type, ShipType):
|
elif issubclass(unit_type, ShipType):
|
||||||
moving_group = self.m.ship_group(
|
moving_group = self.m.ship_group(
|
||||||
self.country,
|
self.country,
|
||||||
group_name,
|
group_name,
|
||||||
unit_type,
|
unit_type,
|
||||||
position=unit.position,
|
position=unit.position,
|
||||||
heading=unit.position.heading.degrees,
|
heading=unit.position.heading.degrees,
|
||||||
)
|
)
|
||||||
if moving_group:
|
elif issubclass(unit_type, StaticType):
|
||||||
moving_group.units[0].name = unit_name
|
static_group = self.m.static_group(
|
||||||
self.set_alarm_state(moving_group)
|
country=self.country,
|
||||||
self._register_ground_unit(unit, moving_group.units[0])
|
name=unit_name,
|
||||||
else:
|
_type=unit_type,
|
||||||
raise RuntimeError("DCS Group creation failed")
|
position=unit.position,
|
||||||
|
heading=unit.position.heading.degrees,
|
||||||
|
dead=not unit.alive,
|
||||||
|
)
|
||||||
|
self._register_ground_unit(unit, static_group.units[0])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if moving_group:
|
||||||
|
moving_group.units[0].name = unit_name
|
||||||
|
self.set_alarm_state(moving_group)
|
||||||
|
self._register_ground_unit(unit, moving_group.units[0])
|
||||||
else:
|
else:
|
||||||
# Additional Units in the group
|
raise RuntimeError("DCS Group creation failed")
|
||||||
dcs_unit: Optional[Unit] = None
|
else:
|
||||||
if issubclass(unit_type, VehicleType):
|
# Additional Units in the group
|
||||||
dcs_unit = Vehicle(
|
dcs_unit: Optional[Unit] = None
|
||||||
self.m.next_unit_id(),
|
if issubclass(unit_type, VehicleType):
|
||||||
unit_name,
|
dcs_unit = Vehicle(
|
||||||
unit.type,
|
self.m.next_unit_id(),
|
||||||
)
|
unit_name,
|
||||||
dcs_unit.player_can_drive = True
|
unit.type,
|
||||||
elif issubclass(unit_type, ShipType):
|
)
|
||||||
dcs_unit = Ship(
|
dcs_unit.player_can_drive = True
|
||||||
self.m.next_unit_id(),
|
elif issubclass(unit_type, ShipType):
|
||||||
unit_name,
|
dcs_unit = Ship(
|
||||||
unit_type,
|
self.m.next_unit_id(),
|
||||||
)
|
unit_name,
|
||||||
if dcs_unit:
|
unit_type,
|
||||||
dcs_unit.position = unit.position
|
)
|
||||||
dcs_unit.heading = unit.position.heading.degrees
|
if dcs_unit:
|
||||||
moving_group.add_unit(dcs_unit)
|
dcs_unit.position = unit.position
|
||||||
self._register_ground_unit(unit, dcs_unit)
|
dcs_unit.heading = unit.position.heading.degrees
|
||||||
else:
|
moving_group.add_unit(dcs_unit)
|
||||||
raise RuntimeError("DCS Unit creation failed")
|
self._register_ground_unit(unit, dcs_unit)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("DCS Unit creation failed")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||||
if unit_type.eplrs:
|
if unit_type.eplrs:
|
||||||
group.points[0].tasks.append(EPLRS(group.id))
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
|
|
||||||
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None:
|
def set_alarm_state(self, group: MovingGroup[Any]) -> None:
|
||||||
if self.game.settings.perf_red_alert_state:
|
if self.game.settings.perf_red_alert_state:
|
||||||
group.points[0].tasks.append(OptAlarmState(2))
|
group.points[0].tasks.append(OptAlarmState(2))
|
||||||
else:
|
else:
|
||||||
@ -287,7 +271,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
|
|||||||
# TODO : Should be pre-planned ?
|
# TODO : Should be pre-planned ?
|
||||||
# TODO : Add delay to task to spread fire task over mission duration ?
|
# TODO : Add delay to task to spread fire task over mission duration ?
|
||||||
for group in self.ground_object.groups:
|
for group in self.ground_object.groups:
|
||||||
vg = self.m.find_group(group.name)
|
vg = self.m.find_group(group.group_name)
|
||||||
if vg is not None:
|
if vg is not None:
|
||||||
targets = self.possible_missile_targets()
|
targets = self.possible_missile_targets()
|
||||||
if targets:
|
if targets:
|
||||||
@ -327,7 +311,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
|
|||||||
"""
|
"""
|
||||||
site_range = 0
|
site_range = 0
|
||||||
for group in self.ground_object.groups:
|
for group in self.ground_object.groups:
|
||||||
vg = self.m.find_group(group.name)
|
vg = self.m.find_group(group.group_name)
|
||||||
if vg is not None:
|
if vg is not None:
|
||||||
for u in vg.units:
|
for u in vg.units:
|
||||||
if u.type in vehicle_map:
|
if u.type in vehicle_map:
|
||||||
@ -383,7 +367,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
|
|
||||||
ship_group = self.m.ship_group(
|
ship_group = self.m.ship_group(
|
||||||
self.country,
|
self.country,
|
||||||
group.name,
|
group.group_name if unique_name else group.name,
|
||||||
unit_type,
|
unit_type,
|
||||||
position=group.units[0].position,
|
position=group.units[0].position,
|
||||||
heading=group.units[0].position.heading.degrees,
|
heading=group.units[0].position.heading.degrees,
|
||||||
@ -523,23 +507,20 @@ class CarrierGenerator(GenericCarrierGenerator):
|
|||||||
|
|
||||||
def tacan_callsign(self) -> str:
|
def tacan_callsign(self) -> str:
|
||||||
# TODO: Assign these properly.
|
# TODO: Assign these properly.
|
||||||
if self.control_point.name == "Carrier Strike Group 8":
|
return random.choice(
|
||||||
return "TRU"
|
[
|
||||||
else:
|
"STE",
|
||||||
return random.choice(
|
"CVN",
|
||||||
[
|
"CVH",
|
||||||
"STE",
|
"CCV",
|
||||||
"CVN",
|
"ACC",
|
||||||
"CVH",
|
"ARC",
|
||||||
"CCV",
|
"GER",
|
||||||
"ACC",
|
"ABR",
|
||||||
"ARC",
|
"LIN",
|
||||||
"GER",
|
"TRU",
|
||||||
"ABR",
|
]
|
||||||
"LIN",
|
)
|
||||||
"TRU",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LhaGenerator(GenericCarrierGenerator):
|
class LhaGenerator(GenericCarrierGenerator):
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from game.config import RUNWAY_REPAIR_COST
|
from game.config import RUNWAY_REPAIR_COST
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import ControlPoint, MissionTarget
|
from game.theater import ControlPoint, MissionTarget
|
||||||
|
|
||||||
@ -116,7 +116,7 @@ class ProcurementAi:
|
|||||||
return budget
|
return budget
|
||||||
|
|
||||||
def affordable_ground_unit_of_class(
|
def affordable_ground_unit_of_class(
|
||||||
self, budget: float, unit_class: GroundUnitClass
|
self, budget: float, unit_class: UnitClass
|
||||||
) -> Optional[GroundUnitType]:
|
) -> Optional[GroundUnitType]:
|
||||||
faction_units = set(self.faction.frontline_units) | set(
|
faction_units = set(self.faction.frontline_units) | set(
|
||||||
self.faction.artillery_units
|
self.faction.artillery_units
|
||||||
@ -154,10 +154,10 @@ class ProcurementAi:
|
|||||||
|
|
||||||
return budget
|
return budget
|
||||||
|
|
||||||
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
|
def most_needed_unit_class(self, cp: ControlPoint) -> UnitClass:
|
||||||
worst_balanced: Optional[GroundUnitClass] = None
|
worst_balanced: Optional[UnitClass] = None
|
||||||
worst_fulfillment = math.inf
|
worst_fulfillment = math.inf
|
||||||
for unit_class in GroundUnitClass:
|
for unit_class in UnitClass:
|
||||||
if not self.faction.has_access_to_unit_class(unit_class):
|
if not self.faction.has_access_to_unit_class(unit_class):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ class ProcurementAi:
|
|||||||
worst_fulfillment = fulfillment
|
worst_fulfillment = fulfillment
|
||||||
worst_balanced = unit_class
|
worst_balanced = unit_class
|
||||||
if worst_balanced is None:
|
if worst_balanced is None:
|
||||||
return GroundUnitClass.Tank
|
return UnitClass.Tank
|
||||||
return worst_balanced
|
return worst_balanced
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -300,7 +300,7 @@ class ProcurementAi:
|
|||||||
return understaffed
|
return understaffed
|
||||||
|
|
||||||
def cost_ratio_of_ground_unit(
|
def cost_ratio_of_ground_unit(
|
||||||
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
self, control_point: ControlPoint, unit_class: UnitClass
|
||||||
) -> float:
|
) -> float:
|
||||||
allocations = control_point.allocated_ground_units(
|
allocations = control_point.allocated_ground_units(
|
||||||
self.game.coalition_for(self.is_player).transfers
|
self.game.coalition_for(self.is_player).transfers
|
||||||
|
|||||||
@ -48,6 +48,7 @@ from .theatergroundobject import (
|
|||||||
GroundUnit,
|
GroundUnit,
|
||||||
)
|
)
|
||||||
from ..ato.starttype import StartType
|
from ..ato.starttype import StartType
|
||||||
|
from ..data.units import UnitClass
|
||||||
from ..dcs.aircrafttype import AircraftType
|
from ..dcs.aircrafttype import AircraftType
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
from ..dcs.groundunittype import GroundUnitType
|
||||||
from ..utils import nautical_miles
|
from ..utils import nautical_miles
|
||||||
@ -521,20 +522,13 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
ControlPointType.LHA_GROUP,
|
ControlPointType.LHA_GROUP,
|
||||||
]:
|
]:
|
||||||
for g in self.ground_objects:
|
for g in self.ground_objects:
|
||||||
if isinstance(g, CarrierGroundObject):
|
for group in g.groups:
|
||||||
for group in g.groups:
|
for u in group.units:
|
||||||
for u in group.units:
|
if u.unit_type and u.unit_type.unit_class in [
|
||||||
if unit_type_from_name(u.type) in [
|
UnitClass.AircraftCarrier,
|
||||||
Forrestal,
|
UnitClass.HelicopterCarrier,
|
||||||
Stennis,
|
]:
|
||||||
KUZNECOW,
|
return group.group_name
|
||||||
]:
|
|
||||||
return group.name
|
|
||||||
elif isinstance(g, LhaGroundObject):
|
|
||||||
for group in g.groups:
|
|
||||||
for u in group.units:
|
|
||||||
if unit_type_from_name(u.type) in [LHA_Tarawa]:
|
|
||||||
return group.name
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
@ -668,6 +662,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
self._retreat_squadron(game, squadron)
|
self._retreat_squadron(game, squadron)
|
||||||
|
|
||||||
def depopulate_uncapturable_tgos(self) -> None:
|
def depopulate_uncapturable_tgos(self) -> None:
|
||||||
|
# TODO Rework this.
|
||||||
for tgo in self.connected_objectives:
|
for tgo in self.connected_objectives:
|
||||||
if not tgo.capturable:
|
if not tgo.capturable:
|
||||||
tgo.clear()
|
tgo.clear()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
@ -18,7 +18,7 @@ from game.theater.theatergroundobject import (
|
|||||||
)
|
)
|
||||||
from game.utils import Heading
|
from game.utils import Heading
|
||||||
from game.version import VERSION
|
from game.version import VERSION
|
||||||
from gen.templates import GroundObjectTemplates, TemplateCategory, GroundObjectTemplate
|
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
|
||||||
from gen.naming import namegen
|
from gen.naming import namegen
|
||||||
from . import (
|
from . import (
|
||||||
ConflictTheater,
|
ConflictTheater,
|
||||||
@ -28,6 +28,9 @@ from . import (
|
|||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
)
|
)
|
||||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||||
|
from ..data.units import UnitClass
|
||||||
|
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS
|
||||||
|
from ..dcs.unitgroup import UnitGroup
|
||||||
from ..profiling import logged_duration
|
from ..profiling import logged_duration
|
||||||
from ..settings import Settings
|
from ..settings import Settings
|
||||||
|
|
||||||
@ -68,15 +71,15 @@ class GameGenerator:
|
|||||||
generator_settings: GeneratorSettings,
|
generator_settings: GeneratorSettings,
|
||||||
mod_settings: ModSettings,
|
mod_settings: ModSettings,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.player = player.apply_mod_settings(mod_settings)
|
self.player = player
|
||||||
self.enemy = enemy.apply_mod_settings(mod_settings)
|
self.enemy = enemy
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.air_wing_config = air_wing_config
|
self.air_wing_config = air_wing_config
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.generator_settings = generator_settings
|
self.generator_settings = generator_settings
|
||||||
|
|
||||||
with logged_duration(f"Initializing templates"):
|
with logged_duration(f"Initializing faction and templates"):
|
||||||
self.load_templates()
|
self.initialize_factions(mod_settings)
|
||||||
|
|
||||||
def generate(self) -> Game:
|
def generate(self) -> Game:
|
||||||
with logged_duration("TGO population"):
|
with logged_duration("TGO population"):
|
||||||
@ -123,12 +126,11 @@ class GameGenerator:
|
|||||||
for cp in to_remove:
|
for cp in to_remove:
|
||||||
self.theater.controlpoints.remove(cp)
|
self.theater.controlpoints.remove(cp)
|
||||||
|
|
||||||
def load_templates(self) -> None:
|
def initialize_factions(self, mod_settings: ModSettings) -> None:
|
||||||
templates = GroundObjectTemplates.from_json(
|
with logged_duration("Loading Templates from mapping"):
|
||||||
"resources/templates/templates.json"
|
templates = GroundObjectTemplates.from_folder("resources/templates/")
|
||||||
)
|
self.player.initialize(templates, mod_settings)
|
||||||
self.player.load_templates(templates)
|
self.enemy.initialize(templates, mod_settings)
|
||||||
self.enemy.load_templates(templates)
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPointGroundObjectGenerator:
|
class ControlPointGroundObjectGenerator:
|
||||||
@ -152,21 +154,23 @@ class ControlPointGroundObjectGenerator:
|
|||||||
|
|
||||||
def generate(self) -> bool:
|
def generate(self) -> bool:
|
||||||
self.control_point.connected_objectives = []
|
self.control_point.connected_objectives = []
|
||||||
if self.faction.navy_generators:
|
self.generate_navy()
|
||||||
# Even airbases can generate navies if they are close enough to the water.
|
|
||||||
self.generate_navy()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def generate_random_from_templates(
|
def generate_random_ground_object(
|
||||||
self, templates: list[GroundObjectTemplate], position: PointWithHeading
|
self, unit_groups: list[UnitGroup], position: PointWithHeading
|
||||||
|
) -> None:
|
||||||
|
self.generate_ground_object_from_group(random.choice(unit_groups), position)
|
||||||
|
|
||||||
|
def generate_ground_object_from_group(
|
||||||
|
self, unit_group: UnitGroup, position: PointWithHeading
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
template = random.choice(templates)
|
|
||||||
with logged_duration(
|
with logged_duration(
|
||||||
f"Ground Object generation from template {template.name}"
|
f"Ground Object generation for unit_group "
|
||||||
|
f"{unit_group.name} ({unit_group.role.value})"
|
||||||
):
|
):
|
||||||
ground_object = template.generate(
|
ground_object = unit_group.generate(
|
||||||
namegen.random_objective_name(),
|
namegen.random_objective_name(),
|
||||||
position,
|
position,
|
||||||
self.control_point,
|
self.control_point,
|
||||||
@ -176,23 +180,23 @@ class ControlPointGroundObjectGenerator:
|
|||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
logging.error("Template Generator not implemented yet")
|
logging.error("Template Generator not implemented yet")
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.error(f"No templates to generate object")
|
logging.error(f"No templates to generate object from {unit_group.name}")
|
||||||
|
|
||||||
def generate_navy(self) -> None:
|
def generate_navy(self) -> None:
|
||||||
skip_player_navy = self.generator_settings.no_player_navy
|
skip_player_navy = self.generator_settings.no_player_navy
|
||||||
if self.control_point.captured and skip_player_navy:
|
if self.control_point.captured and skip_player_navy:
|
||||||
return
|
return
|
||||||
|
|
||||||
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
skip_enemy_navy = self.generator_settings.no_enemy_navy
|
||||||
if not self.control_point.captured and skip_enemy_navy:
|
if not self.control_point.captured and skip_enemy_navy:
|
||||||
return
|
return
|
||||||
|
|
||||||
templates = list(
|
|
||||||
self.faction.templates.for_category(TemplateCategory.Naval, "ship")
|
|
||||||
)
|
|
||||||
|
|
||||||
for position in self.control_point.preset_locations.ships:
|
for position in self.control_point.preset_locations.ships:
|
||||||
self.generate_random_from_templates(templates, position)
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
|
GroupRole.Naval, GroupTask.Navy
|
||||||
|
)
|
||||||
|
if not unit_group:
|
||||||
|
logging.error(f"{self.faction_name} has no UnitGroup for Navy")
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
|
|
||||||
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||||
@ -213,12 +217,14 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
templates = list(
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
self.faction.templates.for_category(TemplateCategory.Naval, "carrier")
|
GroupRole.Naval, GroupTask.AircraftCarrier
|
||||||
)
|
)
|
||||||
|
if not unit_group:
|
||||||
self.generate_random_from_templates(
|
logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
|
||||||
templates,
|
return False
|
||||||
|
self.generate_ground_object_from_group(
|
||||||
|
unit_group,
|
||||||
PointWithHeading.from_point(
|
PointWithHeading.from_point(
|
||||||
self.control_point.position, self.control_point.heading
|
self.control_point.position, self.control_point.heading
|
||||||
),
|
),
|
||||||
@ -240,12 +246,14 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
templates = list(
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
self.faction.templates.for_category(TemplateCategory.Naval, "lha")
|
GroupRole.Naval, GroupTask.HelicopterCarrier
|
||||||
)
|
)
|
||||||
|
if not unit_group:
|
||||||
self.generate_random_from_templates(
|
logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
|
||||||
templates,
|
return False
|
||||||
|
self.generate_ground_object_from_group(
|
||||||
|
unit_group,
|
||||||
PointWithHeading.from_point(
|
PointWithHeading.from_point(
|
||||||
self.control_point.position, self.control_point.heading
|
self.control_point.position, self.control_point.heading
|
||||||
),
|
),
|
||||||
@ -280,110 +288,83 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
self.generate_offshore_strike_targets()
|
self.generate_offshore_strike_targets()
|
||||||
self.generate_factories()
|
self.generate_factories()
|
||||||
self.generate_ammunition_depots()
|
self.generate_ammunition_depots()
|
||||||
|
self.generate_missile_sites()
|
||||||
if self.faction.missiles:
|
self.generate_coastal_sites()
|
||||||
self.generate_missile_sites()
|
|
||||||
|
|
||||||
if self.faction.coastal_defenses:
|
|
||||||
self.generate_coastal_sites()
|
|
||||||
|
|
||||||
def generate_armor_groups(self) -> None:
|
def generate_armor_groups(self) -> None:
|
||||||
templates = list(self.faction.templates.for_category(TemplateCategory.Armor))
|
|
||||||
if not templates:
|
|
||||||
logging.error(f"{self.faction_name} has no access to Armor templates")
|
|
||||||
return
|
|
||||||
|
|
||||||
for position in self.control_point.preset_locations.armor_groups:
|
for position in self.control_point.preset_locations.armor_groups:
|
||||||
self.generate_random_from_templates(templates, position)
|
unit_group = self.faction.random_group_for_role_and_tasks(
|
||||||
|
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
|
||||||
|
)
|
||||||
|
if not unit_group:
|
||||||
|
logging.error(f"{self.faction_name} has no templates for Armor Groups")
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_aa(self) -> None:
|
def generate_aa(self) -> None:
|
||||||
presets = self.control_point.preset_locations
|
presets = self.control_point.preset_locations
|
||||||
for position in presets.long_range_sams:
|
aa_tasking = [GroupTask.AAA]
|
||||||
self.generate_aa_at(
|
|
||||||
position,
|
|
||||||
[
|
|
||||||
AirDefenseRange.Long,
|
|
||||||
AirDefenseRange.Medium,
|
|
||||||
AirDefenseRange.Short,
|
|
||||||
AirDefenseRange.AAA,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
for position in presets.medium_range_sams:
|
|
||||||
self.generate_aa_at(
|
|
||||||
position,
|
|
||||||
[
|
|
||||||
AirDefenseRange.Medium,
|
|
||||||
AirDefenseRange.Short,
|
|
||||||
AirDefenseRange.AAA,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
for position in presets.short_range_sams:
|
|
||||||
self.generate_aa_at(
|
|
||||||
position,
|
|
||||||
[AirDefenseRange.Short, AirDefenseRange.AAA],
|
|
||||||
)
|
|
||||||
for position in presets.aaa:
|
for position in presets.aaa:
|
||||||
self.generate_aa_at(
|
self.generate_aa_at(position, aa_tasking)
|
||||||
position,
|
aa_tasking.insert(0, GroupTask.SHORAD)
|
||||||
[AirDefenseRange.AAA],
|
for position in presets.short_range_sams:
|
||||||
)
|
self.generate_aa_at(position, aa_tasking)
|
||||||
|
aa_tasking.insert(0, GroupTask.MERAD)
|
||||||
|
for position in presets.medium_range_sams:
|
||||||
|
self.generate_aa_at(position, aa_tasking)
|
||||||
|
aa_tasking.insert(0, GroupTask.LORAD)
|
||||||
|
for position in presets.long_range_sams:
|
||||||
|
self.generate_aa_at(position, aa_tasking)
|
||||||
|
|
||||||
def generate_ewrs(self) -> None:
|
def generate_ewrs(self) -> None:
|
||||||
templates = list(
|
|
||||||
self.faction.templates.for_category(TemplateCategory.AirDefence, "EWR")
|
|
||||||
)
|
|
||||||
if not templates:
|
|
||||||
logging.error(f"{self.faction_name} has no access to EWR templates")
|
|
||||||
return
|
|
||||||
|
|
||||||
for position in self.control_point.preset_locations.ewrs:
|
for position in self.control_point.preset_locations.ewrs:
|
||||||
self.generate_random_from_templates(templates, position)
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
|
GroupRole.AntiAir, GroupTask.EWR
|
||||||
|
)
|
||||||
|
if not unit_group:
|
||||||
|
logging.error(f"{self.faction_name} has no UnitGroup for EWR")
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_building_at(
|
def generate_building_at(
|
||||||
self,
|
self,
|
||||||
template_category: TemplateCategory,
|
group_task: GroupTask,
|
||||||
building_category: str,
|
|
||||||
position: PointWithHeading,
|
position: PointWithHeading,
|
||||||
) -> None:
|
) -> None:
|
||||||
templates = list(
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
self.faction.templates.for_category(template_category, building_category)
|
GroupRole.Building, group_task
|
||||||
)
|
)
|
||||||
if templates:
|
if not unit_group:
|
||||||
self.generate_random_from_templates(templates, position)
|
|
||||||
else:
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{self.faction_name} has no access to Building type {building_category}"
|
f"{self.faction_name} has no access to Building ({group_task.value})"
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_ammunition_depots(self) -> None:
|
def generate_ammunition_depots(self) -> None:
|
||||||
for position in self.control_point.preset_locations.ammunition_depots:
|
for position in self.control_point.preset_locations.ammunition_depots:
|
||||||
self.generate_building_at(TemplateCategory.Building, "ammo", position)
|
self.generate_building_at(GroupTask.Ammo, position)
|
||||||
|
|
||||||
def generate_factories(self) -> None:
|
def generate_factories(self) -> None:
|
||||||
for position in self.control_point.preset_locations.factories:
|
for position in self.control_point.preset_locations.factories:
|
||||||
self.generate_building_at(TemplateCategory.Building, "factory", position)
|
self.generate_building_at(GroupTask.Factory, position)
|
||||||
|
|
||||||
def generate_aa_at(
|
def generate_aa_at(
|
||||||
self, position: PointWithHeading, ranges: list[AirDefenseRange]
|
self, position: PointWithHeading, tasks: list[GroupTask]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
for task in tasks:
|
||||||
templates = []
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
for aa_range in ranges:
|
GroupRole.AntiAir, task
|
||||||
for template in self.faction.templates.for_category(
|
)
|
||||||
TemplateCategory.AirDefence, aa_range.name
|
if unit_group:
|
||||||
):
|
|
||||||
templates.append(template)
|
|
||||||
if len(templates) > 0:
|
|
||||||
# Only take next (smaller) aa_range when no template available for the
|
# Only take next (smaller) aa_range when no template available for the
|
||||||
# most requested range. Otherwise break the loop and continue
|
# most requested range. Otherwise break the loop and continue
|
||||||
break
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
return
|
||||||
|
|
||||||
if templates:
|
logging.error(
|
||||||
self.generate_random_from_templates(templates, position)
|
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
|
||||||
else:
|
)
|
||||||
logging.error(
|
|
||||||
f"{self.faction_name} has no access to SAM Templates ({', '.join([range.name for range in ranges])})"
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_scenery_sites(self) -> None:
|
def generate_scenery_sites(self) -> None:
|
||||||
presets = self.control_point.preset_locations
|
presets = self.control_point.preset_locations
|
||||||
@ -423,38 +404,32 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
self.control_point.connected_objectives.append(g)
|
self.control_point.connected_objectives.append(g)
|
||||||
|
|
||||||
def generate_missile_sites(self) -> None:
|
def generate_missile_sites(self) -> None:
|
||||||
templates = list(self.faction.templates.for_category(TemplateCategory.Missile))
|
|
||||||
if not templates:
|
|
||||||
logging.error(f"{self.faction_name} has no access to Missile templates")
|
|
||||||
return
|
|
||||||
for position in self.control_point.preset_locations.missile_sites:
|
for position in self.control_point.preset_locations.missile_sites:
|
||||||
self.generate_random_from_templates(templates, position)
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
|
GroupRole.Defenses, GroupTask.Missile
|
||||||
|
)
|
||||||
|
if not unit_group:
|
||||||
|
logging.error(f"{self.faction_name} has no UnitGroup for Missile")
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_coastal_sites(self) -> None:
|
def generate_coastal_sites(self) -> None:
|
||||||
templates = list(self.faction.templates.for_category(TemplateCategory.Coastal))
|
|
||||||
if not templates:
|
|
||||||
logging.error(f"{self.faction_name} has no access to Coastal templates")
|
|
||||||
return
|
|
||||||
for position in self.control_point.preset_locations.coastal_defenses:
|
for position in self.control_point.preset_locations.coastal_defenses:
|
||||||
self.generate_random_from_templates(templates, position)
|
unit_group = self.faction.random_group_for_role_and_task(
|
||||||
|
GroupRole.Defenses, GroupTask.Coastal
|
||||||
|
)
|
||||||
|
if not unit_group:
|
||||||
|
logging.error(f"{self.faction_name} has no UnitGroup for Coastal")
|
||||||
|
return
|
||||||
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_strike_targets(self) -> None:
|
def generate_strike_targets(self) -> None:
|
||||||
building_set = list(set(self.faction.building_set) - {"oil"})
|
|
||||||
if not building_set:
|
|
||||||
logging.error(f"{self.faction_name} has no buildings defined")
|
|
||||||
return
|
|
||||||
for position in self.control_point.preset_locations.strike_locations:
|
for position in self.control_point.preset_locations.strike_locations:
|
||||||
category = random.choice(building_set)
|
self.generate_building_at(GroupTask.StrikeTarget, position)
|
||||||
self.generate_building_at(TemplateCategory.Building, category, position)
|
|
||||||
|
|
||||||
def generate_offshore_strike_targets(self) -> None:
|
def generate_offshore_strike_targets(self) -> None:
|
||||||
if "oil" not in self.faction.building_set:
|
|
||||||
logging.error(
|
|
||||||
f"{self.faction_name} does not support offshore strike targets"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||||
self.generate_building_at(TemplateCategory.Building, "oil", position)
|
self.generate_building_at(GroupTask.Oil, position)
|
||||||
|
|
||||||
|
|
||||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||||
@ -466,8 +441,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|||||||
|
|
||||||
def generate_fob(self) -> None:
|
def generate_fob(self) -> None:
|
||||||
self.generate_building_at(
|
self.generate_building_at(
|
||||||
TemplateCategory.Building,
|
GroupTask.FOB,
|
||||||
"fob",
|
|
||||||
PointWithHeading.from_point(
|
PointWithHeading.from_point(
|
||||||
self.control_point.position, self.control_point.heading
|
self.control_point.position, self.control_point.heading
|
||||||
),
|
),
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from dataclasses import dataclass
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
|
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
|
||||||
|
|
||||||
|
from dcs.unittype import VehicleType, ShipType
|
||||||
from dcs.vehicles import vehicle_map
|
from dcs.vehicles import vehicle_map
|
||||||
from dcs.ships import ship_map
|
from dcs.ships import ship_map
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ from dcs.triggers import TriggerZone
|
|||||||
from game.dcs.helpers import unit_type_from_name
|
from game.dcs.helpers import unit_type_from_name
|
||||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
from ..dcs.groundunittype import GroundUnitType
|
||||||
|
from ..dcs.shipunittype import ShipUnitType
|
||||||
from ..dcs.unittype import UnitType
|
from ..dcs.unittype import UnitType
|
||||||
from ..point_with_heading import PointWithHeading
|
from ..point_with_heading import PointWithHeading
|
||||||
from ..utils import Distance, Heading, meters
|
from ..utils import Distance, Heading, meters
|
||||||
@ -90,11 +92,13 @@ class GroundUnit:
|
|||||||
_unit_type: Optional[UnitType[Any]] = None
|
_unit_type: Optional[UnitType[Any]] = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit:
|
def from_template(
|
||||||
|
id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject
|
||||||
|
) -> GroundUnit:
|
||||||
return GroundUnit(
|
return GroundUnit(
|
||||||
id,
|
id,
|
||||||
t.name,
|
t.name,
|
||||||
t.type,
|
unit_type,
|
||||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||||
go,
|
go,
|
||||||
)
|
)
|
||||||
@ -103,20 +107,16 @@ class GroundUnit:
|
|||||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||||
if not self._unit_type:
|
if not self._unit_type:
|
||||||
try:
|
try:
|
||||||
if self.type in vehicle_map:
|
unit_type: Optional[UnitType[Any]] = None
|
||||||
vehicle_type = db.vehicle_type_from_name(self.type)
|
dcs_type = db.unit_type_from_name(self.type)
|
||||||
self._unit_type = next(GroundUnitType.for_dcs_type(vehicle_type))
|
if dcs_type and issubclass(dcs_type, VehicleType):
|
||||||
elif self.type in ship_map:
|
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
|
||||||
ship_type = db.ship_type_from_name(self.type)
|
elif dcs_type and issubclass(dcs_type, ShipType):
|
||||||
# TODO Allow handling of Ships. This requires extension of UnitType
|
unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
|
||||||
return None
|
self._unit_type = unit_type
|
||||||
elif (static_type := db.static_type_from_name(self.type)) is not None:
|
|
||||||
# TODO Allow handling of Statics
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return None
|
logging.error(f"No UnitType for {self.type}")
|
||||||
|
pass
|
||||||
return self._unit_type
|
return self._unit_type
|
||||||
|
|
||||||
def kill(self) -> None:
|
def kill(self) -> None:
|
||||||
@ -153,36 +153,15 @@ class GroundGroup:
|
|||||||
id: int,
|
id: int,
|
||||||
g: GroupTemplate,
|
g: GroupTemplate,
|
||||||
go: TheaterGroundObject,
|
go: TheaterGroundObject,
|
||||||
randomization: bool = True,
|
|
||||||
) -> GroundGroup:
|
) -> GroundGroup:
|
||||||
|
|
||||||
units = []
|
|
||||||
if g.randomizer:
|
|
||||||
g.randomizer.randomize()
|
|
||||||
|
|
||||||
for u_id, unit in enumerate(g.units):
|
|
||||||
tgo_unit = GroundUnit.from_template(u_id, unit, go)
|
|
||||||
if randomization and g.randomizer:
|
|
||||||
if g.randomizer.unit_type:
|
|
||||||
tgo_unit.type = g.randomizer.unit_type
|
|
||||||
try:
|
|
||||||
# Check if unit can be assigned
|
|
||||||
g.randomizer.use_unit()
|
|
||||||
except IndexError:
|
|
||||||
# Do not generate the unit as no more units are available
|
|
||||||
continue
|
|
||||||
units.append(tgo_unit)
|
|
||||||
|
|
||||||
tgo_group = GroundGroup(
|
tgo_group = GroundGroup(
|
||||||
id,
|
id,
|
||||||
g.name,
|
g.name,
|
||||||
PointWithHeading.from_point(go.position, go.heading),
|
PointWithHeading.from_point(go.position, go.heading),
|
||||||
units,
|
g.generate_units(go),
|
||||||
go,
|
go,
|
||||||
)
|
)
|
||||||
|
|
||||||
tgo_group.static_group = g.static
|
tgo_group.static_group = g.static
|
||||||
|
|
||||||
return tgo_group
|
return tgo_group
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -1305,7 +1305,7 @@ class FlightPlanBuilder:
|
|||||||
for group in location.groups:
|
for group in location.groups:
|
||||||
if group.units:
|
if group.units:
|
||||||
targets.append(
|
targets.append(
|
||||||
StrikeTarget(f"{group.name} at {location.name}", group)
|
StrikeTarget(f"{group.group_name} at {location.name}", group)
|
||||||
)
|
)
|
||||||
elif isinstance(location, Convoy):
|
elif isinstance(location, Convoy):
|
||||||
targets.append(StrikeTarget(location.name, location))
|
targets.append(StrikeTarget(location.name, location))
|
||||||
@ -1318,7 +1318,7 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
|
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
|
||||||
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
|
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||||
|
|
||||||
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
"""Generates an anti-ship flight plan.
|
"""Generates an anti-ship flight plan.
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import random
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, TYPE_CHECKING
|
from typing import Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from gen.ground_forces.combat_stance import CombatStance
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
@ -100,28 +100,28 @@ class GroundPlanner:
|
|||||||
# Create combat groups and assign them randomly to each enemy CP
|
# Create combat groups and assign them randomly to each enemy CP
|
||||||
for unit_type in self.cp.base.armor:
|
for unit_type in self.cp.base.armor:
|
||||||
unit_class = unit_type.unit_class
|
unit_class = unit_type.unit_class
|
||||||
if unit_class is GroundUnitClass.Tank:
|
if unit_class is UnitClass.Tank:
|
||||||
collection = self.tank_groups
|
collection = self.tank_groups
|
||||||
role = CombatGroupRole.TANK
|
role = CombatGroupRole.TANK
|
||||||
elif unit_class is GroundUnitClass.Apc:
|
elif unit_class is UnitClass.Apc:
|
||||||
collection = self.apc_group
|
collection = self.apc_group
|
||||||
role = CombatGroupRole.APC
|
role = CombatGroupRole.APC
|
||||||
elif unit_class is GroundUnitClass.Artillery:
|
elif unit_class is UnitClass.Artillery:
|
||||||
collection = self.art_group
|
collection = self.art_group
|
||||||
role = CombatGroupRole.ARTILLERY
|
role = CombatGroupRole.ARTILLERY
|
||||||
elif unit_class is GroundUnitClass.Ifv:
|
elif unit_class is UnitClass.Ifv:
|
||||||
collection = self.ifv_group
|
collection = self.ifv_group
|
||||||
role = CombatGroupRole.IFV
|
role = CombatGroupRole.IFV
|
||||||
elif unit_class is GroundUnitClass.Logistics:
|
elif unit_class is UnitClass.Logistics:
|
||||||
collection = self.logi_groups
|
collection = self.logi_groups
|
||||||
role = CombatGroupRole.LOGI
|
role = CombatGroupRole.LOGI
|
||||||
elif unit_class is GroundUnitClass.Atgm:
|
elif unit_class is UnitClass.Atgm:
|
||||||
collection = self.atgm_group
|
collection = self.atgm_group
|
||||||
role = CombatGroupRole.ATGM
|
role = CombatGroupRole.ATGM
|
||||||
elif unit_class is GroundUnitClass.Shorads:
|
elif unit_class is UnitClass.SHORAD:
|
||||||
collection = self.shorad_groups
|
collection = self.shorad_groups
|
||||||
role = CombatGroupRole.SHORAD
|
role = CombatGroupRole.SHORAD
|
||||||
elif unit_class is GroundUnitClass.Recon:
|
elif unit_class is UnitClass.Recon:
|
||||||
collection = self.recon_groups
|
collection = self.recon_groups
|
||||||
role = CombatGroupRole.RECON
|
role = CombatGroupRole.RECON
|
||||||
else:
|
else:
|
||||||
|
|||||||
820
gen/templates.py
820
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 import Game
|
||||||
|
from game.data.groups import GroupRole, ROLE_TASKINGS, GroupTask
|
||||||
from game.point_with_heading import PointWithHeading
|
from game.point_with_heading import PointWithHeading
|
||||||
from game.theater import TheaterGroundObject
|
from game.theater import TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
@ -26,7 +27,6 @@ from game.theater.theatergroundobject import (
|
|||||||
GroundGroup,
|
GroundGroup,
|
||||||
)
|
)
|
||||||
from gen.templates import (
|
from gen.templates import (
|
||||||
TemplateCategory,
|
|
||||||
GroundObjectTemplate,
|
GroundObjectTemplate,
|
||||||
GroupTemplate,
|
GroupTemplate,
|
||||||
)
|
)
|
||||||
@ -41,7 +41,9 @@ class QGroundObjectGroupTemplate(QGroupBox):
|
|||||||
# If the group is not randomizable: Just view labels instead of edit fields
|
# If the group is not randomizable: Just view labels instead of edit fields
|
||||||
|
|
||||||
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
|
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
|
||||||
super(QGroundObjectGroupTemplate, self).__init__(str(group_id + 1))
|
super(QGroundObjectGroupTemplate, self).__init__(
|
||||||
|
f"{group_id + 1}: {group_template.name}"
|
||||||
|
)
|
||||||
self.group_template = group_template
|
self.group_template = group_template
|
||||||
|
|
||||||
self.group_layout = QGridLayout()
|
self.group_layout = QGridLayout()
|
||||||
@ -51,12 +53,12 @@ class QGroundObjectGroupTemplate(QGroupBox):
|
|||||||
self.unit_selector = QComboBox()
|
self.unit_selector = QComboBox()
|
||||||
self.group_selector = QCheckBox()
|
self.group_selector = QCheckBox()
|
||||||
|
|
||||||
self.group_selector.setChecked(True)
|
self.group_selector.setChecked(self.group_template.should_be_generated)
|
||||||
self.group_selector.setEnabled(self.group_template.optional)
|
self.group_selector.setEnabled(self.group_template.optional)
|
||||||
|
|
||||||
if self.group_template.randomizer:
|
if self.group_template.can_be_modified:
|
||||||
# Group can be randomized
|
# Group can be modified (more than 1 possible unit_type for the group)
|
||||||
for unit in self.group_template.randomizer.possible_ground_units:
|
for unit in self.group_template.possible_units:
|
||||||
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
|
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
|
||||||
self.group_layout.addWidget(
|
self.group_layout.addWidget(
|
||||||
self.unit_selector, 0, 0, alignment=Qt.AlignRight
|
self.unit_selector, 0, 0, alignment=Qt.AlignRight
|
||||||
@ -66,17 +68,21 @@ class QGroundObjectGroupTemplate(QGroupBox):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.amount_selector.setMinimum(1)
|
self.amount_selector.setMinimum(1)
|
||||||
self.amount_selector.setMaximum(len(self.group_template.units))
|
self.amount_selector.setMaximum(self.group_template.max_size)
|
||||||
self.amount_selector.setValue(self.group_template.randomizer.unit_count)
|
self.amount_selector.setValue(self.group_template.size)
|
||||||
|
|
||||||
self.on_group_changed()
|
self.on_group_changed()
|
||||||
else:
|
else:
|
||||||
# Group can not be randomized so just show the group info
|
# Group can not be randomized so just show the group info
|
||||||
group_info = QVBoxLayout()
|
group_info = QVBoxLayout()
|
||||||
for unit_type, count in self.group_template.unit_types_count.items():
|
try:
|
||||||
group_info.addWidget(
|
unit_name = next(self.group_template.possible_units)
|
||||||
QLabel(f"{count}x {unit_type}"), alignment=Qt.AlignLeft
|
except StopIteration:
|
||||||
)
|
unit_name = self.group_template.unit_type
|
||||||
|
group_info.addWidget(
|
||||||
|
QLabel(f"{self.group_template.size}x {unit_name}"),
|
||||||
|
alignment=Qt.AlignLeft,
|
||||||
|
)
|
||||||
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
|
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
|
||||||
|
|
||||||
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
|
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
|
||||||
@ -86,10 +92,11 @@ class QGroundObjectGroupTemplate(QGroupBox):
|
|||||||
self.group_selector.stateChanged.connect(self.on_group_changed)
|
self.group_selector.stateChanged.connect(self.on_group_changed)
|
||||||
|
|
||||||
def on_group_changed(self) -> None:
|
def on_group_changed(self) -> None:
|
||||||
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
|
self.group_template.set_enabled(self.group_selector.isChecked())
|
||||||
count = self.amount_selector.value() if self.group_selector.isChecked() else 0
|
if self.group_template.can_be_modified:
|
||||||
self.group_template.randomizer.count = count
|
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
|
||||||
self.group_template.randomizer.force_type(unit_type.dcs_id)
|
self.group_template.unit_count = [self.amount_selector.value()]
|
||||||
|
self.group_template.set_unit_type(unit_type.dcs_id)
|
||||||
self.group_template_changed.emit(self.group_template)
|
self.group_template_changed.emit(self.group_template)
|
||||||
|
|
||||||
|
|
||||||
@ -143,8 +150,6 @@ class QGroundObjectTemplateLayout(QGroupBox):
|
|||||||
|
|
||||||
def update_price(self) -> None:
|
def update_price(self) -> None:
|
||||||
price = "$" + str(self.template.estimated_price_for(self.ground_object))
|
price = "$" + str(self.template.estimated_price_for(self.ground_object))
|
||||||
if self.template.randomizable:
|
|
||||||
price = "~" + price
|
|
||||||
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
|
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
|
||||||
|
|
||||||
def buy_group(self):
|
def buy_group(self):
|
||||||
@ -216,30 +221,40 @@ class QGroundObjectBuyMenu(QDialog):
|
|||||||
self.mainLayout = QGridLayout()
|
self.mainLayout = QGridLayout()
|
||||||
self.setLayout(self.mainLayout)
|
self.setLayout(self.mainLayout)
|
||||||
|
|
||||||
|
self.unit_group_selector = QComboBox()
|
||||||
self.template_selector = QComboBox()
|
self.template_selector = QComboBox()
|
||||||
self.template_selector.currentIndexChanged.connect(self.template_changed)
|
self.template_selector.setEnabled(False)
|
||||||
|
|
||||||
# Get the templates and fill the combobox
|
# Get the templates and fill the combobox
|
||||||
template_sub_category = None
|
template_sub_category = None
|
||||||
|
tasks = []
|
||||||
if isinstance(ground_object, SamGroundObject):
|
if isinstance(ground_object, SamGroundObject):
|
||||||
template_category = TemplateCategory.AirDefence
|
role = GroupRole.AntiAir
|
||||||
elif isinstance(ground_object, VehicleGroupGroundObject):
|
elif isinstance(ground_object, VehicleGroupGroundObject):
|
||||||
template_category = TemplateCategory.Armor
|
role = GroupRole.GroundForce
|
||||||
elif isinstance(ground_object, EwrGroundObject):
|
elif isinstance(ground_object, EwrGroundObject):
|
||||||
template_category = TemplateCategory.AirDefence
|
role = GroupRole.AntiAir
|
||||||
template_sub_category = "EWR"
|
tasks.append(GroupTask.EWR)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
for template in game.blue.faction.templates.for_category(
|
if not tasks:
|
||||||
template_category, template_sub_category
|
tasks = ROLE_TASKINGS[role]
|
||||||
):
|
|
||||||
self.template_selector.addItem(template.name, userData=template)
|
for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks):
|
||||||
|
self.unit_group_selector.addItem(unit_group.name, userData=unit_group)
|
||||||
|
|
||||||
|
self.template_selector.currentIndexChanged.connect(self.template_changed)
|
||||||
|
self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed)
|
||||||
|
|
||||||
template_selector_layout = QGridLayout()
|
template_selector_layout = QGridLayout()
|
||||||
template_selector_layout.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft)
|
template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
|
||||||
template_selector_layout.addWidget(
|
template_selector_layout.addWidget(
|
||||||
self.template_selector, 0, 1, alignment=Qt.AlignRight
|
self.unit_group_selector, 0, 1, alignment=Qt.AlignRight
|
||||||
|
)
|
||||||
|
template_selector_layout.addWidget(QLabel("Template :"), 1, 0, Qt.AlignLeft)
|
||||||
|
template_selector_layout.addWidget(
|
||||||
|
self.template_selector, 1, 1, alignment=Qt.AlignRight
|
||||||
)
|
)
|
||||||
self.mainLayout.addLayout(template_selector_layout, 0, 0)
|
self.mainLayout.addLayout(template_selector_layout, 0, 0)
|
||||||
|
|
||||||
@ -250,10 +265,22 @@ class QGroundObjectBuyMenu(QDialog):
|
|||||||
self.setLayout(self.mainLayout)
|
self.setLayout(self.mainLayout)
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
self.template_changed()
|
self.unit_group_changed()
|
||||||
|
|
||||||
|
def unit_group_changed(self) -> None:
|
||||||
|
unit_group = self.unit_group_selector.itemData(
|
||||||
|
self.unit_group_selector.currentIndex()
|
||||||
|
)
|
||||||
|
self.template_selector.clear()
|
||||||
|
if unit_group.templates:
|
||||||
|
for template in unit_group.templates:
|
||||||
|
self.template_selector.addItem(template.name, userData=template)
|
||||||
|
# Enable if more than one template is available
|
||||||
|
self.template_selector.setEnabled(len(unit_group.templates) > 1)
|
||||||
|
|
||||||
def template_changed(self):
|
def template_changed(self):
|
||||||
template = self.template_selector.itemData(
|
template = self.template_selector.itemData(
|
||||||
self.template_selector.currentIndex()
|
self.template_selector.currentIndex()
|
||||||
)
|
)
|
||||||
self.template_changed_signal.emit(template)
|
if template is not None:
|
||||||
|
self.template_changed_signal.emit(template)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from dcs import Point, vehicles
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.config import REWARDS
|
from game.config import REWARDS
|
||||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import ControlPoint, TheaterGroundObject
|
from game.theater import ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
@ -181,8 +181,11 @@ class QGroundObjectMenu(QDialog):
|
|||||||
return
|
return
|
||||||
for u in self.ground_object.units:
|
for u in self.ground_object.units:
|
||||||
# Hack: Unknown variant.
|
# Hack: Unknown variant.
|
||||||
unit_type = next(GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type]))
|
if u.type in vehicles.vehicle_map:
|
||||||
total_value += unit_type.price
|
unit_type = next(
|
||||||
|
GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])
|
||||||
|
)
|
||||||
|
total_value += unit_type.price
|
||||||
if self.sell_all_button is not None:
|
if self.sell_all_button is not None:
|
||||||
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
|
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
|
||||||
self.total_value = total_value
|
self.total_value = total_value
|
||||||
|
|||||||
@ -52,18 +52,9 @@
|
|||||||
"sams": [
|
"sams": [
|
||||||
"HawkGenerator"
|
"HawkGenerator"
|
||||||
],
|
],
|
||||||
"aircraft_carrier": [
|
"naval_units": [
|
||||||
"Stennis"
|
"LHA-1 Tarawa",
|
||||||
],
|
"CVN-74 John C. Stennis"
|
||||||
"helicopter_carrier": [
|
|
||||||
"LHA_Tarawa"
|
|
||||||
],
|
|
||||||
"destroyers": [
|
|
||||||
"PERRY",
|
|
||||||
"USS_Arleigh_Burke_IIa"
|
|
||||||
],
|
|
||||||
"cruisers": [
|
|
||||||
"TICONDEROG"
|
|
||||||
],
|
],
|
||||||
"requirements": {"mod": "Some mod is required"},
|
"requirements": {"mod": "Some mod is required"},
|
||||||
"carrier_names": [
|
"carrier_names": [
|
||||||
@ -79,10 +70,6 @@
|
|||||||
"LHA-4 Nassau",
|
"LHA-4 Nassau",
|
||||||
"LHA-5 Peleliu"
|
"LHA-5 Peleliu"
|
||||||
],
|
],
|
||||||
"navy_generators": [
|
|
||||||
"OliverHazardPerryGroupGenerator",
|
|
||||||
"ArleighBurkeGroupGenerator"
|
|
||||||
],
|
|
||||||
"has_jtac": true,
|
"has_jtac": true,
|
||||||
"jtac_unit": "MQ_9_Reaper"
|
"jtac_unit": "MQ_9_Reaper"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,15 +95,8 @@ class TestFactionLoader(unittest.TestCase):
|
|||||||
self.assertIn(Infantry.Soldier_M4, faction.infantry_units)
|
self.assertIn(Infantry.Soldier_M4, faction.infantry_units)
|
||||||
self.assertIn(Infantry.Soldier_M249, faction.infantry_units)
|
self.assertIn(Infantry.Soldier_M249, faction.infantry_units)
|
||||||
|
|
||||||
self.assertIn("AvengerGenerator", faction.air_defenses)
|
self.assertIn(Stennis.name, faction.naval_units)
|
||||||
|
self.assertIn(LHA_Tarawa.name, faction.naval_units)
|
||||||
self.assertIn("HawkGenerator", faction.air_defenses)
|
|
||||||
|
|
||||||
self.assertIn(Stennis, faction.aircraft_carrier)
|
|
||||||
self.assertIn(LHA_Tarawa, faction.helicopter_carrier)
|
|
||||||
self.assertIn(PERRY, faction.destroyers)
|
|
||||||
self.assertIn(USS_Arleigh_Burke_IIa, faction.destroyers)
|
|
||||||
self.assertIn(TICONDEROG, faction.cruisers)
|
|
||||||
|
|
||||||
self.assertIn("mod", faction.requirements.keys())
|
self.assertIn("mod", faction.requirements.keys())
|
||||||
self.assertIn("Some mod is required", faction.requirements.values())
|
self.assertIn("Some mod is required", faction.requirements.values())
|
||||||
@ -111,9 +104,6 @@ class TestFactionLoader(unittest.TestCase):
|
|||||||
self.assertEqual(4, len(faction.carrier_names))
|
self.assertEqual(4, len(faction.carrier_names))
|
||||||
self.assertEqual(5, len(faction.helicopter_carrier_names))
|
self.assertEqual(5, len(faction.helicopter_carrier_names))
|
||||||
|
|
||||||
self.assertIn("OliverHazardPerryGroupGenerator", faction.navy_generators)
|
|
||||||
self.assertIn("ArleighBurkeGroupGenerator", faction.navy_generators)
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Faction unit names in the json files are outdated")
|
@pytest.mark.skip(reason="Faction unit names in the json files are outdated")
|
||||||
def test_load_valid_faction_with_invalid_country(self) -> None:
|
def test_load_valid_faction_with_invalid_country(self) -> None:
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user