mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add support for Control Point Influence Zones (#540)
This commit is contained in:
parent
ce70952ee0
commit
5d038e8247
@ -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
|
||||||
|
|
||||||
|
|||||||
65
game/controlpoint_influenceradius.py
Normal file
65
game/controlpoint_influenceradius.py
Normal 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}")
|
||||||
@ -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]:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user