mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Added new options in settings: - Carriers steam into wind - Navmesh to use for Pretense carrier zones - Remove ground spawn statics, including invisible FARPs, at airbases. - Percentage of randomly selected aircraft types (only for generated squadrons) intended to allow the user to increase aircraft variety. Will now store the ICLS channel and Link4 frequency in missiondata.py CarrierInfo. Implemented artillery groups as Pretense garrisons. Artillery groups are spawned by the Artillery Bunker. Will now also ensure that the logistics units spawned as part of Pretense garrisons are actually capable of ammo resupply. Fixed the Pretense generator generating a bit too many missions per squadron. Ground spawns: Also hot start aircraft which require ground crew support (ground air or chock removal) which might not be available at roadbases. Also, pretensetgogenerator.py will now correctly handle air defence units in ground_unit_of_class(). Added Roland groups in the Pretense generator.
469 lines
20 KiB
Python
469 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
import random
|
|
from typing import TYPE_CHECKING, List
|
|
|
|
from dcs import Point
|
|
from dcs.action import (
|
|
ClearFlag,
|
|
DoScript,
|
|
MarkToAll,
|
|
SetFlag,
|
|
RemoveSceneObjects,
|
|
RemoveSceneObjectsMask,
|
|
SceneryDestructionZone,
|
|
Smoke,
|
|
)
|
|
from dcs.condition import (
|
|
AllOfCoalitionOutsideZone,
|
|
FlagIsFalse,
|
|
FlagIsTrue,
|
|
PartOfCoalitionInZone,
|
|
TimeAfter,
|
|
TimeSinceFlag,
|
|
)
|
|
from dcs.mission import Mission
|
|
from dcs.task import Option
|
|
from dcs.terrain.caucasus.airports import Krasnodar_Pashkovsky
|
|
from dcs.terrain.syria.airports import Damascus, Khalkhalah
|
|
from dcs.translation import String
|
|
from dcs.triggers import Event, TriggerCondition, TriggerOnce
|
|
from dcs.unit import Skill
|
|
from numpy import cross, einsum, arctan2
|
|
from shapely import MultiPolygon, Point as ShapelyPoint
|
|
|
|
from game.naming import ALPHA_MILITARY
|
|
from game.theater import Airfield
|
|
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn
|
|
|
|
if TYPE_CHECKING:
|
|
from game.game import Game
|
|
|
|
PUSH_TRIGGER_SIZE = 3000
|
|
PUSH_TRIGGER_ACTIVATION_AGL = 25
|
|
|
|
REGROUP_ZONE_DISTANCE = 12000
|
|
REGROUP_ALT = 5000
|
|
|
|
TRIGGER_WAYPOINT_OFFSET = 2
|
|
TRIGGER_MIN_DISTANCE_FROM_START = 10000
|
|
# modified since we now have advanced SAM units
|
|
TRIGGER_RADIUS_MINIMUM = 3000000
|
|
|
|
TRIGGER_RADIUS_SMALL = 50000
|
|
TRIGGER_RADIUS_MEDIUM = 100000
|
|
TRIGGER_RADIUS_LARGE = 150000
|
|
TRIGGER_RADIUS_ALL_MAP = 3000000
|
|
TRIGGER_RADIUS_CLEAR_SCENERY = 1000
|
|
TRIGGER_RADIUS_PRETENSE_TGO = 500
|
|
TRIGGER_RADIUS_PRETENSE_SUPPLY = 500
|
|
TRIGGER_RADIUS_PRETENSE_HELI = 1000
|
|
TRIGGER_RADIUS_PRETENSE_HELI_BUFFER = 500
|
|
TRIGGER_RADIUS_PRETENSE_CARRIER = 20000
|
|
TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL = 3000
|
|
TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER = 25000
|
|
TRIGGER_RUNWAY_LENGTH_PRETENSE = 2500
|
|
TRIGGER_RUNWAY_WIDTH_PRETENSE = 400
|
|
|
|
SIMPLIFY_RUNS_PRETENSE_CARRIER = 10000
|
|
|
|
|
|
class Silence(Option):
|
|
Key = 7
|
|
|
|
|
|
class PretenseTriggerGenerator:
|
|
capture_zone_types = (Fob, Airfield)
|
|
capture_zone_flag = 600
|
|
|
|
def __init__(self, mission: Mission, game: Game) -> None:
|
|
self.mission = mission
|
|
self.game = game
|
|
|
|
def _set_allegiances(self, player_coalition: str, enemy_coalition: str) -> None:
|
|
"""
|
|
Set airbase initial coalition
|
|
"""
|
|
|
|
# Empty neutrals airports
|
|
airfields = [
|
|
cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield)
|
|
]
|
|
airport_ids = {cp.airport.id for cp in airfields}
|
|
for airport in self.mission.terrain.airport_list():
|
|
if airport.id not in airport_ids:
|
|
airport.unlimited_fuel = False
|
|
airport.unlimited_munitions = False
|
|
airport.unlimited_aircrafts = False
|
|
airport.gasoline_init = 0
|
|
airport.methanol_mixture_init = 0
|
|
airport.diesel_init = 0
|
|
airport.jet_init = 0
|
|
airport.operating_level_air = 0
|
|
airport.operating_level_equipment = 0
|
|
airport.operating_level_fuel = 0
|
|
|
|
for airport in self.mission.terrain.airport_list():
|
|
if airport.id not in airport_ids:
|
|
airport.unlimited_fuel = True
|
|
airport.unlimited_munitions = True
|
|
airport.unlimited_aircrafts = True
|
|
|
|
for airfield in airfields:
|
|
cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id)
|
|
if cp_airport is None:
|
|
raise RuntimeError(
|
|
f"Could not find {airfield.airport.name} in the mission"
|
|
)
|
|
cp_airport.set_coalition(
|
|
airfield.captured and player_coalition or enemy_coalition
|
|
)
|
|
|
|
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
|
|
"""
|
|
Set skill level for all aircraft in the mission
|
|
"""
|
|
for coalition_name, coalition in self.mission.coalition.items():
|
|
if coalition_name == player_coalition:
|
|
skill_level = Skill(self.game.settings.player_skill)
|
|
elif coalition_name == enemy_coalition:
|
|
skill_level = Skill(self.game.settings.enemy_vehicle_skill)
|
|
else:
|
|
continue
|
|
|
|
for country in coalition.countries.values():
|
|
for vehicle_group in country.vehicle_group:
|
|
vehicle_group.set_skill(skill_level)
|
|
|
|
def _gen_markers(self) -> None:
|
|
"""
|
|
Generate markers on F10 map for each existing objective
|
|
"""
|
|
if self.game.settings.generate_marks:
|
|
mark_trigger = TriggerOnce(Event.NoEvent, "Marks generator")
|
|
mark_trigger.add_condition(TimeAfter(1))
|
|
v = 10
|
|
for cp in self.game.theater.controlpoints:
|
|
seen = set()
|
|
for ground_object in cp.ground_objects:
|
|
if ground_object.obj_name in seen:
|
|
continue
|
|
|
|
seen.add(ground_object.obj_name)
|
|
for location in ground_object.mark_locations:
|
|
zone = self.mission.triggers.add_triggerzone(
|
|
location, radius=10, hidden=True, name="MARK"
|
|
)
|
|
if cp.captured:
|
|
name = ground_object.obj_name + " [ALLY]"
|
|
else:
|
|
name = ground_object.obj_name + " [ENEMY]"
|
|
mark_trigger.add_action(MarkToAll(v, zone.id, String(name)))
|
|
v += 1
|
|
self.mission.triggerrules.triggers.append(mark_trigger)
|
|
|
|
def _generate_capture_triggers(
|
|
self, player_coalition: str, enemy_coalition: str
|
|
) -> None:
|
|
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`.
|
|
One for the initial capture of a control point, and one if it is recaptured.
|
|
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
|
|
"""
|
|
for cp in self.game.theater.controlpoints:
|
|
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
|
|
if cp.captured:
|
|
attacking_coalition = enemy_coalition
|
|
attack_coalition_int = 1 # 1 is the Event int for Red
|
|
defending_coalition = player_coalition
|
|
defend_coalition_int = 2 # 2 is the Event int for Blue
|
|
else:
|
|
attacking_coalition = player_coalition
|
|
attack_coalition_int = 2
|
|
defending_coalition = enemy_coalition
|
|
defend_coalition_int = 1
|
|
|
|
trigger_zone = self.mission.triggers.add_triggerzone(
|
|
cp.position,
|
|
radius=TRIGGER_RADIUS_CAPTURE,
|
|
hidden=False,
|
|
name="CAPTURE",
|
|
)
|
|
flag = self.get_capture_zone_flag()
|
|
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
|
capture_trigger.add_condition(
|
|
AllOfCoalitionOutsideZone(
|
|
defending_coalition, trigger_zone.id, unit_type="GROUND"
|
|
)
|
|
)
|
|
capture_trigger.add_condition(
|
|
PartOfCoalitionInZone(
|
|
attacking_coalition, trigger_zone.id, unit_type="GROUND"
|
|
)
|
|
)
|
|
capture_trigger.add_condition(FlagIsFalse(flag=flag))
|
|
script_string = String(
|
|
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
|
|
)
|
|
capture_trigger.add_action(DoScript(script_string))
|
|
capture_trigger.add_action(SetFlag(flag=flag))
|
|
self.mission.triggerrules.triggers.append(capture_trigger)
|
|
|
|
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
|
|
recapture_trigger.add_condition(
|
|
AllOfCoalitionOutsideZone(
|
|
attacking_coalition, trigger_zone.id, unit_type="GROUND"
|
|
)
|
|
)
|
|
recapture_trigger.add_condition(
|
|
PartOfCoalitionInZone(
|
|
defending_coalition, trigger_zone.id, unit_type="GROUND"
|
|
)
|
|
)
|
|
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
|
|
script_string = String(
|
|
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
|
|
)
|
|
recapture_trigger.add_action(DoScript(script_string))
|
|
recapture_trigger.add_action(ClearFlag(flag=flag))
|
|
self.mission.triggerrules.triggers.append(recapture_trigger)
|
|
|
|
def _generate_pretense_zone_triggers(self) -> None:
|
|
"""Creates triggger zones for the Pretense campaign. These include:
|
|
- Carrier zones for friendly forces, generated from the navmesh / sea zone intersection
|
|
- Carrier zones for opposing forces
|
|
- Airfield and FARP zones
|
|
- Airfield and FARP spawn points / helicopter spawn points / ground object positions
|
|
"""
|
|
|
|
# First generate carrier zones for friendly forces
|
|
use_blue_navmesh = (
|
|
self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh"
|
|
)
|
|
sea_zones_landmap = self.game.coalition_for(
|
|
player=False
|
|
).nav_mesh.theater.landmap
|
|
if (
|
|
self.game.settings.pretense_controllable_carrier
|
|
and sea_zones_landmap is not None
|
|
):
|
|
navmesh_number = 0
|
|
for navmesh_poly in self.game.coalition_for(
|
|
player=use_blue_navmesh
|
|
).nav_mesh.polys:
|
|
navmesh_number += 1
|
|
if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly):
|
|
# Get the intersection between the navmesh zone and the sea zone
|
|
navmesh_sea_intersection = sea_zones_landmap.sea_zones.intersection(
|
|
navmesh_poly.poly
|
|
)
|
|
navmesh_zone_verticies = navmesh_sea_intersection
|
|
|
|
# Simplify it to get a quadrangle
|
|
for simplify_run in range(SIMPLIFY_RUNS_PRETENSE_CARRIER):
|
|
navmesh_zone_verticies = navmesh_sea_intersection.simplify(
|
|
float(simplify_run * 10), preserve_topology=False
|
|
)
|
|
if isinstance(navmesh_zone_verticies, MultiPolygon):
|
|
break
|
|
if len(navmesh_zone_verticies.exterior.coords) <= 4:
|
|
break
|
|
if isinstance(navmesh_zone_verticies, MultiPolygon):
|
|
continue
|
|
trigger_zone_verticies = []
|
|
terrain = self.game.theater.terrain
|
|
alpha = random.choice(ALPHA_MILITARY)
|
|
|
|
# Generate the quadrangle zone and four points inside it for carrier navigation
|
|
if len(navmesh_zone_verticies.exterior.coords) == 4:
|
|
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
|
|
corner_point_num = 0
|
|
for point_coord in navmesh_zone_verticies.exterior.coords:
|
|
corner_point = Point(
|
|
x=point_coord[0], y=point_coord[1], terrain=terrain
|
|
)
|
|
nav_point = corner_point.point_from_heading(
|
|
corner_point.heading_between_point(
|
|
navmesh_sea_intersection.centroid
|
|
),
|
|
TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER,
|
|
)
|
|
corner_point_num += 1
|
|
|
|
zone_name = f"{alpha}-{navmesh_number}-{corner_point_num}"
|
|
if sea_zones_landmap.sea_zones.contains(
|
|
ShapelyPoint(nav_point.x, nav_point.y)
|
|
):
|
|
self.mission.triggers.add_triggerzone(
|
|
nav_point,
|
|
radius=TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL,
|
|
hidden=False,
|
|
name=zone_name,
|
|
color=zone_color,
|
|
)
|
|
|
|
trigger_zone_verticies.append(corner_point)
|
|
|
|
zone_name = f"{alpha}-{navmesh_number}"
|
|
trigger_zone = self.mission.triggers.add_triggerzone_quad(
|
|
navmesh_sea_intersection.centroid,
|
|
trigger_zone_verticies,
|
|
hidden=False,
|
|
name=zone_name,
|
|
color=zone_color,
|
|
)
|
|
try:
|
|
if len(self.game.pretense_carrier_zones) == 0:
|
|
self.game.pretense_carrier_zones = []
|
|
except AttributeError:
|
|
self.game.pretense_carrier_zones = []
|
|
self.game.pretense_carrier_zones.append(zone_name)
|
|
|
|
for cp in self.game.theater.controlpoints:
|
|
if (
|
|
cp.is_fleet
|
|
and self.game.settings.pretense_controllable_carrier
|
|
and cp.captured
|
|
):
|
|
# Friendly carrier zones are generated above
|
|
continue
|
|
elif cp.is_fleet:
|
|
trigger_radius = float(TRIGGER_RADIUS_PRETENSE_CARRIER)
|
|
elif isinstance(cp, Fob) and cp.has_helipads:
|
|
trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI
|
|
for helipad in list(
|
|
cp.helipads + cp.helipads_quad + cp.helipads_invisible
|
|
):
|
|
if cp.position.distance_to_point(helipad) > trigger_radius:
|
|
trigger_radius = cp.position.distance_to_point(helipad)
|
|
for ground_spawn, ground_spawn_wp in list(
|
|
cp.ground_spawns + cp.ground_spawns_roadbase
|
|
):
|
|
if cp.position.distance_to_point(ground_spawn) > trigger_radius:
|
|
trigger_radius = cp.position.distance_to_point(ground_spawn)
|
|
trigger_radius += TRIGGER_RADIUS_PRETENSE_HELI_BUFFER
|
|
else:
|
|
if cp.dcs_airport is not None and (
|
|
isinstance(cp.dcs_airport, Damascus)
|
|
or isinstance(cp.dcs_airport, Khalkhalah)
|
|
or isinstance(cp.dcs_airport, Krasnodar_Pashkovsky)
|
|
):
|
|
# Increase the size of Pretense zones at Damascus, Khalkhalah and Krasnodar-Pashkovsky
|
|
# (which are quite spread out) so the zone would encompass the entire airfield.
|
|
trigger_radius = int(TRIGGER_RADIUS_CAPTURE * 1.8)
|
|
else:
|
|
trigger_radius = TRIGGER_RADIUS_CAPTURE
|
|
cp_name = "".join(
|
|
[i for i in cp.name if i.isalnum() or i.isspace() or i == "-"]
|
|
)
|
|
if not isinstance(cp, OffMapSpawn):
|
|
zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.15}
|
|
self.mission.triggers.add_triggerzone(
|
|
cp.position,
|
|
radius=trigger_radius,
|
|
hidden=False,
|
|
name=cp_name,
|
|
color=zone_color,
|
|
)
|
|
cp_name_trimmed = "".join([i for i in cp.name.lower() if i.isalpha()])
|
|
tgo_num = 0
|
|
for tgo in cp.ground_objects:
|
|
if cp.is_fleet or tgo.sea_object:
|
|
continue
|
|
tgo_num += 1
|
|
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
|
|
self.mission.triggers.add_triggerzone(
|
|
tgo.position,
|
|
radius=TRIGGER_RADIUS_PRETENSE_TGO,
|
|
hidden=False,
|
|
name=f"{cp_name_trimmed}-{tgo_num}",
|
|
color=zone_color,
|
|
)
|
|
for helipad in cp.helipads + cp.helipads_invisible + cp.helipads_quad:
|
|
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
|
|
self.mission.triggers.add_triggerzone(
|
|
position=helipad,
|
|
radius=TRIGGER_RADIUS_PRETENSE_HELI,
|
|
hidden=False,
|
|
name=f"{cp_name_trimmed}-hsp",
|
|
color=zone_color,
|
|
)
|
|
break
|
|
for supply_route in cp.convoy_routes.values():
|
|
tgo_num += 1
|
|
zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15}
|
|
origin_position = supply_route[0]
|
|
next_position = supply_route[1]
|
|
convoy_heading = origin_position.heading_between_point(next_position)
|
|
supply_position = origin_position.point_from_heading(
|
|
convoy_heading, 300
|
|
)
|
|
self.mission.triggers.add_triggerzone(
|
|
supply_position,
|
|
radius=TRIGGER_RADIUS_PRETENSE_TGO,
|
|
hidden=False,
|
|
name=f"{cp_name_trimmed}-sp",
|
|
color=zone_color,
|
|
)
|
|
break
|
|
airfields = [
|
|
cp for cp in self.game.theater.controlpoints if isinstance(cp, Airfield)
|
|
]
|
|
for airfield in airfields:
|
|
cp_airport = self.mission.terrain.airport_by_id(airfield.airport.id)
|
|
if cp_airport is None:
|
|
continue
|
|
cp_name_trimmed = "".join(
|
|
[i for i in cp_airport.name.lower() if i.isalpha()]
|
|
)
|
|
zone_color = {1: 0.0, 2: 1.0, 3: 0.5, 4: 0.15}
|
|
if cp_airport is None:
|
|
raise RuntimeError(
|
|
f"Could not find {airfield.airport.name} in the mission"
|
|
)
|
|
for runway in cp_airport.runways:
|
|
runway_end_1 = cp_airport.position.point_from_heading(
|
|
runway.heading, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2
|
|
)
|
|
runway_end_2 = cp_airport.position.point_from_heading(
|
|
runway.heading + 180, TRIGGER_RUNWAY_LENGTH_PRETENSE / 2
|
|
)
|
|
runway_verticies = [
|
|
runway_end_1.point_from_heading(
|
|
runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
|
|
),
|
|
runway_end_1.point_from_heading(
|
|
runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
|
|
),
|
|
runway_end_2.point_from_heading(
|
|
runway.heading + 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
|
|
),
|
|
runway_end_2.point_from_heading(
|
|
runway.heading - 90, TRIGGER_RUNWAY_WIDTH_PRETENSE / 2
|
|
),
|
|
]
|
|
trigger_zone = self.mission.triggers.add_triggerzone_quad(
|
|
cp_airport.position,
|
|
runway_verticies,
|
|
hidden=False,
|
|
name=f"{cp_name_trimmed}-runway-{runway.id}",
|
|
color=zone_color,
|
|
)
|
|
break
|
|
|
|
def generate(self) -> None:
|
|
player_coalition = "blue"
|
|
enemy_coalition = "red"
|
|
|
|
self._set_skill(player_coalition, enemy_coalition)
|
|
self._set_allegiances(player_coalition, enemy_coalition)
|
|
self._generate_pretense_zone_triggers()
|
|
self._generate_capture_triggers(player_coalition, enemy_coalition)
|
|
|
|
@classmethod
|
|
def get_capture_zone_flag(cls) -> int:
|
|
flag = cls.capture_zone_flag
|
|
cls.capture_zone_flag += 1
|
|
return flag
|