mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Refactor ground objects and prepare template system
- completly refactored the way TGO handles groups and replaced the usage of the pydcs ground groups (vehicle, ship, static) with an own Group and Unit class. - this allows us to only take care of dcs group generation during miz generation, where it should have always been. - We can now have any type of unit (even statics) in the same logic ground group we handle in liberation. this is independent from the dcs group handling. the dcs group will only be genarted when takeoff is pressed. - Refactored the unitmap and the scenery object handling to adopt to changes that now TGOs can hold all Units we want. - Cleaned up many many many lines of uneeded hacks to build stuff around dcs groups. - Removed IDs for TGOs as the names we generate are unique and for liberation to work we need no ids. Unique IDs for dcs will be generated for the units and groups only.
This commit is contained in:
parent
6baf36c587
commit
d154069877
@ -10,7 +10,7 @@ from game.ato.flighttype import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
|
||||
class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.strike_targets:
|
||||
return False
|
||||
|
||||
@ -57,7 +57,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
enemy_ships: list[NavalGroundObject]
|
||||
enemy_garrisons: dict[ControlPoint, Garrisons]
|
||||
oca_targets: list[ControlPoint]
|
||||
strike_targets: list[TheaterGroundObject[Any]]
|
||||
strike_targets: list[TheaterGroundObject]
|
||||
enemy_barcaps: list[ControlPoint]
|
||||
threat_zones: ThreatZones
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
|
||||
@ -38,5 +37,5 @@ class AlicCodes:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def code_for(cls, unit: Unit) -> int:
|
||||
return cls.CODES[unit.type]
|
||||
def code_for(cls, unit_type: str) -> int:
|
||||
return cls.CODES[unit_type]
|
||||
|
||||
@ -5,6 +5,7 @@ from dcs.planes import plane_map
|
||||
from dcs.ships import ship_map
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.statics import fortification_map, groundobject_map, warehouse_map, cargo_map
|
||||
|
||||
|
||||
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
@ -16,5 +17,19 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
return ship_map[name]
|
||||
if name in helicopter_map:
|
||||
return helicopter_map[name]
|
||||
else:
|
||||
# Try statics
|
||||
return static_type_from_name(name)
|
||||
|
||||
|
||||
def static_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
if name in fortification_map:
|
||||
return fortification_map[name]
|
||||
elif name in groundobject_map:
|
||||
return groundobject_map[name]
|
||||
elif name in warehouse_map:
|
||||
return warehouse_map[name]
|
||||
if name in cargo_map:
|
||||
return cargo_map[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -23,12 +23,12 @@ if TYPE_CHECKING:
|
||||
from game.transfers import CargoShip
|
||||
from game.unitmap import (
|
||||
AirliftUnits,
|
||||
Building,
|
||||
ConvoyUnit,
|
||||
FlyingUnit,
|
||||
FrontLineUnit,
|
||||
GroundObjectUnit,
|
||||
GroundObjectMapping,
|
||||
UnitMap,
|
||||
SceneryObjectMapping,
|
||||
)
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
@ -72,11 +72,11 @@ class GroundLosses:
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
player_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
|
||||
|
||||
player_buildings: List[Building] = field(default_factory=list)
|
||||
enemy_buildings: List[Building] = field(default_factory=list)
|
||||
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||
|
||||
player_airfields: List[Airfield] = field(default_factory=list)
|
||||
enemy_airfields: List[Airfield] = field(default_factory=list)
|
||||
@ -158,14 +158,14 @@ class Debriefing:
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectMapping]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@property
|
||||
def building_losses(self) -> Iterator[Building]:
|
||||
yield from self.ground_losses.player_buildings
|
||||
yield from self.ground_losses.enemy_buildings
|
||||
def scenery_object_losses(self) -> Iterator[SceneryObjectMapping]:
|
||||
yield from self.ground_losses.player_scenery
|
||||
yield from self.ground_losses.enemy_scenery
|
||||
|
||||
@property
|
||||
def damaged_runways(self) -> Iterator[Airfield]:
|
||||
@ -217,17 +217,32 @@ class Debriefing:
|
||||
losses_by_type[unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_buildings
|
||||
losses = self.ground_losses.player_ground_objects
|
||||
else:
|
||||
losses = self.ground_losses.enemy_buildings
|
||||
losses = self.ground_losses.enemy_ground_objects
|
||||
for loss in losses:
|
||||
if loss.ground_object.control_point.captured != player:
|
||||
continue
|
||||
# We do not have handling for ships and statics UniType yet so we have to
|
||||
# take more care here. Fallback for ship and static is to use the type str
|
||||
# which is the dcs_type.id
|
||||
unit_type = (
|
||||
loss.ground_unit.unit_type.name
|
||||
if loss.ground_unit.unit_type
|
||||
else loss.ground_unit.type
|
||||
)
|
||||
losses_by_type[unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
losses_by_type[loss.ground_object.dcs_identifier] += 1
|
||||
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_scenery
|
||||
else:
|
||||
losses = self.ground_losses.enemy_scenery
|
||||
for loss in losses:
|
||||
losses_by_type[loss.trigger_zone.name] += 1
|
||||
return losses_by_type
|
||||
|
||||
def dead_aircraft(self) -> AirLosses:
|
||||
@ -271,25 +286,21 @@ class Debriefing:
|
||||
losses.enemy_cargo_ships.append(cargo_ship)
|
||||
continue
|
||||
|
||||
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
|
||||
if ground_object_unit is not None:
|
||||
if ground_object_unit.ground_object.control_point.captured:
|
||||
losses.player_ground_objects.append(ground_object_unit)
|
||||
ground_object = self.unit_map.ground_object(unit_name)
|
||||
if ground_object is not None:
|
||||
if ground_object.ground_unit.ground_object.is_friendly(to_player=True):
|
||||
losses.player_ground_objects.append(ground_object)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object_unit)
|
||||
losses.enemy_ground_objects.append(ground_object)
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
scenery_object = self.unit_map.scenery_object(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
if scenery_object is not None:
|
||||
if scenery_object.ground_unit.ground_object.is_friendly(to_player=True):
|
||||
losses.player_scenery.append(scenery_object)
|
||||
else:
|
||||
losses.enemy_buildings.append(building)
|
||||
losses.enemy_scenery.append(scenery_object)
|
||||
continue
|
||||
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
|
||||
@ -45,6 +45,7 @@ if TYPE_CHECKING:
|
||||
from .navmesh import NavMesh
|
||||
from .squadrons import AirWing
|
||||
from .threatzones import ThreatZones
|
||||
from .factions.faction import Faction
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
|
||||
@ -19,7 +19,9 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
elif isinstance(target, MultiGroupTransport):
|
||||
group_names.append(target.name)
|
||||
elif isinstance(target, NavalControlPoint):
|
||||
group_names.append(target.get_carrier_group_name())
|
||||
carrier_name = target.get_carrier_group_name()
|
||||
if carrier_name:
|
||||
group_names.append(carrier_name)
|
||||
else:
|
||||
logging.error(
|
||||
"Unexpected target type for BAI mission: %s",
|
||||
@ -28,12 +30,12 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
return
|
||||
|
||||
for group_name in group_names:
|
||||
group = self.mission.find_group(group_name)
|
||||
if group is None:
|
||||
miz_group = self.mission.find_group(group_name)
|
||||
if miz_group is None:
|
||||
logging.error("Could not find group for BAI mission %s", group_name)
|
||||
continue
|
||||
|
||||
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
|
||||
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
|
||||
@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
|
||||
from game.ato import Flight, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.missiongenerator.airsupport import AirSupport
|
||||
from game.theater import MissionTarget
|
||||
from game.theater import MissionTarget, GroundUnit
|
||||
|
||||
TARGET_WAYPOINTS = (
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
|
||||
return False
|
||||
|
||||
def register_special_waypoints(
|
||||
self, targets: Iterable[Union[MissionTarget, Unit]]
|
||||
self, targets: Iterable[Union[MissionTarget, GroundUnit]]
|
||||
) -> None:
|
||||
"""Create special target waypoints for various aircraft"""
|
||||
for i, t in enumerate(targets):
|
||||
|
||||
@ -32,7 +32,6 @@ from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from dcs.mission import Mission
|
||||
from dcs.unit import Unit
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
@ -41,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.data.alic import AlicCodes
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.radio.radios import RadioFrequency
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject
|
||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.weather import Weather
|
||||
@ -608,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
|
||||
self.theater = theater
|
||||
|
||||
@property
|
||||
def target_units(self) -> Iterator[Unit]:
|
||||
def target_units(self) -> Iterator[GroundUnit]:
|
||||
if isinstance(self.flight.package.target, TheaterGroundObject):
|
||||
yield from self.flight.package.target.units
|
||||
yield from self.flight.package.target.strike_targets
|
||||
|
||||
@staticmethod
|
||||
def alic_for(unit: Unit) -> str:
|
||||
def alic_for(unit_type: str) -> str:
|
||||
try:
|
||||
return str(AlicCodes.code_for(unit))
|
||||
return str(AlicCodes.code_for(unit_type))
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
@ -635,11 +634,15 @@ class SeadTaskPage(KneeboardPage):
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def target_info_row(self, unit: Unit) -> List[str]:
|
||||
def target_info_row(self, unit: GroundUnit) -> List[str]:
|
||||
ll = self.theater.point_to_ll(unit.position)
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
name = unit.name if unit_type is None else unit_type.name
|
||||
return [name, self.alic_for(unit), ll.format_dms(include_decimal_seconds=True)]
|
||||
return [
|
||||
name,
|
||||
self.alic_for(unit.type),
|
||||
ll.format_dms(include_decimal_seconds=True),
|
||||
]
|
||||
|
||||
|
||||
class StrikeTaskPage(KneeboardPage):
|
||||
|
||||
@ -20,6 +20,8 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
TypeVar,
|
||||
List,
|
||||
Any,
|
||||
Union,
|
||||
)
|
||||
|
||||
@ -49,24 +51,23 @@ from dcs.task import (
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
|
||||
from dcs.unit import InvisibleFARP, Ship, Unit, Vehicle
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup, MovingGroup
|
||||
from dcs.unittype import ShipType, StaticType, VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from game.dcs.helpers import static_type_from_name, unit_type_from_name
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
FactoryGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SceneryGroundObject,
|
||||
ShipGroundObject,
|
||||
GroundGroup,
|
||||
GroundUnit,
|
||||
SceneryGroundUnit,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading, feet, knots, mps
|
||||
@ -79,18 +80,12 @@ FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
|
||||
|
||||
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
|
||||
|
||||
|
||||
class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||
"""An unspecialized ground object generator.
|
||||
|
||||
Currently used only for SAM
|
||||
"""
|
||||
class GroundObjectGenerator:
|
||||
"""generates the DCS groups and units from the TheaterGroundObject"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: TgoT,
|
||||
ground_object: TheaterGroundObject,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
@ -106,7 +101,7 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||
def culled(self) -> bool:
|
||||
return self.game.iads_considerate_culling(self.ground_object)
|
||||
|
||||
def generate(self) -> None:
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
if self.culled:
|
||||
return
|
||||
|
||||
@ -114,29 +109,102 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
group_name = group.group_name if unique_name else group.name
|
||||
if group.static_group:
|
||||
# Static Group
|
||||
for i, u in enumerate(group.units):
|
||||
if isinstance(u, SceneryGroundUnit):
|
||||
# Special handling for scenery objects:
|
||||
# Only create a trigger zone and no "real" dcs unit
|
||||
self.add_trigger_zone_for_scenery(u)
|
||||
continue
|
||||
|
||||
unit_type = vehicle_map[group.units[0].type]
|
||||
vg = self.m.vehicle_group(
|
||||
self.country,
|
||||
group.name,
|
||||
unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading,
|
||||
)
|
||||
vg.units[0].name = group.units[0].name
|
||||
vg.units[0].player_can_drive = True
|
||||
for i, u in enumerate(group.units):
|
||||
if i > 0:
|
||||
vehicle = Vehicle(self.m.next_unit_id(), u.name, u.type)
|
||||
vehicle.position.x = u.position.x
|
||||
vehicle.position.y = u.position.y
|
||||
vehicle.heading = u.heading
|
||||
vehicle.player_can_drive = True
|
||||
vg.add_unit(vehicle)
|
||||
# Only skip dead units after trigger zone for scenery created!
|
||||
if not u.alive:
|
||||
continue
|
||||
|
||||
self.enable_eplrs(vg, unit_type)
|
||||
self.set_alarm_state(vg)
|
||||
self._register_unit_group(group, vg)
|
||||
unit_type = static_type_from_name(u.type)
|
||||
if not unit_type:
|
||||
raise RuntimeError(
|
||||
f"Unit type {u.type} is not a valid dcs unit type"
|
||||
)
|
||||
|
||||
sg = self.m.static_group(
|
||||
country=self.country,
|
||||
name=u.unit_name if unique_name else u.name,
|
||||
_type=unit_type,
|
||||
position=u.position,
|
||||
heading=u.position.heading.degrees,
|
||||
dead=not u.alive,
|
||||
)
|
||||
self._register_ground_unit(u, sg.units[0])
|
||||
else:
|
||||
# Moving Group
|
||||
moving_group: Optional[MovingGroup[Any]] = None
|
||||
for i, unit in enumerate(group.units):
|
||||
if not unit.alive:
|
||||
continue
|
||||
if unit.type in vehicle_map:
|
||||
# Vehicle Group
|
||||
unit_type = vehicle_map[unit.type]
|
||||
elif unit.type in ship_map:
|
||||
# Ship Group
|
||||
unit_type = ship_map[group.units[0].type]
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Unit type {unit.type} is not a valid dcs unit type"
|
||||
)
|
||||
|
||||
unit_name = unit.unit_name if unique_name else unit.name
|
||||
if moving_group is None:
|
||||
# First unit of the group will create the dcs group
|
||||
if issubclass(unit_type, VehicleType):
|
||||
moving_group = self.m.vehicle_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit_type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
moving_group.units[0].player_can_drive = True
|
||||
self.enable_eplrs(moving_group, unit_type)
|
||||
if issubclass(unit_type, ShipType):
|
||||
moving_group = self.m.ship_group(
|
||||
self.country,
|
||||
group_name,
|
||||
unit_type,
|
||||
position=unit.position,
|
||||
heading=unit.position.heading.degrees,
|
||||
)
|
||||
if moving_group:
|
||||
moving_group.units[0].name = unit_name
|
||||
self.set_alarm_state(moving_group)
|
||||
self._register_ground_unit(unit, moving_group.units[0])
|
||||
else:
|
||||
raise RuntimeError("DCS Group creation failed")
|
||||
else:
|
||||
# Additional Units in the group
|
||||
dcs_unit: Optional[Unit] = None
|
||||
if issubclass(unit_type, VehicleType):
|
||||
dcs_unit = Vehicle(
|
||||
self.m.next_unit_id(),
|
||||
unit_name,
|
||||
unit.type,
|
||||
)
|
||||
dcs_unit.player_can_drive = True
|
||||
elif issubclass(unit_type, ShipType):
|
||||
dcs_unit = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit_name,
|
||||
unit_type,
|
||||
)
|
||||
if dcs_unit:
|
||||
dcs_unit.position = unit.position
|
||||
dcs_unit.heading = unit.position.heading.degrees
|
||||
moving_group.add_unit(dcs_unit)
|
||||
self._register_ground_unit(unit, dcs_unit)
|
||||
else:
|
||||
raise RuntimeError("DCS Unit creation failed")
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||
@ -149,24 +217,71 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
def _register_unit_group(
|
||||
def _register_ground_unit(
|
||||
self,
|
||||
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||
miz_group: Union[ShipGroup, VehicleGroup],
|
||||
ground_unit: GroundUnit,
|
||||
dcs_unit: Unit,
|
||||
) -> None:
|
||||
self.unit_map.add_ground_object_units(
|
||||
self.ground_object, persistence_group, miz_group
|
||||
self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
|
||||
|
||||
def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None:
|
||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||
color = (
|
||||
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||
if scenery.ground_object.is_friendly(to_player=True)
|
||||
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
|
||||
)
|
||||
|
||||
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap
|
||||
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
|
||||
smallest_valid_radius = feet(16).meters
|
||||
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
|
||||
trigger_zone = self.m.triggers.add_triggerzone(
|
||||
scenery.zone.position,
|
||||
smallest_valid_radius,
|
||||
scenery.zone.hidden,
|
||||
scenery.zone.name,
|
||||
color,
|
||||
scenery.zone.properties,
|
||||
)
|
||||
# DCS only visually shows a scenery object is dead when
|
||||
# this trigger rule is applied. Otherwise you can kill a
|
||||
# structure twice.
|
||||
if not scenery.alive:
|
||||
self.generate_destruction_trigger_rule(trigger_zone)
|
||||
else:
|
||||
self.generate_on_dead_trigger_rule(trigger_zone)
|
||||
|
||||
self.unit_map.add_scenery(scenery, trigger_zone)
|
||||
|
||||
def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None:
|
||||
# Add destruction zone trigger
|
||||
t = TriggerStart(comment="Destruction")
|
||||
t.actions.append(
|
||||
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
|
||||
)
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
def generate_on_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
|
||||
# Add a TriggerRule with the MapObjectIsDead condition to recognize killed
|
||||
# map objects and add them to the state.json with a DoScript
|
||||
t = TriggerOnce(Event.NoEvent, f"MapObjectIsDead Trigger {trigger_zone.id}")
|
||||
t.add_condition(MapObjectIsDead(trigger_zone.id))
|
||||
script_string = String(
|
||||
f'killed_ground_units[#killed_ground_units + 1] = "{trigger_zone.name}"'
|
||||
)
|
||||
t.actions.append(DoScript(script_string))
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
|
||||
class MissileSiteGenerator(GroundObjectGenerator):
|
||||
@property
|
||||
def culled(self) -> bool:
|
||||
# Don't cull missile sites - their range is long enough to make them easily
|
||||
# culled despite being a threat.
|
||||
return False
|
||||
|
||||
def generate(self) -> None:
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
# TODO : Should be pre-planned ?
|
||||
@ -221,148 +336,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
|
||||
return site_range
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
|
||||
"""Generator for building sites.
|
||||
|
||||
Building sites are the primary type of non-airbase objective locations that
|
||||
appear on the map. They come in a handful of variants each with different
|
||||
types of buildings and ground units.
|
||||
"""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
if self.ground_object.dcs_identifier in warehouse_map:
|
||||
static_type = warehouse_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in fortification_map:
|
||||
static_type = fortification_map[self.ground_object.dcs_identifier]
|
||||
self.generate_static(static_type)
|
||||
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
|
||||
for f in FORTIFICATION_UNITS:
|
||||
if f.id == self.ground_object.dcs_identifier:
|
||||
unit_type = f
|
||||
self.generate_vehicle_group(unit_type)
|
||||
break
|
||||
else:
|
||||
logging.error(
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps"
|
||||
)
|
||||
|
||||
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
group = self.m.vehicle_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=unit_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading.degrees,
|
||||
)
|
||||
self._register_fortification(group)
|
||||
|
||||
def generate_static(self, static_type: Type[StaticType]) -> None:
|
||||
group = self.m.static_group(
|
||||
country=self.country,
|
||||
name=self.ground_object.group_name,
|
||||
_type=static_type,
|
||||
position=self.ground_object.position,
|
||||
heading=self.ground_object.heading.degrees,
|
||||
dead=self.ground_object.is_dead,
|
||||
)
|
||||
self._register_building(group)
|
||||
|
||||
def _register_fortification(self, fortification: VehicleGroup) -> None:
|
||||
assert isinstance(self.ground_object, BuildingGroundObject)
|
||||
self.unit_map.add_fortification(self.ground_object, fortification)
|
||||
|
||||
def _register_building(self, building: StaticGroup) -> None:
|
||||
assert isinstance(self.ground_object, BuildingGroundObject)
|
||||
self.unit_map.add_building(self.ground_object, building)
|
||||
|
||||
|
||||
class FactoryGenerator(BuildingSiteGenerator):
|
||||
"""Generator for factory sites.
|
||||
|
||||
Factory sites are the buildings that allow the recruitment of ground units.
|
||||
Destroying these prevents the owner from recruiting ground units at the connected
|
||||
control point.
|
||||
"""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
# TODO: Faction specific?
|
||||
self.generate_static(Fortification.Workshop_A)
|
||||
|
||||
|
||||
class SceneryGenerator(BuildingSiteGenerator):
|
||||
def generate(self) -> None:
|
||||
assert isinstance(self.ground_object, SceneryGroundObject)
|
||||
|
||||
trigger_zone = self.generate_trigger_zone(self.ground_object)
|
||||
|
||||
# DCS only visually shows a scenery object is dead when
|
||||
# this trigger rule is applied. Otherwise you can kill a
|
||||
# structure twice.
|
||||
if self.ground_object.is_dead:
|
||||
self.generate_destruction_trigger_rule(trigger_zone)
|
||||
else:
|
||||
self.generate_on_dead_trigger_rule(trigger_zone)
|
||||
|
||||
# Tell Liberation to manage this groundobjectsgen as part of the campaign.
|
||||
self.register_scenery()
|
||||
|
||||
def generate_trigger_zone(self, scenery: SceneryGroundObject) -> TriggerZone:
|
||||
|
||||
zone = scenery.zone
|
||||
|
||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||
if scenery.is_friendly(to_player=True):
|
||||
color = {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||
else:
|
||||
color = {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
|
||||
|
||||
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap
|
||||
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
|
||||
smallest_valid_radius = feet(16).meters
|
||||
|
||||
return self.m.triggers.add_triggerzone(
|
||||
zone.position,
|
||||
smallest_valid_radius,
|
||||
zone.hidden,
|
||||
zone.name,
|
||||
color,
|
||||
zone.properties,
|
||||
)
|
||||
|
||||
def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None:
|
||||
# Add destruction zone trigger
|
||||
t = TriggerStart(comment="Destruction")
|
||||
t.actions.append(
|
||||
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
|
||||
)
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
def generate_on_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
|
||||
# Add a TriggerRule with the MapObjectIsDead condition to recognize killed
|
||||
# map objects and add them to the state.json with a DoScript
|
||||
t = TriggerOnce(Event.NoEvent, f"MapObjectIsDead Trigger {trigger_zone.id}")
|
||||
t.add_condition(MapObjectIsDead(trigger_zone.id))
|
||||
script_string = String(
|
||||
f'killed_ground_units[#killed_ground_units + 1] = "{trigger_zone.name}"'
|
||||
)
|
||||
t.actions.append(DoScript(script_string))
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
def register_scenery(self) -> None:
|
||||
scenery = self.ground_object
|
||||
assert isinstance(scenery, SceneryGroundObject)
|
||||
self.unit_map.add_scenery(scenery)
|
||||
|
||||
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
|
||||
class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
"""Base type for carrier group generation.
|
||||
|
||||
Used by both CV(N) groups and LHA groups.
|
||||
@ -389,67 +363,73 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
|
||||
self.icls_alloc = icls_alloc
|
||||
self.runways = runways
|
||||
|
||||
def generate(self) -> None:
|
||||
# TODO: Require single group?
|
||||
for group in self.ground_object.groups:
|
||||
def generate(self, unique_name: bool = True) -> None:
|
||||
|
||||
# This can also be refactored as the general generation was updated
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
|
||||
for g_id, group in enumerate(self.ground_object.groups):
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty carrier group in {self.control_point}")
|
||||
continue
|
||||
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
ship_group = self.configure_carrier(group, atc)
|
||||
for unit in group.units[1:]:
|
||||
ship_group.add_unit(self.create_ship(unit, atc))
|
||||
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
# Correct unit type for the carrier.
|
||||
# This is only used for the super carrier setting
|
||||
unit_type = (
|
||||
self.get_carrier_type(group)
|
||||
if g_id == 0
|
||||
else ship_map[group.units[0].type]
|
||||
)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
group.name,
|
||||
unit_type,
|
||||
position=group.units[0].position,
|
||||
heading=group.units[0].position.heading.degrees,
|
||||
)
|
||||
|
||||
ship_group.set_frequency(atc.hertz)
|
||||
ship_group.units[0].name = (
|
||||
group.units[0].unit_name if unique_name else group.units[0].name
|
||||
)
|
||||
self._register_ground_unit(group.units[0], ship_group.units[0])
|
||||
|
||||
for unit in group.units[1:]:
|
||||
ship = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit.unit_name if unique_name else unit.name,
|
||||
unit_type_from_name(unit.type),
|
||||
)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.position.heading.degrees
|
||||
ship.set_frequency(atc.hertz)
|
||||
ship_group.add_unit(ship)
|
||||
self._register_ground_unit(unit, ship)
|
||||
|
||||
# Always steam into the wind, even if the carrier is being moved.
|
||||
# There are multiple unsimulated hours between turns, so we can
|
||||
# count those as the time the carrier uses to move and the mission
|
||||
# time as the recovery window.
|
||||
brc = self.steam_into_wind(ship_group)
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||
self.add_runway_data(
|
||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||
)
|
||||
self._register_unit_group(group, ship_group)
|
||||
|
||||
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
|
||||
# Set Carrier Specific Options
|
||||
if g_id == 0:
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||
self.add_runway_data(
|
||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||
)
|
||||
|
||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
||||
return ship_map[group.units[0].type]
|
||||
|
||||
def configure_carrier(
|
||||
self, group: ShipGroup, atc_channel: RadioFrequency
|
||||
) -> ShipGroup:
|
||||
unit_type = self.get_carrier_type(group)
|
||||
|
||||
ship_group = self.m.ship_group(
|
||||
self.country,
|
||||
group.name,
|
||||
unit_type,
|
||||
position=group.position,
|
||||
heading=group.units[0].heading,
|
||||
)
|
||||
ship_group.set_frequency(atc_channel.hertz)
|
||||
ship_group.units[0].name = group.units[0].name
|
||||
return ship_group
|
||||
|
||||
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
|
||||
ship = Ship(
|
||||
self.m.next_unit_id(),
|
||||
unit.name,
|
||||
unit_type_from_name(unit.type),
|
||||
)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
# TODO: Verify.
|
||||
ship.set_frequency(atc_channel.hertz)
|
||||
return ship
|
||||
|
||||
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
|
||||
wind = self.game.conditions.weather.wind.at_0m
|
||||
brc = Heading.from_degrees(wind.direction).opposite
|
||||
@ -515,7 +495,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
|
||||
class CarrierGenerator(GenericCarrierGenerator):
|
||||
"""Generator for CV(N) groups."""
|
||||
|
||||
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
|
||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||
@ -579,42 +559,6 @@ class LhaGenerator(GenericCarrierGenerator):
|
||||
)
|
||||
|
||||
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
|
||||
"""Generator for non-carrier naval groups."""
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
self.generate_group(group, ship_map[group.units[0].type])
|
||||
|
||||
def generate_group(
|
||||
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
|
||||
) -> None:
|
||||
group = self.m.ship_group(
|
||||
self.country,
|
||||
group_def.name,
|
||||
first_unit_type,
|
||||
position=group_def.position,
|
||||
heading=group_def.units[0].heading,
|
||||
)
|
||||
group.units[0].name = group_def.units[0].name
|
||||
# TODO: Skipping the first unit looks like copy pasta from the carrier.
|
||||
for unit in group_def.units[1:]:
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
ship = Ship(self.m.next_unit_id(), unit.name, unit_type)
|
||||
ship.position.x = unit.position.x
|
||||
ship.position.y = unit.position.y
|
||||
ship.heading = unit.heading
|
||||
group.add_unit(ship)
|
||||
self.set_alarm_state(group)
|
||||
self._register_unit_group(group_def, group)
|
||||
|
||||
|
||||
class HelipadGenerator:
|
||||
"""
|
||||
Generates helipads for given control point
|
||||
@ -726,20 +670,8 @@ class TgoGenerator:
|
||||
self.helipads[cp] = helipad_gen.helipads
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
generator: GenericGroundObjectGenerator[Any]
|
||||
if isinstance(ground_object, FactoryGroundObject):
|
||||
generator = FactoryGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, SceneryGroundObject):
|
||||
generator = SceneryGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, CarrierGroundObject):
|
||||
generator: GroundObjectGenerator
|
||||
if isinstance(ground_object, CarrierGroundObject):
|
||||
generator = CarrierGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
@ -753,7 +685,7 @@ class TgoGenerator:
|
||||
self.unit_map,
|
||||
)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
generator = CarrierGenerator(
|
||||
generator = LhaGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
country,
|
||||
@ -765,16 +697,12 @@ class TgoGenerator:
|
||||
self.runways,
|
||||
self.unit_map,
|
||||
)
|
||||
elif isinstance(ground_object, ShipGroundObject):
|
||||
generator = ShipObjectGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, MissileSiteGroundObject):
|
||||
generator = MissileSiteGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
else:
|
||||
generator = GenericGroundObjectGenerator(
|
||||
generator = GroundObjectGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
generator.generate()
|
||||
|
||||
@ -11,9 +11,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class DefendingSam(FrozenCombat):
|
||||
def __init__(
|
||||
self, flight: Flight, air_defenses: list[TheaterGroundObject[Any]]
|
||||
) -> None:
|
||||
def __init__(self, flight: Flight, air_defenses: list[TheaterGroundObject]) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.air_defenses = air_defenses
|
||||
|
||||
@ -17,7 +17,7 @@ class SamEngagementZones:
|
||||
def __init__(
|
||||
self,
|
||||
threat_zones: ThreatPoly,
|
||||
individual_zones: list[tuple[TheaterGroundObject[Any], ThreatPoly]],
|
||||
individual_zones: list[tuple[TheaterGroundObject, ThreatPoly]],
|
||||
) -> None:
|
||||
self.threat_zones = threat_zones
|
||||
self.individual_zones = individual_zones
|
||||
@ -25,9 +25,7 @@ class SamEngagementZones:
|
||||
def covers(self, position: Point) -> bool:
|
||||
return self.threat_zones.intersects(dcs_to_shapely_point(position))
|
||||
|
||||
def iter_threatening_sams(
|
||||
self, position: Point
|
||||
) -> Iterator[TheaterGroundObject[Any]]:
|
||||
def iter_threatening_sams(self, position: Point) -> Iterator[TheaterGroundObject]:
|
||||
for tgo, zone in self.individual_zones:
|
||||
if zone.intersects(dcs_to_shapely_point(position)):
|
||||
yield tgo
|
||||
@ -44,7 +42,7 @@ class SamEngagementZones:
|
||||
return SamEngagementZones(unary_union(commit_regions), individual_zones)
|
||||
|
||||
@classmethod
|
||||
def threat_region(cls, tgo: TheaterGroundObject[Any]) -> Optional[ThreatPoly]:
|
||||
def threat_region(cls, tgo: TheaterGroundObject) -> Optional[ThreatPoly]:
|
||||
threat_range = tgo.max_threat_range()
|
||||
if threat_range <= meters(0):
|
||||
return None
|
||||
|
||||
@ -29,8 +29,7 @@ class MissionResultsProcessor:
|
||||
self.commit_convoy_losses(debriefing)
|
||||
self.commit_cargo_ship_losses(debriefing)
|
||||
self.commit_airlift_losses(debriefing)
|
||||
self.commit_ground_object_losses(debriefing)
|
||||
self.commit_building_losses(debriefing)
|
||||
self.commit_ground_losses(debriefing)
|
||||
self.commit_damaged_runways(debriefing)
|
||||
self.commit_captures(debriefing)
|
||||
self.commit_front_line_battle_impact(debriefing)
|
||||
@ -131,23 +130,11 @@ class MissionResultsProcessor:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_ground_object_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.ground_object_losses:
|
||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||
if not hasattr(loss.group, "units_losts"):
|
||||
loss.group.units_losts = [] # type: ignore
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit) # type: ignore
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
self.game.message(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
)
|
||||
def commit_ground_losses(debriefing: Debriefing) -> None:
|
||||
for ground_object_loss in debriefing.ground_object_losses:
|
||||
ground_object_loss.ground_unit.kill()
|
||||
for scenery_object_loss in debriefing.scenery_object_losses:
|
||||
scenery_object_loss.ground_unit.kill()
|
||||
|
||||
@staticmethod
|
||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||
|
||||
@ -25,6 +25,7 @@ from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .latlon import LatLon
|
||||
from .projections import TransverseMercator
|
||||
from .seasonalconditions import SeasonalConditions
|
||||
from ..utils import Heading
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint, MissionTarget
|
||||
@ -85,7 +86,7 @@ class ConflictTheater:
|
||||
|
||||
def find_ground_objects_by_obj_name(
|
||||
self, obj_name: str
|
||||
) -> list[TheaterGroundObject[Any]]:
|
||||
) -> list[TheaterGroundObject]:
|
||||
found = []
|
||||
for cp in self.controlpoints:
|
||||
for g in cp.ground_objects:
|
||||
@ -265,6 +266,29 @@ class ConflictTheater:
|
||||
x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng)
|
||||
return Point(x, y)
|
||||
|
||||
def heading_to_conflict_from(self, position: Point) -> Optional[Heading]:
|
||||
# Heading for a Group to the enemy.
|
||||
# Should be the point between the nearest and the most distant conflict
|
||||
conflicts: dict[MissionTarget, float] = {}
|
||||
|
||||
for conflict in self.conflicts():
|
||||
conflicts[conflict] = conflict.position.distance_to_point(position)
|
||||
|
||||
if len(conflicts) == 0:
|
||||
return None
|
||||
|
||||
sorted_conflicts = [
|
||||
k for k, v in sorted(conflicts.items(), key=lambda item: item[1])
|
||||
]
|
||||
last = len(sorted_conflicts) - 1
|
||||
|
||||
conflict_center = Point(
|
||||
(sorted_conflicts[0].position.x + sorted_conflicts[last].position.x) / 2,
|
||||
(sorted_conflicts[0].position.y + sorted_conflicts[last].position.y) / 2,
|
||||
)
|
||||
|
||||
return Heading.from_degrees(position.heading_between_point(conflict_center))
|
||||
|
||||
|
||||
class CaucasusTheater(ConflictTheater):
|
||||
terrain = caucasus.Caucasus()
|
||||
|
||||
@ -42,6 +42,10 @@ from .theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
TheaterGroundObject,
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
GroundUnit,
|
||||
)
|
||||
from ..ato.starttype import StartType
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
@ -306,7 +310,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.starts_blue = starts_blue
|
||||
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
|
||||
@ -345,7 +349,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
return list(self.connected_objectives)
|
||||
|
||||
@property
|
||||
@ -517,7 +521,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
ControlPointType.LHA_GROUP,
|
||||
]:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier == "CARRIER":
|
||||
if isinstance(g, CarrierGroundObject):
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if unit_type_from_name(u.type) in [
|
||||
@ -526,7 +530,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
KUZNECOW,
|
||||
]:
|
||||
return group.name
|
||||
elif g.dcs_identifier == "LHA":
|
||||
elif isinstance(g, LhaGroundObject):
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if unit_type_from_name(u.type) in [LHA_Tarawa]:
|
||||
@ -539,7 +543,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
def find_ground_objects_by_obj_name(
|
||||
self, obj_name: str
|
||||
) -> list[TheaterGroundObject[Any]]:
|
||||
) -> list[TheaterGroundObject]:
|
||||
found = []
|
||||
for g in self.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
@ -857,7 +861,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
return []
|
||||
|
||||
@property
|
||||
@ -1000,10 +1004,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
def find_main_tgo(self) -> GenericCarrierGroundObject:
|
||||
for g in self.ground_objects:
|
||||
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [
|
||||
"CARRIER",
|
||||
"LHA",
|
||||
]:
|
||||
if isinstance(g, GenericCarrierGroundObject):
|
||||
return g
|
||||
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from dcs.unit import Unit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.theater.theatergroundobject import GroundUnit
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
@ -46,5 +47,5 @@ class MissionTarget:
|
||||
]
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
return []
|
||||
|
||||
@ -4,19 +4,23 @@ import itertools
|
||||
import logging
|
||||
from abc import ABC
|
||||
from collections.abc import Sequence
|
||||
from typing import Generic, Iterator, List, TYPE_CHECKING, TypeVar, Union
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import ShipGroup, VehicleGroup
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..utils import Distance, Heading, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.templates import UnitTemplate, GroupTemplate, TemplateRandomizer
|
||||
from .controlpoint import ControlPoint
|
||||
from ..ato.flighttype import FlightType
|
||||
|
||||
@ -45,56 +49,187 @@ NAME_BY_CATEGORY = {
|
||||
}
|
||||
|
||||
|
||||
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
|
||||
class SkynetRole(Enum):
|
||||
#: A radar SAM that should be controlled by Skynet.
|
||||
Sam = "Sam"
|
||||
|
||||
#: A radar SAM that should be controlled and used as an EWR by Skynet.
|
||||
SamAsEwr = "SamAsEwr"
|
||||
|
||||
#: An air defense unit that should be used as point defense by Skynet.
|
||||
PointDefense = "PD"
|
||||
|
||||
#: All other types of groups that might be present in a SAM TGO. This includes
|
||||
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
|
||||
#: should use this role.
|
||||
NoSkynetBehavior = "NoSkynetBehavior"
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
class AirDefenseRange(Enum):
|
||||
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
|
||||
Short = ("short", SkynetRole.NoSkynetBehavior)
|
||||
Medium = ("medium", SkynetRole.Sam)
|
||||
Long = ("long", SkynetRole.SamAsEwr)
|
||||
|
||||
def __init__(self, description: str, default_role: SkynetRole) -> None:
|
||||
self.range_name = description
|
||||
self.default_role = default_role
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundUnit:
|
||||
# Units can be everything.. Static, Vehicle, Ship.
|
||||
id: int
|
||||
name: str
|
||||
type: str # dcs.UnitType as string
|
||||
position: PointWithHeading
|
||||
ground_object: TheaterGroundObject
|
||||
alive: bool = True
|
||||
_dcs_type: Optional[GroundUnitType] = None
|
||||
|
||||
@staticmethod
|
||||
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit:
|
||||
return GroundUnit(
|
||||
id,
|
||||
t.name,
|
||||
t.type,
|
||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||
go,
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[GroundUnitType]:
|
||||
if not self._dcs_type:
|
||||
try:
|
||||
if self.type in vehicle_map:
|
||||
dcs_unit_type = vehicle_map[self.type]
|
||||
else:
|
||||
return None
|
||||
self._dcs_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
|
||||
except StopIteration:
|
||||
return None
|
||||
return self._dcs_type
|
||||
|
||||
def kill(self) -> None:
|
||||
self.alive = False
|
||||
|
||||
@property
|
||||
def unit_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
unit_label = self.unit_type or self.type or self.name
|
||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
||||
|
||||
|
||||
class SceneryGroundUnit(GroundUnit):
|
||||
"""Special GroundUnit for handling scenery ground objects"""
|
||||
|
||||
zone: TriggerZone
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundGroup:
|
||||
id: int
|
||||
name: str
|
||||
position: PointWithHeading
|
||||
units: list[GroundUnit]
|
||||
ground_object: TheaterGroundObject
|
||||
static_group: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_template(
|
||||
id: int,
|
||||
g: GroupTemplate,
|
||||
go: TheaterGroundObject,
|
||||
randomization: bool = True,
|
||||
) -> GroundGroup:
|
||||
|
||||
units = []
|
||||
if g.randomizer:
|
||||
g.randomizer.randomize()
|
||||
|
||||
for u_id, unit in enumerate(g.units):
|
||||
tgo_unit = GroundUnit.from_template(u_id, unit, go)
|
||||
if randomization and g.randomizer:
|
||||
if g.randomizer.unit_type:
|
||||
tgo_unit.type = g.randomizer.unit_type
|
||||
try:
|
||||
# Check if unit can be assigned
|
||||
g.randomizer.use_unit()
|
||||
except IndexError:
|
||||
# Do not generate the unit as no more units are available
|
||||
continue
|
||||
units.append(tgo_unit)
|
||||
|
||||
tgo_group = GroundGroup(
|
||||
id,
|
||||
g.name,
|
||||
PointWithHeading.from_point(go.position, go.heading),
|
||||
units,
|
||||
go,
|
||||
)
|
||||
|
||||
tgo_group.static_group = g.static
|
||||
|
||||
return tgo_group
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||
|
||||
@property
|
||||
def alive_units(self) -> int:
|
||||
return sum([unit.alive for unit in self.units])
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
sea_object: bool,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
self.category = category
|
||||
self.group_id = group_id
|
||||
self.heading = heading
|
||||
self.control_point = control_point
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[GroupT] = []
|
||||
self.groups: List[GroundGroup] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
return self.alive_unit_count == 0
|
||||
|
||||
@property
|
||||
def units(self) -> List[Unit]:
|
||||
def units(self) -> Iterator[GroundUnit]:
|
||||
"""
|
||||
:return: all the units at this location
|
||||
"""
|
||||
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
||||
yield from itertools.chain.from_iterable([g.units for g in self.groups])
|
||||
|
||||
@property
|
||||
def dead_units(self) -> List[Unit]:
|
||||
def statics(self) -> Iterator[GroundUnit]:
|
||||
for group in self.groups:
|
||||
if group.static_group:
|
||||
yield from group.units
|
||||
|
||||
@property
|
||||
def dead_units(self) -> list[GroundUnit]:
|
||||
"""
|
||||
:return: all the dead units at this location
|
||||
"""
|
||||
return list(
|
||||
itertools.chain.from_iterable(
|
||||
[getattr(g, "units_losts", []) for g in self.groups]
|
||||
)
|
||||
)
|
||||
return [unit for unit in self.units if not unit.alive]
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}"
|
||||
return f"{self.category}|{self.name}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
@ -103,9 +238,6 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
def __str__(self) -> str:
|
||||
return NAME_BY_CATEGORY[self.category]
|
||||
|
||||
def is_same_group(self, identifier: str) -> bool:
|
||||
return self.group_id == identifier
|
||||
|
||||
@property
|
||||
def obj_name(self) -> str:
|
||||
return self.name
|
||||
@ -135,7 +267,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
|
||||
@property
|
||||
def alive_unit_count(self) -> int:
|
||||
return sum(len(g.units) for g in self.groups)
|
||||
return sum([g.alive_units for g in self.groups])
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
@ -149,7 +281,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
|
||||
def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance:
|
||||
if not self.might_have_aa:
|
||||
return meters(0)
|
||||
|
||||
@ -170,7 +302,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
def max_detection_range(self) -> Distance:
|
||||
return max(self.detection_range(g) for g in self.groups)
|
||||
|
||||
def detection_range(self, group: GroupT) -> Distance:
|
||||
def detection_range(self, group: GroundGroup) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def max_threat_range(self) -> Distance:
|
||||
@ -178,7 +310,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
|
||||
)
|
||||
|
||||
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
@property
|
||||
@ -195,8 +327,8 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
return False
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
return self.units
|
||||
def strike_targets(self) -> list[GroundUnit]:
|
||||
return [unit for unit in self.units if unit.alive]
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
@ -214,70 +346,31 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
object_id: int,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
is_fob_structure: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
sea_object=False,
|
||||
)
|
||||
self.is_fob_structure = is_fob_structure
|
||||
self.object_id = object_id
|
||||
# Other TGOs track deadness based on the number of alive units, but
|
||||
# buildings don't have groups assigned to the TGO.
|
||||
self._dead = False
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
"""The name of the unit group."""
|
||||
return f"{self.category}|{self.group_id}|{self.object_id}"
|
||||
|
||||
@property
|
||||
def waypoint_name(self) -> str:
|
||||
return f"{super().waypoint_name} #{self.object_id}"
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
if not hasattr(self, "_dead"):
|
||||
self._dead = False
|
||||
return self._dead
|
||||
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
def iter_building_group(self) -> Iterator[BuildingGroundObject]:
|
||||
for tgo in self.control_point.ground_objects:
|
||||
if (
|
||||
tgo.obj_name == self.obj_name
|
||||
and not tgo.is_dead
|
||||
and isinstance(tgo, BuildingGroundObject)
|
||||
):
|
||||
yield tgo
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[BuildingGroundObject]:
|
||||
return list(self.iter_building_group())
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
for building in self.iter_building_group():
|
||||
yield building.position
|
||||
# Special handling to mark all buildings of the TGO
|
||||
for unit in self.strike_targets:
|
||||
yield unit.position
|
||||
|
||||
@property
|
||||
def is_control_point(self) -> bool:
|
||||
@ -298,55 +391,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
return meters(0)
|
||||
|
||||
|
||||
class SceneryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
object_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
zone: TriggerZone,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
object_id=object_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
is_fob_structure=False,
|
||||
)
|
||||
self.zone = zone
|
||||
|
||||
|
||||
class FactoryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
heading: Heading,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="factory",
|
||||
group_id=group_id,
|
||||
object_id=0,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="Workshop A",
|
||||
is_fob_structure=False,
|
||||
)
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
|
||||
class NavalGroundObject(TheaterGroundObject):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
@ -375,15 +420,13 @@ class GenericCarrierGroundObject(NavalGroundObject):
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||
def __init__(self, name: str, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="CARRIER",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@ -399,15 +442,13 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
|
||||
# TODO: Why is this both a CP and a TGO?
|
||||
class LhaGroundObject(GenericCarrierGroundObject):
|
||||
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
|
||||
def __init__(self, name: str, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="LHA",
|
||||
group_id=group_id,
|
||||
position=control_point.position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@ -421,18 +462,14 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
return f"LHA {self.name}"
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||
) -> None:
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="missile",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -445,11 +482,10 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
return False
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
heading: Heading,
|
||||
@ -457,11 +493,9 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="coastal",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -474,7 +508,7 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
return False
|
||||
|
||||
|
||||
class IadsGroundObject(TheaterGroundObject[VehicleGroup], ABC):
|
||||
class IadsGroundObject(TheaterGroundObject, ABC):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
@ -490,18 +524,15 @@ class SamGroundObject(IadsGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="aa",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -521,7 +552,7 @@ class SamGroundObject(IadsGroundObject):
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
||||
max_non_radar = meters(0)
|
||||
live_trs = set()
|
||||
max_telar_range = meters(0)
|
||||
@ -554,22 +585,19 @@ class SamGroundObject(IadsGroundObject):
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="armor",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -586,18 +614,15 @@ class EwrGroundObject(IadsGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="ewr",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -605,7 +630,7 @@ class EwrGroundObject(IadsGroundObject):
|
||||
def group_name(self) -> str:
|
||||
# Prefix the group names with the side color so Skynet can find them.
|
||||
# Use Group Id and uppercase EWR
|
||||
return f"{self.faction_color}|EWR|{self.group_id}"
|
||||
return f"{self.faction_color}|EWR|{self.name}"
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
@ -621,17 +646,13 @@ class EwrGroundObject(IadsGroundObject):
|
||||
|
||||
|
||||
class ShipGroundObject(NavalGroundObject):
|
||||
def __init__(
|
||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||
) -> None:
|
||||
def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category="ship",
|
||||
group_id=group_id,
|
||||
position=position,
|
||||
heading=Heading.from_degrees(0),
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
|
||||
@ -208,7 +208,7 @@ class ThreatZones:
|
||||
cls,
|
||||
doctrine: Doctrine,
|
||||
barcap_locations: Iterable[ControlPoint],
|
||||
air_defenses: Iterable[TheaterGroundObject[Any]],
|
||||
air_defenses: Iterable[TheaterGroundObject],
|
||||
) -> ThreatZones:
|
||||
"""Generates the threat zones projected by the given locations.
|
||||
|
||||
|
||||
118
game/unitmap.py
118
game/unitmap.py
@ -4,16 +4,17 @@ from __future__ import annotations
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Any, TYPE_CHECKING, Union, TypeVar, Generic
|
||||
from typing import Dict, Optional, Any, TYPE_CHECKING
|
||||
|
||||
from dcs.unit import Vehicle, Ship
|
||||
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||
from game.theater import Airfield, ControlPoint, GroundUnit
|
||||
from game.ato.flight import Flight
|
||||
from game.theater.theatergroundobject import SceneryGroundUnit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||
@ -31,14 +32,16 @@ class FrontLineUnit:
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
UnitT = TypeVar("UnitT", Ship, Vehicle)
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectMapping:
|
||||
ground_unit: GroundUnit
|
||||
dcs_unit: Unit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectUnit(Generic[UnitT]):
|
||||
ground_object: TheaterGroundObject[Any]
|
||||
group: MovingGroup[UnitT]
|
||||
unit: UnitT
|
||||
class SceneryObjectMapping:
|
||||
ground_unit: GroundUnit
|
||||
trigger_zone: TriggerZone
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -53,18 +56,13 @@ class AirliftUnits:
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Building:
|
||||
ground_object: BuildingGroundObject
|
||||
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
self.ground_objects: Dict[str, GroundObjectMapping] = {}
|
||||
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
|
||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
||||
@ -105,41 +103,18 @@ class UnitMap:
|
||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||
return self.front_line_units.get(name, None)
|
||||
|
||||
def add_ground_object_units(
|
||||
self,
|
||||
ground_object: TheaterGroundObject[Any],
|
||||
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||
miz_group: Union[ShipGroup, VehicleGroup],
|
||||
def add_ground_object_mapping(
|
||||
self, ground_unit: GroundUnit, dcs_unit: Unit
|
||||
) -> None:
|
||||
"""Adds a group associated with a TGO to the unit map.
|
||||
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
|
||||
# the GroundGroup, so we have to match the dcs unit with the liberation unit
|
||||
name = str(dcs_unit.name)
|
||||
if name in self.ground_objects:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit)
|
||||
|
||||
Args:
|
||||
ground_object: The TGO the group is associated with.
|
||||
persistence_group: The Group tracked by the TGO itself.
|
||||
miz_group: The Group spawned for the miz to match persistence_group.
|
||||
"""
|
||||
# Deaths for units at TGOs are recorded in the Group that is contained
|
||||
# by the TGO, but when groundobjectsgen populates the miz it creates new
|
||||
# groups based on that template, so the units and groups in the miz are
|
||||
# not a direct match for the units and groups that persist in the TGO.
|
||||
#
|
||||
# This means that we need to map the spawned unit names back to the
|
||||
# original TGO units, not the ones in the miz.
|
||||
if len(persistence_group.units) != len(miz_group.units):
|
||||
raise ValueError("Persistent group does not match generated group")
|
||||
unit_pairs = zip(persistence_group.units, miz_group.units)
|
||||
for persistent_unit, miz_unit in unit_pairs:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(miz_unit.name)
|
||||
if name in self.ground_object_units:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.ground_object_units[name] = GroundObjectUnit(
|
||||
ground_object, persistence_group, persistent_unit
|
||||
)
|
||||
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
|
||||
return self.ground_objects.get(name, None)
|
||||
|
||||
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
|
||||
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||
@ -195,40 +170,13 @@ class UnitMap:
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||
return self.airlifts.get(name, None)
|
||||
|
||||
def add_building(
|
||||
self, ground_object: BuildingGroundObject, group: StaticGroup
|
||||
def add_scenery(
|
||||
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
|
||||
) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
# The name of the initiator in the DCS dead event will have " object"
|
||||
# appended for statics.
|
||||
name = f"{group.name} object"
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
name = str(trigger_zone.name)
|
||||
if name in self.scenery_objects:
|
||||
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
|
||||
self.scenery_objects[name] = SceneryObjectMapping(scenery_unit, trigger_zone)
|
||||
|
||||
def add_fortification(
|
||||
self, ground_object: BuildingGroundObject, group: VehicleGroup
|
||||
) -> None:
|
||||
if len(group.units) != 1:
|
||||
raise ValueError("Fortification groups must have exactly one unit.")
|
||||
unit = group.units[0]
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
|
||||
name = str(ground_object.zone.name)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(
|
||||
f"Duplicate TGO unit: {name}. TriggerZone name: "
|
||||
f"{ground_object.dcs_identifier}"
|
||||
)
|
||||
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def building_or_fortification(self, name: str) -> Optional[Building]:
|
||||
return self.buildings.get(name, None)
|
||||
def scenery_object(self, name: str) -> Optional[SceneryObjectMapping]:
|
||||
return self.scenery_objects.get(name, None)
|
||||
|
||||
@ -46,9 +46,9 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
EwrGroundObject,
|
||||
NavalGroundObject,
|
||||
GroundUnit,
|
||||
)
|
||||
from game.typeguard import self_type_guard
|
||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||
@ -1086,7 +1086,7 @@ class FlightPlanBuilder:
|
||||
self,
|
||||
flight: Flight,
|
||||
# TODO: Custom targets should be an attribute of the flight.
|
||||
custom_targets: Optional[List[Unit]] = None,
|
||||
custom_targets: Optional[List[GroundUnit]] = None,
|
||||
) -> None:
|
||||
"""Creates a default flight plan for the given mission."""
|
||||
if flight not in self.package.flights:
|
||||
@ -1106,7 +1106,7 @@ class FlightPlanBuilder:
|
||||
) from ex
|
||||
|
||||
def generate_flight_plan(
|
||||
self, flight: Flight, custom_targets: Optional[List[Unit]]
|
||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
||||
) -> FlightPlan:
|
||||
# TODO: Flesh out mission types.
|
||||
task = flight.flight_type
|
||||
@ -1207,16 +1207,9 @@ class FlightPlanBuilder:
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
targets: List[StrikeTarget] = []
|
||||
if isinstance(location, BuildingGroundObject):
|
||||
# A building "group" is implemented as multiple TGOs with the same name.
|
||||
for building in location.strike_targets:
|
||||
targets.append(StrikeTarget(building.category, building))
|
||||
else:
|
||||
# TODO: Replace with DEAD?
|
||||
# Strike missions on SEAD targets target units.
|
||||
for g in location.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
targets.append(StrikeTarget(f"{u.type} #{j}", u))
|
||||
|
||||
for j, u in enumerate(location.strike_targets):
|
||||
targets.append(StrikeTarget(f"{u.type} #{j}", u))
|
||||
|
||||
return self.strike_flightplan(
|
||||
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
|
||||
@ -1675,7 +1668,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def generate_dead(
|
||||
self, flight: Flight, custom_targets: Optional[List[Unit]]
|
||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
||||
) -> StrikeFlightPlan:
|
||||
"""Generate a DEAD flight at a given location.
|
||||
|
||||
@ -1745,7 +1738,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def generate_sead(
|
||||
self, flight: Flight, custom_targets: Optional[List[Unit]]
|
||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
||||
) -> StrikeFlightPlan:
|
||||
"""Generate a SEAD flight at a given location.
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@ -11,12 +10,9 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
Any,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup, ShipGroup
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
@ -32,14 +28,13 @@ if TYPE_CHECKING:
|
||||
from game.ato.flight import Flight
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
from game.theater.theatergroundobject import GroundUnit, GroundGroup
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[
|
||||
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
|
||||
]
|
||||
target: Union[TheaterGroundObject, GroundGroup, GroundUnit, MultiGroupTransport]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
|
||||
@ -2,6 +2,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPointType, BuildingGroundObject
|
||||
from game.theater.theatergroundobject import IadsGroundObject
|
||||
from game.utils import Distance
|
||||
from game.missiongenerator.frontlineconflictdescription import (
|
||||
FrontLineConflictDescription,
|
||||
@ -115,8 +116,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and (
|
||||
ground_object.dcs_identifier == "AA"
|
||||
or ground_object.dcs_identifier == "EWR"
|
||||
isinstance(ground_object, IadsGroundObject)
|
||||
):
|
||||
for g in ground_object.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.theater import SamGroundObject
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
|
||||
|
||||
@ -53,7 +54,7 @@ class QStrikeTargetSelectionComboBox(QFilteredComboBox):
|
||||
target.location = g
|
||||
target.name = g.obj_name
|
||||
|
||||
if g.dcs_identifier == "AA":
|
||||
if isinstance(g, SamGroundObject):
|
||||
target.name = g.obj_name + " [units]"
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
|
||||
@ -3,11 +3,8 @@ from __future__ import annotations
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.server.leaflet import LeafletLatLon
|
||||
from game.theater import TheaterGroundObject
|
||||
from qt_ui.dialogs import Dialog
|
||||
@ -30,7 +27,6 @@ class GroundObjectJs(QObject):
|
||||
self.tgo = tgo
|
||||
self.game = game
|
||||
self.theater = game.theater
|
||||
self.buildings = self.theater.find_ground_objects_by_obj_name(self.tgo.obj_name)
|
||||
self.dialog: Optional[QGroundObjectMenu] = None
|
||||
|
||||
@Slot()
|
||||
@ -39,7 +35,6 @@ class GroundObjectJs(QObject):
|
||||
self.dialog = QGroundObjectMenu(
|
||||
None,
|
||||
self.tgo,
|
||||
self.buildings,
|
||||
self.tgo.control_point,
|
||||
self.game,
|
||||
)
|
||||
@ -61,38 +56,9 @@ class GroundObjectJs(QObject):
|
||||
def category(self) -> str:
|
||||
return self.tgo.category
|
||||
|
||||
@staticmethod
|
||||
def make_unit_name(unit: Unit, dead: bool) -> str:
|
||||
dead_label = " [DEAD]" if dead else ""
|
||||
unit_display_name = unit.type
|
||||
dcs_unit_type = vehicle_map.get(unit.type)
|
||||
if dcs_unit_type is not None:
|
||||
# TODO: Make the TGO contain GroundUnitType instead of the pydcs Group.
|
||||
# This is a hack because we can't know which variant was used.
|
||||
try:
|
||||
unit_display_name = next(
|
||||
GroundUnitType.for_dcs_type(dcs_unit_type)
|
||||
).name
|
||||
except StopIteration:
|
||||
pass
|
||||
return f"Unit #{unit.id} - {unit_display_name}{dead_label}"
|
||||
|
||||
@Property(list, notify=unitsChanged)
|
||||
def units(self) -> List[str]:
|
||||
units = []
|
||||
# TGOs with a non-empty group set are non-building TGOs. Building TGOs have no
|
||||
# groups set, but instead are one TGO per building "group" (DCS doesn't support
|
||||
# groups of statics) all with the same name.
|
||||
if self.tgo.groups:
|
||||
for unit in self.tgo.units:
|
||||
units.append(self.make_unit_name(unit, dead=False))
|
||||
for unit in self.tgo.dead_units:
|
||||
units.append(self.make_unit_name(unit, dead=True))
|
||||
else:
|
||||
for building in self.buildings:
|
||||
dead = " [DEAD]" if building.is_dead else ""
|
||||
units.append(f"{building.dcs_identifier}{dead}")
|
||||
return units
|
||||
return [unit.display_name for unit in self.tgo.units]
|
||||
|
||||
@Property(bool, notify=blueChanged)
|
||||
def blue(self) -> bool:
|
||||
@ -105,9 +71,7 @@ class GroundObjectJs(QObject):
|
||||
|
||||
@Property(bool, notify=deadChanged)
|
||||
def dead(self) -> bool:
|
||||
if not self.tgo.groups:
|
||||
return all(b.is_dead for b in self.buildings)
|
||||
return not any(g.units for g in self.tgo.groups)
|
||||
return not any(g.alive_units > 0 for g in self.tgo.groups)
|
||||
|
||||
@Property(list, notify=samThreatRangesChanged)
|
||||
def samThreatRanges(self) -> List[float]:
|
||||
|
||||
@ -35,7 +35,8 @@ class LossGrid(QGridLayout):
|
||||
self.add_loss_rows(
|
||||
debriefing.airlift_losses_by_type(player), lambda u: f"{u} from airlift"
|
||||
)
|
||||
self.add_loss_rows(debriefing.building_losses_by_type(player), lambda u: u)
|
||||
self.add_loss_rows(debriefing.ground_object_losses_by_type(player), lambda u: u)
|
||||
self.add_loss_rows(debriefing.scenery_losses_by_type(player), lambda u: u)
|
||||
|
||||
# TODO: Display dead ground object units and runways.
|
||||
|
||||
|
||||
@ -169,12 +169,14 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Ground units lost at objective areas",
|
||||
"Ground Objects destroyed",
|
||||
len(list(debriefing.ground_object_losses)),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Buildings destroyed", len(list(debriefing.building_losses)), update_layout
|
||||
"Scenery Objects destroyed",
|
||||
len(list(debriefing.scenery_object_losses)),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Base capture events", len(debriefing.base_captures), update_layout
|
||||
|
||||
@ -2,6 +2,7 @@ import os
|
||||
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
|
||||
from game.theater import GroundUnit
|
||||
|
||||
from game.config import REWARDS
|
||||
|
||||
@ -9,16 +10,16 @@ from game.config import REWARDS
|
||||
class QBuildingInfo(QGroupBox):
|
||||
def __init__(self, building, ground_object):
|
||||
super(QBuildingInfo, self).__init__()
|
||||
self.building = building
|
||||
self.building: GroundUnit = building
|
||||
self.ground_object = ground_object
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
self.header = QLabel()
|
||||
path = os.path.join(
|
||||
"./resources/ui/units/buildings/" + self.building.dcs_identifier + ".png"
|
||||
"./resources/ui/units/buildings/" + self.building.type + ".png"
|
||||
)
|
||||
if self.building.is_dead:
|
||||
if not self.building.alive:
|
||||
pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
|
||||
elif os.path.isfile(path):
|
||||
pixmap = QPixmap(path)
|
||||
@ -26,8 +27,8 @@ class QBuildingInfo(QGroupBox):
|
||||
pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
|
||||
self.header.setPixmap(pixmap)
|
||||
name = "<b>{}</b> {}".format(
|
||||
self.building.dcs_identifier[0:18],
|
||||
"[DEAD]" if self.building.is_dead else "",
|
||||
self.building.type[0:18],
|
||||
"[DEAD]" if not self.building.alive else "",
|
||||
)
|
||||
self.name = QLabel(name)
|
||||
self.name.setProperty("style", "small")
|
||||
@ -35,9 +36,11 @@ class QBuildingInfo(QGroupBox):
|
||||
layout.addWidget(self.header)
|
||||
layout.addWidget(self.name)
|
||||
|
||||
if self.building.category in REWARDS.keys():
|
||||
income_label_text = "Value: " + str(REWARDS[self.building.category]) + "M"
|
||||
if self.building.is_dead:
|
||||
if self.ground_object.category in REWARDS.keys():
|
||||
income_label_text = (
|
||||
"Value: " + str(REWARDS[self.ground_object.category]) + "M"
|
||||
)
|
||||
if not self.building.alive:
|
||||
income_label_text = "<s>" + income_label_text + "</s>"
|
||||
self.reward = QLabel(income_label_text)
|
||||
layout.addWidget(self.reward)
|
||||
|
||||
@ -107,58 +107,21 @@ class QGroundObjectMenu(QDialog):
|
||||
self.intelLayout = QGridLayout()
|
||||
i = 0
|
||||
for g in self.ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for unit in g.units:
|
||||
unit_display_name = unit.type
|
||||
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
|
||||
if dcs_unit_type is not None:
|
||||
# Hack: Don't know which variant is used.
|
||||
try:
|
||||
unit_display_name = next(
|
||||
GroundUnitType.for_dcs_type(dcs_unit_type)
|
||||
).name
|
||||
except StopIteration:
|
||||
pass
|
||||
self.intelLayout.addWidget(
|
||||
QLabel(
|
||||
"<b>Unit #"
|
||||
+ str(unit.id)
|
||||
+ " - "
|
||||
+ str(unit_display_name)
|
||||
+ "</b>"
|
||||
),
|
||||
i,
|
||||
0,
|
||||
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0
|
||||
)
|
||||
i = i + 1
|
||||
|
||||
for unit in g.units_losts:
|
||||
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
|
||||
if dcs_unit_type is None:
|
||||
continue
|
||||
|
||||
# Hack: Don't know which variant is used.
|
||||
|
||||
try:
|
||||
unit_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
|
||||
name = unit_type.name
|
||||
price = unit_type.price
|
||||
except StopIteration:
|
||||
name = dcs_unit_type.name
|
||||
price = 0
|
||||
|
||||
self.intelLayout.addWidget(
|
||||
QLabel(f"<b>Unit #{unit.id} - {name}</b> [DEAD]"), i, 0
|
||||
)
|
||||
if self.cp.captured:
|
||||
if not unit.alive and self.cp.captured:
|
||||
price = unit.unit_type.price if unit.unit_type else 0
|
||||
repair = QPushButton(f"Repair [{price}M]")
|
||||
repair.setProperty("style", "btn-success")
|
||||
repair.clicked.connect(
|
||||
lambda u=unit, g=g, p=unit_type.price: self.repair_unit(g, u, p)
|
||||
lambda u=unit, p=price: self.repair_unit(u, p)
|
||||
)
|
||||
self.intelLayout.addWidget(repair, i, 1)
|
||||
i = i + 1
|
||||
i += 1
|
||||
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
self.intelLayout.addLayout(stretch, i, 0)
|
||||
@ -169,19 +132,19 @@ class QGroundObjectMenu(QDialog):
|
||||
j = 0
|
||||
total_income = 0
|
||||
received_income = 0
|
||||
for i, building in enumerate(self.buildings):
|
||||
if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
|
||||
for static in self.ground_object.statics:
|
||||
if static not in FORTIFICATION_BUILDINGS:
|
||||
self.buildingsLayout.addWidget(
|
||||
QBuildingInfo(building, self.ground_object), j / 3, j % 3
|
||||
QBuildingInfo(static, self.ground_object), j / 3, j % 3
|
||||
)
|
||||
j = j + 1
|
||||
|
||||
if building.category in REWARDS.keys():
|
||||
total_income = total_income + REWARDS[building.category]
|
||||
if not building.is_dead:
|
||||
received_income = received_income + REWARDS[building.category]
|
||||
if self.ground_object.category in REWARDS.keys():
|
||||
total_income += REWARDS[self.ground_object.category]
|
||||
if static.alive:
|
||||
received_income += REWARDS[self.ground_object.category]
|
||||
else:
|
||||
logging.warning(building.category + " not in REWARDS")
|
||||
logging.warning(self.ground_object.category + " not in REWARDS")
|
||||
|
||||
self.financesBox = QGroupBox("Finances: ")
|
||||
self.financesBoxLayout = QGridLayout()
|
||||
@ -235,11 +198,10 @@ class QGroundObjectMenu(QDialog):
|
||||
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
|
||||
self.total_value = total_value
|
||||
|
||||
def repair_unit(self, group, unit, price):
|
||||
def repair_unit(self, unit, price):
|
||||
if self.game.blue.budget > price:
|
||||
self.game.blue.budget -= price
|
||||
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
|
||||
group.units.append(unit)
|
||||
unit.alive = True
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
# Remove destroyed units in the vicinity
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user