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