Add support for Control Point Influence Zones (#540)

This commit is contained in:
Druss99 2025-08-02 12:47:55 -04:00 committed by GitHub
parent ce70952ee0
commit 5d038e8247
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 138 additions and 1 deletions

View File

@ -16,6 +16,7 @@ from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.positioned import Positioned from game.positioned import Positioned
from game.profiling import logged_duration from game.profiling import logged_duration
@ -24,6 +25,7 @@ from game.theater.controlpoint import (
Airfield, Airfield,
Carrier, Carrier,
ControlPoint, ControlPoint,
ControlPointType,
Fob, Fob,
Lha, Lha,
OffMapSpawn, OffMapSpawn,
@ -278,6 +280,12 @@ class MizCampaignLoader:
def scenery(self) -> List[SceneryGroup]: def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones) return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@cached_property
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
return ControlPointInfluenceRadius.from_trigger_zones(
self.mission.triggers._zones
)
@cached_property @cached_property
def control_points(self) -> dict[UUID, ControlPoint]: def control_points(self) -> dict[UUID, ControlPoint]:
control_points = {} control_points = {}
@ -331,6 +339,11 @@ class MizCampaignLoader:
control_point.captured_invert = fob.late_activation control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
if self.cp_influence_zones:
for cp in control_points.values():
for influence_radius in self.cp_influence_zones:
if cp.full_name == influence_radius.cp_name:
cp.influence_radius = influence_radius
return control_points return control_points
@property @property
@ -490,7 +503,59 @@ class MizCampaignLoader:
def objective_info( def objective_info(
self, near: Positioned, allow_naval: bool = False self, near: Positioned, allow_naval: bool = False
) -> Tuple[ControlPoint, Distance]: ) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(near.position, allow_naval) zones_containing_point = [
z
for z in self.cp_influence_zones
if point_in_zone(z.zone_def, near.position)
]
# Ensure we only consider naval control points if allow_naval is True
candidates = [
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
]
if not allow_naval:
candidates = [
cp
for cp in candidates
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
if candidates:
closest = min(
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
# If no zones contain the point, find the closest control point without an influence radius
if not allow_naval:
fallback_candidates = [
cp
for cp in self.theater.controlpoints
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
else:
fallback_candidates = self.theater.controlpoints
fallback_candidates = [
cp for cp in fallback_candidates if not cp.influence_radius
]
if not fallback_candidates:
raise RuntimeError(
f"All control points have an influence zone but no zones contain {near} at {near.position}"
)
closest = min(
fallback_candidates,
key=lambda cp: cp.position.distance_to_point(near.position),
)
distance = meters(closest.position.distance_to_point(near.position)) distance = meters(closest.position.distance_to_point(near.position))
return closest, distance return closest, distance

View File

@ -0,0 +1,65 @@
from __future__ import annotations
from typing import Iterable, List, TYPE_CHECKING
from functools import cached_property
from dcs.mapping import Polygon
from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
from game.theater.theatergroundobject import NAME_BY_CATEGORY
if TYPE_CHECKING:
from dcs.mapping import Point
class ControlPointInfluenceRadiusError(RuntimeError):
"""Error for when there are insufficient conditions to create a ControlPointInfluenceRadius."""
pass
class ControlPointInfluenceRadius:
"""Store information about a scenery objective."""
def __init__(self, zone_def: TriggerZone, cp_name: str) -> None:
self.zone_def = zone_def
self.position = zone_def.position
self.cp_name = cp_name
@staticmethod
def from_trigger_zones(
trigger_zones: Iterable[TriggerZone],
) -> List[ControlPointInfluenceRadius]:
"""Define scenery objectives based on their encompassing blue/red circle."""
zone_definitions = []
cp_influence_zones = []
for zone in trigger_zones:
if ControlPointInfluenceRadius.is_red(zone):
zone_definitions.append(zone)
# For each objective definition.
for zone_def in zone_definitions:
if len(zone_def.properties) == 0:
raise ControlPointInfluenceRadiusError(
"Undefined ControlPointInfluenceRadius category in TriggerZone: "
+ zone_def.name
)
zone_def_cp_name = zone_def.properties[1].get("value")
cp_influence_zones.append(
ControlPointInfluenceRadius(zone_def, zone_def_cp_name)
)
return cp_influence_zones
@staticmethod
def is_red(zone: TriggerZone) -> bool:
# Red in RGB is [1 Red], [0 Green], [0 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 1 and zone.color[2] == 0 and zone.color[3] == 0
def point_in_zone(zone: TriggerZone, pos: Point) -> bool:
if isinstance(zone, TriggerZoneCircular):
return zone.position.distance_to_point(pos) < zone.radius
elif isinstance(zone, TriggerZoneQuadPoint):
return Polygon(pos._terrain, zone.verticies).point_in_poly(pos)
raise RuntimeError(f"Invalid trigger-zone: {zone.name}")

View File

@ -47,6 +47,7 @@ from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import ShipType from dcs.unittype import ShipType
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.controlpoint_influenceradius import ControlPointInfluenceRadius
from game.ground_forces.combat_stance import CombatStance from game.ground_forces.combat_stance import CombatStance
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.runways import RunwayAssigner, RunwayData from game.runways import RunwayAssigner, RunwayData
@ -417,6 +418,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# Initialized late because ControlPoints are constructed before the game is. # Initialized late because ControlPoints are constructed before the game is.
self._front_line_db: Database[FrontLine] | None = None self._front_line_db: Database[FrontLine] | None = None
self.influence_radius: ControlPointInfluenceRadius | None = None
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>" return f"<{self.__class__}: {self.name}>"
@ -1210,6 +1213,7 @@ class Airfield(ControlPoint, CTLD):
theater: ConflictTheater, theater: ConflictTheater,
starts_blue: bool, starts_blue: bool,
ctld_zones: Optional[List[Tuple[Point, float]]] = None, ctld_zones: Optional[List[Tuple[Point, float]]] = None,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
airport.name, airport.name,
@ -1222,6 +1226,7 @@ class Airfield(ControlPoint, CTLD):
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
self.ctld_zones = ctld_zones self.ctld_zones = ctld_zones
self.influence_zone = influence_zone
@property @property
def dcs_airport(self) -> Airport: def dcs_airport(self) -> Airport:
@ -1628,6 +1633,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
starts_blue: bool, starts_blue: bool,
ctld_zones: Optional[List[Tuple[Point, float]]] = None, ctld_zones: Optional[List[Tuple[Point, float]]] = None,
is_invisible: bool = False, is_invisible: bool = False,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
@ -1635,6 +1641,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
self.name = name self.name = name
self.ctld_zones = ctld_zones self.ctld_zones = ctld_zones
self.is_invisible = is_invisible self.is_invisible = is_invisible
self.influence_zone = influence_zone
@property @property
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: