Support quad zones for scenery objectives.

This works by recreating the trigger zone in the generated mission to
exactly (aside from the ID, and a possibly escaped name) match the one
from the campaign definition.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2473.
This commit is contained in:
Dan Albert 2022-10-28 16:17:46 -07:00
parent 66a5878fc6
commit 5c18af4638
4 changed files with 103 additions and 46 deletions

View File

@ -13,7 +13,6 @@ from dcs.planes import F_15C
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
from dcs.statics import Fortification, Warehouse from dcs.statics import Fortification, Warehouse
from dcs.terrain import Airport from dcs.terrain import Airport
from dcs.triggers import TriggerZoneCircular
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
@ -237,11 +236,7 @@ class MizCampaignLoader:
@property @property
def scenery(self) -> List[SceneryGroup]: def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones( return SceneryGroup.from_trigger_zones(z for z in self.mission.triggers.zones())
z
for z in self.mission.triggers._zones
if isinstance(z, TriggerZoneCircular)
)
@cached_property @cached_property
def control_points(self) -> dict[UUID, ControlPoint]: def control_points(self) -> dict[UUID, ControlPoint]:

View File

@ -9,15 +9,13 @@ from __future__ import annotations
import logging import logging
import random import random
from collections import defaultdict
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
import dcs.vehicles import dcs.vehicles
from dcs import Mission, Point, unitgroup from dcs import Mission, Point
from dcs.action import DoScript, SceneryDestructionZone from dcs.action import DoScript, SceneryDestructionZone
from dcs.condition import MapObjectIsDead from dcs.condition import MapObjectIsDead
from dcs.country import Country from dcs.country import Country
from dcs.point import StaticPoint
from dcs.ships import ( from dcs.ships import (
CVN_71, CVN_71,
CVN_72, CVN_72,
@ -27,22 +25,29 @@ from dcs.ships import (
) )
from dcs.statics import Fortification from dcs.statics import Fortification
from dcs.task import ( from dcs.task import (
ActivateACLSCommand,
ActivateBeaconCommand, ActivateBeaconCommand,
ActivateICLSCommand, ActivateICLSCommand,
ActivateLink4Command, ActivateLink4Command,
ActivateACLSCommand,
EPLRS, EPLRS,
FireAtPoint, FireAtPoint,
OptAlarmState, OptAlarmState,
) )
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone from dcs.triggers import (
from dcs.unit import Unit, InvisibleFARP Event,
TriggerOnce,
TriggerStart,
TriggerZone,
TriggerZoneCircular,
TriggerZoneQuadPoint,
)
from dcs.unit import InvisibleFARP, Unit
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData from game.runways import RunwayData
@ -53,7 +58,7 @@ from game.theater.theatergroundobject import (
LhaGroundObject, LhaGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
) )
from game.theater.theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup from game.theater.theatergroup import IadsGroundGroup, SceneryUnit
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps from game.utils import Heading, feet, knots, mps
@ -217,18 +222,17 @@ class GroundObjectGenerator:
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15} 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 trigger_zone: TriggerZone
# is minimized. As long as the triggerzone is over the scenery object, we're ok. if isinstance(scenery.zone, TriggerZoneCircular):
smallest_valid_radius = feet(16).meters trigger_zone = self.create_circular_scenery_trigger(scenery.zone, color)
elif isinstance(scenery.zone, TriggerZoneQuadPoint):
trigger_zone = self.create_quad_scenery_trigger(scenery.zone, color)
else:
raise ValueError(
f"Invalid trigger zone type found for {scenery.name} in "
f"{self.ground_object.name}: {scenery.zone.__class__.__name__}"
)
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 # DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a # this trigger rule is applied. Otherwise you can kill a
# structure twice. # structure twice.
@ -239,6 +243,34 @@ class GroundObjectGenerator:
self.unit_map.add_scenery(scenery, trigger_zone) self.unit_map.add_scenery(scenery, trigger_zone)
def create_circular_scenery_trigger(
self, zone: TriggerZoneCircular, color: dict[int, float]
) -> TriggerZoneCircular:
# 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 create_quad_scenery_trigger(
self, zone: TriggerZoneQuadPoint, color: dict[int, float]
) -> TriggerZoneQuadPoint:
return self.m.triggers.add_triggerzone_quad(
zone.position,
zone.verticies,
zone.hidden,
zone.name,
color,
zone.properties,
)
def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None: def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add destruction zone trigger # Add destruction zone trigger
t = TriggerStart(comment="Destruction") t = TriggerStart(comment="Destruction")

View File

