mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Let map objects be Strike targets.
This PR allows campaign creators to incorporate map objects (referred to as Scenery in the code) into their Liberation campaign. Map objects are defined using white trigger zones created by right clicking on scenery and clicking `assign as...`. Objective groups are defined by creating a blue TriggerZone surrounding the centers of the white trigger zones. The type of objective is determined by the campaign creator, assigning the value of the first property of the blue TriggerZone with the objective type. Map objects maintain their visually dead state by assigning a `Mission Start` `Scenery Object Dead` trigger to the trigger zone. It is important for the Liberation generated TriggerZone to be as small as possible so that no other scenery is marked dead by DCS. TriggerZones are hidden during gameplay (DCS behavior. I don't know if it's possible to turn that off.) TriggerZones are visible in the mission editor and mission planner however. If a player is using an older plane, it is important for them to remember where the target is. In the mission planner, the trigger zones' will be blue or red depending on which faction the map objects belong to. Inherent Resolve campaign has been modified to integrate scenery objects. ### **Limitations:** - Objective definitions (Any Blue TriggerZones) in campaign definition cannot overlap. - Map object deaths in `state.json` is tracking integers. You won't know what died until debriefing. - No images for the various buildings. In theory it can be done, but an unreasonable amount of work. - Every blue trigger zone must have a unique name. (If you let DCS auto increment the names this is not a concern. - No output to screen when scenery object is dead. You can see the building drawn as dead in the F10 map though. ### **Pictures:** An objective:  How the objective looks once in the mission planner/editor. This objective belongs to the enemy faction: 
This commit is contained in:
parent
9a9872812f
commit
bb1a066ff7
@ -117,7 +117,10 @@ class StateData:
|
||||
killed_aircraft=data["killed_aircrafts"],
|
||||
# Airfields emit a new "dead" event every time a bomb is dropped on
|
||||
# them when they've already dead. Dedup.
|
||||
killed_ground_units=list(set(data["killed_ground_units"])),
|
||||
#
|
||||
# Also normalize dead map objects (which are ints) to strings. The unit map
|
||||
# only stores strings.
|
||||
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"],
|
||||
)
|
||||
|
||||
89
game/scenery_group.py
Normal file
89
game/scenery_group.py
Normal file
@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
from game.theater.theatergroundobject import NAME_BY_CATEGORY
|
||||
from dcs.triggers import TriggerZone
|
||||
|
||||
from typing import Iterable, List
|
||||
|
||||
|
||||
class SceneryGroupError(RuntimeError):
|
||||
"""Error for when there are insufficient conditions to create a SceneryGroup."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SceneryGroup:
|
||||
"""Store information about a scenery objective."""
|
||||
|
||||
def __init__(
|
||||
self, zone_def: TriggerZone, zones: Iterable[TriggerZone], category: str
|
||||
) -> None:
|
||||
|
||||
self.zone_def = zone_def
|
||||
self.zones = zones
|
||||
self.position = zone_def.position
|
||||
self.category = category
|
||||
|
||||
@staticmethod
|
||||
def from_trigger_zones(trigger_zones: Iterable[TriggerZone]) -> List[SceneryGroup]:
|
||||
"""Define scenery objectives based on their encompassing blue/red circle."""
|
||||
zone_definitions = []
|
||||
white_zones = []
|
||||
|
||||
scenery_groups = []
|
||||
|
||||
# Aggregate trigger zones into different groups based on color.
|
||||
for zone in trigger_zones:
|
||||
if SceneryGroup.is_blue(zone):
|
||||
zone_definitions.append(zone)
|
||||
if SceneryGroup.is_white(zone):
|
||||
white_zones.append(zone)
|
||||
|
||||
# For each objective definition.
|
||||
for zone_def in zone_definitions:
|
||||
|
||||
zone_def_radius = zone_def.radius
|
||||
zone_def_position = zone_def.position
|
||||
zone_def_name = zone_def.name
|
||||
|
||||
if len(zone_def.properties) == 0:
|
||||
raise SceneryGroupError(
|
||||
"Undefined SceneryGroup category in TriggerZone: " + zone_def_name
|
||||
)
|
||||
|
||||
# Arbitrary campaign design requirement: First property must define the category.
|
||||
zone_def_category = zone_def.properties[1].get("value").lower()
|
||||
|
||||
valid_white_zones = []
|
||||
|
||||
for zone in list(white_zones):
|
||||
if zone.position.distance_to_point(zone_def_position) < zone_def_radius:
|
||||
valid_white_zones.append(zone)
|
||||
white_zones.remove(zone)
|
||||
|
||||
if len(valid_white_zones) > 0 and zone_def_category in NAME_BY_CATEGORY:
|
||||
scenery_groups.append(
|
||||
SceneryGroup(zone_def, valid_white_zones, zone_def_category)
|
||||
)
|
||||
elif len(valid_white_zones) == 0:
|
||||
raise SceneryGroupError(
|
||||
"No white triggerzones found in: " + zone_def_name
|
||||
)
|
||||
elif zone_def_category not in NAME_BY_CATEGORY:
|
||||
raise SceneryGroupError(
|
||||
"Incorrect TriggerZone category definition for: "
|
||||
+ zone_def_name
|
||||
+ " in campaign definition. TriggerZone category: "
|
||||
+ zone_def_category
|
||||
)
|
||||
|
||||
return scenery_groups
|
||||
|
||||
@staticmethod
|
||||
def is_blue(zone: TriggerZone) -> bool:
|
||||
# Blue in RGB is [0 Red], [0 Green], [1 Blue]. Ignore the fourth position: Transparency.
|
||||
return zone.color[1] == 0 and zone.color[2] == 0 and zone.color[3] == 1
|
||||
|
||||
@staticmethod
|
||||
def is_white(zone: TriggerZone) -> bool:
|
||||
# White in RGB is [1 Red], [1 Green], [1 Blue]. Ignore the fourth position: Transparency.
|
||||
return zone.color[1] == 1 and zone.color[2] == 1 and zone.color[3] == 1
|
||||
@ -40,6 +40,10 @@ from dcs.unitgroup import (
|
||||
VehicleGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
from dcs.triggers import Triggers
|
||||
from dcs.triggers import TriggerZone
|
||||
|
||||
from ..scenery_group import SceneryGroup
|
||||
from pyproj import CRS, Transformer
|
||||
from shapely import geometry, ops
|
||||
|
||||
@ -258,6 +262,10 @@ class MizCampaignLoader:
|
||||
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def scenery(self) -> List[SceneryGroup]:
|
||||
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
@ -445,6 +453,10 @@ class MizCampaignLoader:
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.scenery:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.scenery.append(group)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from game.scenery_group import SceneryGroup
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
@ -114,6 +115,9 @@ class PresetLocations:
|
||||
#: Locations of EWRs which should always be spawned.
|
||||
required_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of map scenery to create zones for.
|
||||
scenery: List[SceneryGroup] = field(default_factory=list)
|
||||
|
||||
#: Locations of factories for producing ground units. These will always be spawned.
|
||||
factories: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from game.scenery_group import SceneryGroup
|
||||
|
||||
import logging
|
||||
import pickle
|
||||
@ -23,6 +24,7 @@ from game.theater.theatergroundobject import (
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
SceneryGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
)
|
||||
@ -463,6 +465,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
skip_sams = self.generate_required_aa()
|
||||
skip_ewrs = self.generate_required_ewr()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_factories()
|
||||
|
||||
if self.control_point.is_global:
|
||||
@ -651,6 +654,40 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for scenery_group in presets.scenery:
|
||||
self.generate_tgo_for_scenery(scenery_group)
|
||||
|
||||
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
category = scenery.category
|
||||
group_id = self.game.next_group_id()
|
||||
object_id = 0
|
||||
|
||||
# Each nested trigger zone is a target/building/unit for an objective.
|
||||
for zone in scenery.zones:
|
||||
|
||||
object_id += 1
|
||||
local_position = zone.position
|
||||
local_dcs_identifier = zone.name
|
||||
|
||||
g = SceneryGroundObject(
|
||||
obj_name,
|
||||
category,
|
||||
group_id,
|
||||
object_id,
|
||||
local_position,
|
||||
self.control_point,
|
||||
local_dcs_identifier,
|
||||
zone,
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
return
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Iterator, List, TYPE_CHECKING
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
from dcs.triggers import TriggerZone, Triggers
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import UNITS_WITH_RADAR
|
||||
@ -281,6 +282,42 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
self._dead = True
|
||||
|
||||
|
||||
class SceneryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
category: str,
|
||||
group_id: int,
|
||||
object_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
zone: TriggerZone,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
category=category,
|
||||
group_id=group_id,
|
||||
object_id=object_id,
|
||||
position=position,
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=False,
|
||||
)
|
||||
self.zone = zone
|
||||
try:
|
||||
# In the default TriggerZone using "assign as..." in the DCS Mission Editor,
|
||||
# property three has the scenery's object ID as its value.
|
||||
self.map_object_id = self.zone.properties[3]["value"]
|
||||
except (IndexError, KeyError):
|
||||
logging.exception(
|
||||
"Invalid TriggerZone for Scenery definition. The third property must "
|
||||
"be the map object ID."
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class FactoryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@ -8,7 +8,7 @@ from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
@ -202,5 +202,15 @@ class UnitMap:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
|
||||
name = str(ground_object.map_object_id)
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(
|
||||
f"Duplicate TGO unit: {name}. TriggerZone name: "
|
||||
f"{ground_object.dcs_identifier}"
|
||||
)
|
||||
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
def building_or_fortification(self, name: str) -> Optional[Building]:
|
||||
return self.buildings.get(name, None)
|
||||
|
||||
@ -44,4 +44,13 @@ VERSION = _build_version_string()
|
||||
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
|
||||
#: are navigable by ships and have a route to another port area. DCS ships *will not*
|
||||
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
|
||||
CAMPAIGN_FORMAT_VERSION = 3
|
||||
#:
|
||||
#: Version 4
|
||||
#: * TriggerZones define map based building targets. White TriggerZones created by right
|
||||
#: clicking an object and using "assign as..." define the buildings within an objective.
|
||||
#: Blue circular TriggerZones created normally must surround groups of one or more
|
||||
#: white TriggerZones to define an objective. If a white TriggerZone is not surrounded
|
||||
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
|
||||
#: TriggerZones must also have their first property's value field define the type of
|
||||
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
|
||||
CAMPAIGN_FORMAT_VERSION = 4
|
||||
|
||||
@ -12,6 +12,7 @@ import random
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
||||
|
||||
from dcs import Mission, Point, unitgroup
|
||||
from dcs.action import SceneryDestructionZone
|
||||
from dcs.country import Country
|
||||
from dcs.point import StaticPoint
|
||||
from dcs.statics import Fortification, fortification_map, warehouse_map, Warehouse
|
||||
@ -22,6 +23,7 @@ from dcs.task import (
|
||||
OptAlarmState,
|
||||
FireAtPoint,
|
||||
)
|
||||
from dcs.triggers import TriggerStart, TriggerZone
|
||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad, Static
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
@ -39,9 +41,10 @@ from game.theater.theatergroundobject import (
|
||||
LhaGroundObject,
|
||||
ShipGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SceneryGroundObject,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots, mps
|
||||
from game.utils import feet, knots, mps
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
@ -259,6 +262,58 @@ class FactoryGenerator(BuildingSiteGenerator):
|
||||
self.generate_static(Fortification.Workshop_A)
|
||||
|
||||
|
||||
class SceneryGenerator(BuildingSiteGenerator):
|
||||
def generate(self) -> None:
|
||||
assert isinstance(self.ground_object, SceneryGroundObject)
|
||||
|
||||
trigger_zone = self.generate_trigger_zone(self.ground_object)
|
||||
|
||||
# DCS only visually shows a scenery object is dead when
|
||||
# this trigger rule is applied. Otherwise you can kill a
|
||||
# structure twice.
|
||||
if self.ground_object.is_dead:
|
||||
self.generate_dead_trigger_rule(trigger_zone)
|
||||
|
||||
# Tell Liberation to manage this groundobjectsgen as part of the campaign.
|
||||
self.register_scenery()
|
||||
|
||||
def generate_trigger_zone(self, scenery: SceneryGroundObject) -> TriggerZone:
|
||||
|
||||
zone = scenery.zone
|
||||
|
||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||
if scenery.is_friendly(to_player=True):
|
||||
color = {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||
else:
|
||||
color = {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
|
||||
|
||||
return self.m.triggers.add_triggerzone(
|
||||
zone.position,
|
||||
smallest_valid_radius,
|
||||
zone.hidden,
|
||||
zone.name,
|
||||
color,
|
||||
zone.properties,
|
||||
)
|
||||
|
||||
def generate_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
|
||||
# Add destruction zone trigger
|
||||
t = TriggerStart(comment="Destruction")
|
||||
t.actions.append(
|
||||
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
|
||||
)
|
||||
self.m.triggerrules.triggers.append(t)
|
||||
|
||||
def register_scenery(self) -> None:
|
||||
scenery = self.ground_object
|
||||
assert isinstance(scenery, SceneryGroundObject)
|
||||
self.unit_map.add_scenery(scenery)
|
||||
|
||||
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
"""Base type for carrier group generation.
|
||||
|
||||
@ -576,6 +631,10 @@ class GroundObjectsGenerator:
|
||||
generator = FactoryGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, SceneryGroundObject):
|
||||
generator = SceneryGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
)
|
||||
elif isinstance(ground_object, BuildingGroundObject):
|
||||
generator = BuildingSiteGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"recommended_player_faction": "USA 2005",
|
||||
"recommended_enemy_faction": "Insurgents (Hard)",
|
||||
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"miz": "inherent_resolve.miz",
|
||||
"performance": 1
|
||||
}
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user