mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
66a5878fc6
commit
5c18af4638
@ -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]:
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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])
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user