@ -3,7 +3,8 @@ from __future__ import annotations
from typing import Iterable from typing import Iterable
from dcs import Point from dcs import Point
from dcs.triggers import TriggerZoneCircular from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
from shapely.geometry import Point as ShapelyPoint, Polygon
from game.theater.theatergroundobject import NAME_BY_CATEGORY from game.theater.theatergroundobject import NAME_BY_CATEGORY
@ -16,7 +17,7 @@ class SceneryGroup:
name: str, name: str,
centroid: Point, centroid: Point,
category: str, category: str,
target_zones: Iterable[TriggerZoneCircular], target_zones: Iterable[TriggerZone],
) -> None: ) -> None:
if not target_zones: if not target_zones:
raise ValueError(f"{name} has no valid target zones") raise ValueError(f"{name} has no valid target zones")
@ -33,7 +34,7 @@ class SceneryGroup:
self.target_zones = list(target_zones) self.target_zones = list(target_zones)
@staticmethod @staticmethod
def category_of(group_zone: TriggerZoneCircular) -> str: def category_of(group_zone: TriggerZone) -> str:
try: try:
# The first (1-indexed because lua) property of the group zone defines the # The first (1-indexed because lua) property of the group zone defines the
# TGO category. # TGO category.
@ -46,8 +47,8 @@ class SceneryGroup:
@staticmethod @staticmethod
def from_group_zone( def from_group_zone(
group_zone: TriggerZoneCircular, group_zone: TriggerZone,
unclaimed_target_zones: list[TriggerZoneCircular], unclaimed_target_zones: list[TriggerZone],
) -> SceneryGroup: ) -> SceneryGroup:
return SceneryGroup( return SceneryGroup(
group_zone.name, group_zone.name,
@ -58,7 +59,7 @@ class SceneryGroup:
@staticmethod @staticmethod
def from_trigger_zones( def from_trigger_zones(
trigger_zones: Iterable[TriggerZoneCircular], trigger_zones: Iterable[TriggerZone],
) -> list[SceneryGroup]: ) -> list[SceneryGroup]:
"""Define scenery objectives based on their encompassing blue circle.""" """Define scenery objectives based on their encompassing blue circle."""
group_zones, target_zones = SceneryGroup.collect_scenery_zones(trigger_zones) group_zones, target_zones = SceneryGroup.collect_scenery_zones(trigger_zones)
@ -66,20 +67,28 @@ class SceneryGroup:
@staticmethod @staticmethod
def claim_targets_for( def claim_targets_for(
group_zone: TriggerZoneCircular, group_zone: TriggerZone,
unclaimed_target_zones: list[TriggerZoneCircular], unclaimed_target_zones: list[TriggerZone],
) -> list[TriggerZoneCircular]: ) -> list[TriggerZone]:
claimed_zones = [] claimed_zones = []
for zone in list(unclaimed_target_zones): group_poly = SceneryGroup.poly_for_zone(group_zone)
if zone.position.distance_to_point(group_zone.position) < group_zone.radius: for target in list(unclaimed_target_zones):
claimed_zones.append(zone) # If the target zone is a quad point, the position is arbitrary but visible
unclaimed_target_zones.remove(zone) # to the designer. It is the "X" that marks the trigger zone in the ME. The
# ME seems to place this at the centroid of the zone. If that X is in the
# group zone, that's claimed.
#
# See https://github.com/pydcs/dcs/pull/243#discussion_r1001369516 for more
# info.
if group_poly.contains(ShapelyPoint(target.position.x, target.position.y)):
claimed_zones.append(target)
unclaimed_target_zones.remove(target)
return claimed_zones return claimed_zones
@staticmethod @staticmethod
def collect_scenery_zones( def collect_scenery_zones(
zones: Iterable[TriggerZoneCircular], zones: Iterable[TriggerZone],
) -> tuple[list[TriggerZoneCircular], list[TriggerZoneCircular]]: ) -> tuple[list[TriggerZone], list[TriggerZone]]:
group_zones = [] group_zones = []
target_zones = [] target_zones = []
for zone in zones: for zone in zones:
@ -92,18 +101,36 @@ class SceneryGroup:
return group_zones, target_zones return group_zones, target_zones
@staticmethod @staticmethod
def zone_has_color_rgb( def zone_has_color_rgb(zone: TriggerZone, r: float, g: float, b: float) -> bool:
zone: TriggerZoneCircular, r: float, g: float, b: float
) -> bool:
# TriggerZone.color is a dict with keys 1 through 4, each being a component of # TriggerZone.color is a dict with keys 1 through 4, each being a component of
# RGBA. It's absurd that it's a dict, but that's a lua quirk that's leaking from # RGBA. It's absurd that it's a dict, but that's a lua quirk that's leaking from
# pydcs. # pydcs.
return (zone.color[1], zone.color[2], zone.color[3]) == (r, g, b) return (zone.color[1], zone.color[2], zone.color[3]) == (r, g, b)
@staticmethod @staticmethod
def is_group_zone(zone: TriggerZoneCircular) -> bool: def is_group_zone(zone: TriggerZone) -> bool:
return SceneryGroup.zone_has_color_rgb(zone, r=0, g=0, b=1) return SceneryGroup.zone_has_color_rgb(zone, r=0, g=0, b=1)
@staticmethod @staticmethod
def is_target_zone(zone: TriggerZoneCircular) -> bool: def is_target_zone(zone: TriggerZone) -> bool:
return SceneryGroup.zone_has_color_rgb(zone, r=1, g=1, b=1) return SceneryGroup.zone_has_color_rgb(zone, r=1, g=1, b=1)
@staticmethod
def poly_for_zone(zone: TriggerZone) -> Polygon:
if isinstance(zone, TriggerZoneCircular):
return SceneryGroup.poly_for_circular_zone(zone)
elif isinstance(zone, TriggerZoneQuadPoint):
return SceneryGroup.poly_for_quad_point_zone(zone)
else:
raise ValueError(
f"Invalid trigger zone type found for {zone.name}: "
f"{zone.__class__.__name__}"
)
@staticmethod
def poly_for_circular_zone(zone: TriggerZoneCircular) -> Polygon:
return ShapelyPoint(zone.position.x, zone.position.y).buffer(zone.radius)
@staticmethod
def poly_for_quad_point_zone(zone: TriggerZoneQuadPoint) -> Polygon:
return Polygon([(p.x, p.y) for p in zone.verticies])

View File

@ -166,4 +166,7 @@ VERSION = _build_version_string()
#: #:
#: Version 10.4 #: Version 10.4
#: * Support for the Falklands. #: * Support for the Falklands.
CAMPAIGN_FORMAT_VERSION = (10, 4) #:
#: Version 10.5
#: * Support for scenery objectives defined by quad zones.
CAMPAIGN_FORMAT_VERSION = (10, 5)