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.statics import Fortification, Warehouse
from dcs.terrain import Airport
from dcs.triggers import TriggerZoneCircular
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
@ -237,11 +236,7 @@ class MizCampaignLoader:
@property
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(
z
for z in self.mission.triggers._zones
if isinstance(z, TriggerZoneCircular)
)
return SceneryGroup.from_trigger_zones(z for z in self.mission.triggers.zones())
@cached_property
def control_points(self) -> dict[UUID, ControlPoint]:

View File

@ -9,15 +9,13 @@ from __future__ import annotations
import logging
import random
from collections import defaultdict
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
import dcs.vehicles
from dcs import Mission, Point, unitgroup
from dcs import Mission, Point
from dcs.action import DoScript, SceneryDestructionZone
from dcs.condition import MapObjectIsDead
from dcs.country import Country
from dcs.point import StaticPoint
from dcs.ships import (
CVN_71,
CVN_72,
@ -27,22 +25,29 @@ from dcs.ships import (
)
from dcs.statics import Fortification
from dcs.task import (
ActivateACLSCommand,
ActivateBeaconCommand,
ActivateICLSCommand,
ActivateLink4Command,
ActivateACLSCommand,
EPLRS,
FireAtPoint,
OptAlarmState,
)
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
from dcs.unit import Unit, InvisibleFARP
from dcs.triggers import (
Event,
TriggerOnce,
TriggerStart,
TriggerZone,
TriggerZoneCircular,
TriggerZoneQuadPoint,
)
from dcs.unit import InvisibleFARP, Unit
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
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.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData
@ -53,7 +58,7 @@ from game.theater.theatergroundobject import (
LhaGroundObject,
MissileSiteGroundObject,
)
from game.theater.theatergroup import SceneryUnit, TheaterGroup, IadsGroundGroup
from game.theater.theatergroup import IadsGroundGroup, SceneryUnit
from game.unitmap import UnitMap
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}
)
# 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
trigger_zone = self.m.triggers.add_triggerzone(
scenery.zone.position,
smallest_valid_radius,
scenery.zone.hidden,
scenery.zone.name,
color,
scenery.zone.properties,
trigger_zone: TriggerZone
if isinstance(scenery.zone, TriggerZoneCircular):
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__}"
)
# DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a
# structure twice.
@ -239,6 +243,34 @@ class GroundObjectGenerator:
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:
# Add destruction zone trigger
t = TriggerStart(comment="Destruction")

View File

@ -3,7 +3,8 @@ from __future__ import annotations
from typing import Iterable
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
@ -16,7 +17,7 @@ class SceneryGroup:
name: str,
centroid: Point,
category: str,
target_zones: Iterable[TriggerZoneCircular],
target_zones: Iterable[TriggerZone],
) -> None:
if not target_zones:
raise ValueError(f"{name} has no valid target zones")
@ -33,7 +34,7 @@ class SceneryGroup:
self.target_zones = list(target_zones)
@staticmethod
def category_of(group_zone: TriggerZoneCircular) -> str:
def category_of(group_zone: TriggerZone) -> str:
try:
# The first (1-indexed because lua) property of the group zone defines the
# TGO category.
@ -46,8 +47,8 @@ class SceneryGroup:
@staticmethod
def from_group_zone(
group_zone: TriggerZoneCircular,
unclaimed_target_zones: list[TriggerZoneCircular],
group_zone: TriggerZone,
unclaimed_target_zones: list[TriggerZone],
) -> SceneryGroup:
return SceneryGroup(
group_zone.name,
@ -58,7 +59,7 @@ class SceneryGroup:
@staticmethod
def from_trigger_zones(
trigger_zones: Iterable[TriggerZoneCircular],
trigger_zones: Iterable[TriggerZone],
) -> list[SceneryGroup]:
"""Define scenery objectives based on their encompassing blue circle."""
group_zones, target_zones = SceneryGroup.collect_scenery_zones(trigger_zones)
@ -66,20 +67,28 @@ class SceneryGroup:
@staticmethod
def claim_targets_for(
group_zone: TriggerZoneCircular,
unclaimed_target_zones: list[TriggerZoneCircular],
) -> list[TriggerZoneCircular]:
group_zone: TriggerZone,
unclaimed_target_zones: list[TriggerZone],
) -> list[TriggerZone]:
claimed_zones = []
for zone in list(unclaimed_target_zones):
if zone.position.distance_to_point(group_zone.position) < group_zone.radius:
claimed_zones.append(zone)
unclaimed_target_zones.remove(zone)
group_poly = SceneryGroup.poly_for_zone(group_zone)
for target in list(unclaimed_target_zones):
# If the target zone is a quad point, the position is arbitrary but visible
# 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
@staticmethod
def collect_scenery_zones(
zones: Iterable[TriggerZoneCircular],
) -> tuple[list[TriggerZoneCircular], list[TriggerZoneCircular]]:
zones: Iterable[TriggerZone],
) -> tuple[list[TriggerZone], list[TriggerZone]]:
group_zones = []
target_zones = []
for zone in zones:
@ -92,18 +101,36 @@ class SceneryGroup:
return group_zones, target_zones
@staticmethod
def zone_has_color_rgb(
zone: TriggerZoneCircular, r: float, g: float, b: float
) -> bool:
def zone_has_color_rgb(zone: TriggerZone, r: float, g: float, b: float) -> bool:
# 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
# pydcs.
return (zone.color[1], zone.color[2], zone.color[3]) == (r, g, b)
@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)
@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)
@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
#: * 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)