diff --git a/client/src/api/_liberationApi.ts b/client/src/api/_liberationApi.ts index a1c35b43..5906a221 100644 --- a/client/src/api/_liberationApi.ts +++ b/client/src/api/_liberationApi.ts @@ -426,6 +426,7 @@ export type Tgo = { detection_ranges: number[]; dead: boolean; sidc: string; + task?: string[]; }; export type SupplyRoute = { id: string; diff --git a/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.test.tsx b/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.test.tsx index ac4071e1..c49921da 100644 --- a/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.test.tsx +++ b/client/src/components/airdefenserangelayer/AirDefenseRangeLayer.test.tsx @@ -57,6 +57,7 @@ describe("AirDefenseRangeLayer", () => { detection_ranges: [20], dead: false, sidc: "", + task: [], }, }, }, @@ -86,6 +87,7 @@ describe("AirDefenseRangeLayer", () => { detection_ranges: [20], dead: false, sidc: "", + task: [], }, }, }, @@ -125,6 +127,7 @@ describe("AirDefenseRangeLayer", () => { detection_ranges: [20], dead: false, sidc: "", + task: [], }, }, }, diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index d631227b..24253f73 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -53,6 +53,18 @@ export default function LiberationMap() { + + + + + + + + + + + + diff --git a/client/src/components/tgoslayer/TgosLayer.tsx b/client/src/components/tgoslayer/TgosLayer.tsx index 8a12aacf..01631a0d 100644 --- a/client/src/components/tgoslayer/TgosLayer.tsx +++ b/client/src/components/tgoslayer/TgosLayer.tsx @@ -6,14 +6,21 @@ import { LayerGroup } from "react-leaflet"; interface TgosLayerProps { categories?: string[]; exclude?: true; + task?: string; } export default function TgosLayer(props: TgosLayerProps) { const allTgos = Object.values(useAppSelector(selectTgos).tgos); const categoryFilter = props.categories ?? []; + const taskFilter = props.task ?? ""; const tgos = allTgos.filter( - (tgo) => categoryFilter.includes(tgo.category) === !(props.exclude ?? false) - ); + (tgo) => { + if (taskFilter && tgo.task){ + return taskFilter === tgo.task[0] + } + return categoryFilter.includes(tgo.category) === !(props.exclude ?? false) + } + ); return ( {tgos.map((tgo) => { diff --git a/game/armedforces/forcegroup.py b/game/armedforces/forcegroup.py index 16508b19..b382b75a 100644 --- a/game/armedforces/forcegroup.py +++ b/game/armedforces/forcegroup.py @@ -191,11 +191,12 @@ class ForceGroup: location: PresetLocation, control_point: ControlPoint, game: Game, + task: Optional[GroupTask], ) -> TheaterGroundObject: """Create a random TheaterGroundObject from the available templates""" layout = random.choice(self.layouts) return self.create_ground_object_for_layout( - layout, name, location, control_point, game + layout, name, location, control_point, game, task ) def create_ground_object_for_layout( @@ -205,9 +206,10 @@ class ForceGroup: location: PresetLocation, control_point: ControlPoint, game: Game, + task: Optional[GroupTask], ) -> TheaterGroundObject: """Create a TheaterGroundObject for the given template""" - go = layout.create_ground_object(name, location, control_point) + go = layout.create_ground_object(name, location, control_point, task) # Generate all groups using the randomization if it defined for tgo_group in layout.groups: for unit_group in tgo_group.unit_groups: diff --git a/game/layout/layout.py b/game/layout/layout.py index 6c169005..e5906ee4 100644 --- a/game/layout/layout.py +++ b/game/layout/layout.py @@ -220,6 +220,7 @@ class TgoLayout: name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> TheaterGroundObject: """Create the TheaterGroundObject for the TgoLayout @@ -240,11 +241,12 @@ class AntiAirLayout(TgoLayout): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> IadsGroundObject: if GroupTask.EARLY_WARNING_RADAR in self.tasks: return EwrGroundObject(name, location, control_point) elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks): - return SamGroundObject(name, location, control_point) + return SamGroundObject(name, location, control_point, task) raise RuntimeError( f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})" ) @@ -256,6 +258,7 @@ class BuildingLayout(TgoLayout): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> BuildingGroundObject: iads_role = IadsRole.for_category(self.category) tgo_type = ( @@ -266,6 +269,7 @@ class BuildingLayout(TgoLayout): self.category, location, control_point, + task, self.category == "fob", ) @@ -283,6 +287,7 @@ class NavalLayout(TgoLayout): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> TheaterGroundObject: if GroupTask.NAVY in self.tasks: return ShipGroundObject(name, location, control_point) @@ -299,6 +304,7 @@ class DefensesLayout(TgoLayout): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> TheaterGroundObject: if GroupTask.MISSILE in self.tasks: return MissileSiteGroundObject(name, location, control_point) @@ -313,5 +319,6 @@ class GroundForceLayout(TgoLayout): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> TheaterGroundObject: - return VehicleGroupGroundObject(name, location, control_point) + return VehicleGroupGroundObject(name, location, control_point, task) diff --git a/game/migrator.py b/game/migrator.py index 8ae82940..202bcd99 100644 --- a/game/migrator.py +++ b/game/migrator.py @@ -36,6 +36,7 @@ class Migrator: self._update_squadrons() self._release_untasked_flights() self._update_weather() + self._update_tgos() def _update_doctrine(self) -> None: doctrines = [ @@ -197,3 +198,7 @@ class Migrator: midnight_turbulence_per_10cm=0.4, weather_type_chances=sc.weather_type_chances, ) + + def _update_tgos(self) -> None: + for go in self.game.theater.ground_objects: + go.task = None # TODO: attempt to deduce tasking? diff --git a/game/scenery_group.py b/game/scenery_group.py index 0b2f6b9f..cf7067a0 100644 --- a/game/scenery_group.py +++ b/game/scenery_group.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Iterable, List, TYPE_CHECKING +from typing import Iterable, List, TYPE_CHECKING, Optional from dcs.mapping import Polygon from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint +from game.data.groups import GroupTask from game.theater.theatergroundobject import NAME_BY_CATEGORY if TYPE_CHECKING: @@ -88,6 +89,38 @@ class SceneryGroup: return scenery_groups + @staticmethod + def group_task_for_scenery_group_category(category: str) -> Optional[GroupTask]: + if category == "allycamp": + return GroupTask.ALLY_CAMP + elif category == "ammo": + return GroupTask.AMMO + elif category == "commandcenter": + return GroupTask.COMMAND_CENTER + elif category == "comms": + return GroupTask.COMMS + elif category == "derrick": + return GroupTask.DERRICK + elif category == "factory": + return GroupTask.FACTORY + elif category == "farp": + return GroupTask.FARP + elif category == "fob": + return GroupTask.FOB + elif category == "fuel": + return GroupTask.FUEL + elif category == "oil": + return GroupTask.OIL + elif category == "power": + return GroupTask.POWER + elif category == "village": + return GroupTask.VILLAGE + elif category == "ware": + return GroupTask.WARE + elif category == "ww2bunker": + return GroupTask.WW2_BUNKER + return None + @staticmethod def is_blue(zone: TriggerZone) -> bool: # Blue in RGB is [0 Red], [0 Green], [1 Blue]. Ignore the fourth position: Transparency. diff --git a/game/server/tgos/models.py b/game/server/tgos/models.py index 7124f050..2f18b115 100644 --- a/game/server/tgos/models.py +++ b/game/server/tgos/models.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from uuid import UUID from pydantic import BaseModel +from game.data.groups import GroupTask from game.server.leaflet import LeafletPoint if TYPE_CHECKING: @@ -24,6 +25,7 @@ class TgoJs(BaseModel): detection_ranges: list[float] # TODO: Event stream dead: bool # TODO: Event stream sidc: str # TODO: Event stream + task: Optional[GroupTask] class Config: title = "Tgo" @@ -44,6 +46,7 @@ class TgoJs(BaseModel): detection_ranges=detection_ranges, dead=tgo.is_dead, sidc=str(tgo.sidc()), + task=tgo.groups[0].ground_object.task, ) @staticmethod diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index fdcc0c5f..98dbcb9b 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -177,13 +177,17 @@ class ControlPointGroundObjectGenerator: return True def generate_ground_object_from_group( - self, unit_group: ForceGroup, location: PresetLocation + self, + unit_group: ForceGroup, + location: PresetLocation, + task: Optional[GroupTask] = None, ) -> None: ground_object = unit_group.generate( namegen.random_objective_name(), location, self.control_point, self.game, + task, ) self.control_point.connected_objectives.append(ground_object) @@ -199,7 +203,7 @@ class ControlPointGroundObjectGenerator: if not unit_group: logging.warning(f"{self.faction_name} has no ForceGroup for Navy") return - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group(unit_group, position, GroupTask.NAVY) class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator): @@ -241,6 +245,7 @@ class CarrierGroundObjectGenerator(GenericCarrierGroundObjectGenerator): self.control_point.position, self.control_point.heading, ), + GroupTask.AIRCRAFT_CARRIER, ) self.update_carrier_name(random.choice(list(carrier_names))) return True @@ -272,6 +277,7 @@ class LhaGroundObjectGenerator(GenericCarrierGroundObjectGenerator): self.control_point.position, self.control_point.heading, ), + GroupTask.HELICOPTER_CARRIER, ) self.update_carrier_name(random.choice(list(lha_names))) return True @@ -340,7 +346,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not unit_group: logging.error(f"{self.faction_name} has no ForceGroup for Armor") return - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group( + unit_group, position, GroupTask.BASE_DEFENSE + ) def generate_aa(self) -> None: presets = self.control_point.preset_locations @@ -365,7 +373,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not unit_group: logging.error(f"{self.faction_name} has no ForceGroup for EWR") return - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group( + unit_group, position, GroupTask.EARLY_WARNING_RADAR + ) def generate_building_at( self, @@ -378,7 +388,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): raise RuntimeError( f"{self.faction_name} has no access to Building {group_task.description}" ) - self.generate_ground_object_from_group(unit_group, location) + self.generate_ground_object_from_group(unit_group, location, group_task) def generate_ammunition_depots(self) -> None: for position in self.control_point.preset_locations.ammunition_depots: @@ -394,7 +404,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if unit_group: # Only take next (smaller) aa_range when no template available for the # most requested range. Otherwise break the loop and continue - self.generate_ground_object_from_group(unit_group, location) + self.generate_ground_object_from_group(unit_group, location, task) return logging.error( @@ -430,6 +440,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): scenery.category, PresetLocation(scenery.zone_def.name, scenery.position), self.control_point, + SceneryGroup.group_task_for_scenery_group_category(scenery.category), ) ground_group = TheaterGroup( self.game.next_group_id(), @@ -464,7 +475,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not unit_group: logging.warning(f"{self.faction_name} has no ForceGroup for Missile") return - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group( + unit_group, position, GroupTask.MISSILE + ) def generate_coastal_sites(self) -> None: for position in self.control_point.preset_locations.coastal_defenses: @@ -472,7 +485,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not unit_group: logging.warning(f"{self.faction_name} has no ForceGroup for Coastal") return - self.generate_ground_object_from_group(unit_group, position) + self.generate_ground_object_from_group( + unit_group, position, GroupTask.COASTAL + ) def generate_strike_targets(self) -> None: for position in self.control_point.preset_locations.strike_locations: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 1180bb8e..275dee75 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -21,6 +21,7 @@ from game.sidc import ( ) from game.theater.presetlocation import PresetLocation from .missiontarget import MissionTarget +from ..data.groups import GroupTask from ..utils import Distance, Heading, meters if TYPE_CHECKING: @@ -62,6 +63,7 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): location: PresetLocation, control_point: ControlPoint, sea_object: bool, + task: Optional[GroupTask], ) -> None: super().__init__(name, location) self.id = uuid.uuid4() @@ -72,6 +74,7 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): self.groups: List[TheaterGroup] = [] self.original_name = location.original_name self._threat_poly: ThreatPoly | None = None + self.task = task def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() @@ -286,6 +289,7 @@ class BuildingGroundObject(TheaterGroundObject): category: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], is_fob_structure: bool = False, ) -> None: super().__init__( @@ -294,6 +298,7 @@ class BuildingGroundObject(TheaterGroundObject): location=location, control_point=control_point, sea_object=False, + task=task, ) self.is_fob_structure = is_fob_structure @@ -389,6 +394,7 @@ class CarrierGroundObject(GenericCarrierGroundObject): location=location, control_point=control_point, sea_object=True, + task=GroupTask.AIRCRAFT_CARRIER, ) @property @@ -410,6 +416,7 @@ class LhaGroundObject(GenericCarrierGroundObject): location=location, control_point=control_point, sea_object=True, + task=GroupTask.HELICOPTER_CARRIER, ) @property @@ -430,6 +437,7 @@ class MissileSiteGroundObject(TheaterGroundObject): location=location, control_point=control_point, sea_object=False, + task=GroupTask.MISSILE, ) @property @@ -470,6 +478,7 @@ class CoastalSiteGroundObject(TheaterGroundObject): location=location, control_point=control_point, sea_object=False, + task=GroupTask.COASTAL, ) @property @@ -503,6 +512,7 @@ class IadsGroundObject(TheaterGroundObject, ABC): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], category: str = "aa", ) -> None: super().__init__( @@ -511,6 +521,7 @@ class IadsGroundObject(TheaterGroundObject, ABC): location=location, control_point=control_point, sea_object=False, + task=task, ) def mission_types(self, for_player: bool) -> Iterator[FlightType]: @@ -538,12 +549,14 @@ class SamGroundObject(IadsGroundObject): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> None: super().__init__( name=name, category="aa", location=location, control_point=control_point, + task=task, ) @property @@ -589,6 +602,7 @@ class VehicleGroupGroundObject(TheaterGroundObject): name: str, location: PresetLocation, control_point: ControlPoint, + task: Optional[GroupTask], ) -> None: super().__init__( name=name, @@ -596,6 +610,7 @@ class VehicleGroupGroundObject(TheaterGroundObject): location=location, control_point=control_point, sea_object=False, + task=task, ) @property @@ -637,6 +652,7 @@ class EwrGroundObject(IadsGroundObject): location=location, control_point=control_point, category="ewr", + task=GroupTask.EARLY_WARNING_RADAR, ) @property @@ -662,6 +678,7 @@ class ShipGroundObject(NavalGroundObject): location=location, control_point=control_point, sea_object=True, + task=GroupTask.NAVY, ) @property diff --git a/tests/theater/test_theatergroundobject.py b/tests/theater/test_theatergroundobject.py index 009dd0e6..5afc0ed6 100644 --- a/tests/theater/test_theatergroundobject.py +++ b/tests/theater/test_theatergroundobject.py @@ -1,8 +1,10 @@ -import pytest from typing import Any +import pytest from dcs.mapping import Point + from game.ato.flighttype import FlightType +from game.theater.controlpoint import OffMapSpawn from game.theater.presetlocation import PresetLocation from game.theater.theatergroundobject import ( BuildingGroundObject, @@ -16,7 +18,6 @@ from game.theater.theatergroundobject import ( ShipGroundObject, IadsBuildingGroundObject, ) -from game.theater.controlpoint import OffMapSpawn from game.utils import Heading @@ -49,20 +50,22 @@ def test_mission_types_friendly(mocker: Any) -> None: EwrGroundObject, ShipGroundObject, ]: - ground_object = ground_object_type( # type: ignore + ground_object = ground_object_type( name="test", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(ground_object.mission_types(for_player=True)) assert mission_types == [FlightType.BARCAP] - for ground_object_type in [BuildingGroundObject, IadsBuildingGroundObject]: # type: ignore - ground_object = ground_object_type( # type: ignore + for ground_object_type in [BuildingGroundObject, IadsBuildingGroundObject]: + ground_object = ground_object_type( name="test", category="ammo", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(ground_object.mission_types(for_player=True)) assert mission_types == [FlightType.BARCAP] @@ -94,6 +97,7 @@ def test_mission_types_enemy(mocker: Any) -> None: category="ammo", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(building.mission_types(for_player=False)) assert len(mission_types) == 6 @@ -109,6 +113,7 @@ def test_mission_types_enemy(mocker: Any) -> None: category="ammo", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(iads_building.mission_types(for_player=False)) assert len(mission_types) == 7 @@ -129,6 +134,7 @@ def test_mission_types_enemy(mocker: Any) -> None: name="test", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(ground_object.mission_types(for_player=False)) assert len(mission_types) == 7 @@ -144,6 +150,7 @@ def test_mission_types_enemy(mocker: Any) -> None: name="test", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(sam.mission_types(for_player=False)) assert len(mission_types) == 8 @@ -194,6 +201,7 @@ def test_mission_types_enemy(mocker: Any) -> None: name="test", location=dummy_location, control_point=dummy_control_point, + task=None, ) mission_types = list(vehicles.mission_types(for_player=False)) assert len(mission_types) == 7