Initial support for Armed Recon flight plan

This commit is contained in:
Raffson 2024-07-20 20:24:21 +02:00
parent c24fba0ba4
commit f405ffdfe2
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
19 changed files with 189 additions and 62 deletions

View File

@ -25,6 +25,7 @@
* **[Modding]** Added support for Su-15 Flagon mod (v1.0)
* **[Plugins]** Support for Carsten's Arty Spotter script
* **[Modding]** Added support for SK-60 mod (v1.2.1)
* **[Mission Generation]** Introducing the Armed Recon flight plan, i.e. CAS against any Theater Ground Object
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -0,0 +1,34 @@
from __future__ import annotations
from typing import Type
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .uizonedisplay import UiZone, UiZoneDisplay
from ..flightwaypointtype import FlightWaypointType
from ...utils import nautical_miles
class ArmedReconFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
def ui_zone(self) -> UiZone:
return UiZone(
[self.tot_waypoint.position],
nautical_miles(
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
),
)
class Builder(FormationAttackBuilder[ArmedReconFlightPlan, FormationAttackLayout]):
def layout(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_ARMED_RECON)
def build(self, dump_debug_info: bool = False) -> ArmedReconFlightPlan:
return ArmedReconFlightPlan(self.flight, self.layout())

View File

@ -125,16 +125,12 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
ingress_point_shapely.x, ingress_point_shapely.y
)
patrol_start_waypoint = builder.nav(
patrol_start, ingress_egress_altitude, use_agl_patrol_altitude
)
patrol_start_waypoint = builder.cas(patrol_start, ingress_egress_altitude)
patrol_start_waypoint.name = "FLOT START"
patrol_start_waypoint.pretty_name = "FLOT start"
patrol_start_waypoint.description = "FLOT boundary"
patrol_end_waypoint = builder.nav(
patrol_end, ingress_egress_altitude, use_agl_patrol_altitude
)
patrol_end_waypoint = builder.cas(patrol_end, ingress_egress_altitude)
patrol_end_waypoint.name = "FLOT END"
patrol_end_waypoint.pretty_name = "FLOT end"
patrol_end_waypoint.description = "FLOT boundary"

View File

@ -7,6 +7,7 @@ from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan
from .antiship import AntiShipFlightPlan
from .armedrecon import ArmedReconFlightPlan
from .bai import BaiFlightPlan
from .barcap import BarCapFlightPlan
from .cas import CasFlightPlan
@ -60,6 +61,7 @@ class FlightPlanBuilderTypes:
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
FlightType.FERRY: FerryFlightPlan.builder_type(),
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
}
try:
return builder_dict[flight.flight_type]

View File

@ -286,6 +286,8 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
return builder.sead_area(location)
elif flight.flight_type == FlightType.OCA_AIRCRAFT:
return builder.oca_strike_area(location)
elif flight.flight_type == FlightType.ARMED_RECON:
return builder.armed_recon_area(location)
else:
return builder.strike_area(location)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import math
import random
from dataclasses import dataclass
from typing import (
@ -252,18 +253,9 @@ class WaypointBuilder:
if ingress_type in [
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_OCA_AIRCRAFT,
FlightWaypointType.INGRESS_ARMED_RECON,
]:
weather = self.flight.coalition.game.conditions.weather
max_alt = feet(30000)
if weather.clouds and (
weather.clouds.preset
and "overcast" in weather.clouds.preset.description.lower()
or weather.clouds.density > 5
):
max_alt = meters(
max(feet(500).meters, weather.clouds.base - feet(500).meters)
)
alt = min(alt, max_alt)
alt = self._adjust_altitude_for_clouds(alt)
alt_type: AltitudeReference = "BARO"
if self.is_helo or self.flight.is_hercules:
@ -291,6 +283,19 @@ class WaypointBuilder:
targets=objective.strike_targets,
)
def _adjust_altitude_for_clouds(self, alt: Distance) -> Distance:
weather = self.flight.coalition.game.conditions.weather
max_alt = feet(math.inf)
if weather.clouds and (
weather.clouds.preset
and "overcast" in weather.clouds.preset.description.lower()
or weather.clouds.density > 5
):
max_alt = meters(
max(feet(500).meters, weather.clouds.base - feet(500).meters)
)
return min(alt, max_alt)
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
alt_type: AltitudeReference = "BARO"
if self.is_helo or self.get_combat_altitude.feet <= AGL_TRANSITION_ALT:
@ -354,6 +359,21 @@ class WaypointBuilder:
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"DEAD on {target.name}", target)
def armed_recon_area(self, target: MissionTarget) -> FlightWaypoint:
# Force AI aircraft to fly towards target area
alt = self.get_combat_altitude
alt = self._adjust_altitude_for_clouds(alt)
alt_type: AltitudeReference = "BARO"
if self.is_helo or alt.feet <= AGL_TRANSITION_ALT:
alt_type = "RADIO"
return self._target_area(
f"ARMED RECON {target.name}",
target,
altitude=alt,
alt_type=alt_type,
flyover=True,
)
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
@ -398,15 +418,14 @@ class WaypointBuilder:
waypoint.only_for_player = True
return waypoint
def cas(self, position: Point) -> FlightWaypoint:
def cas(self, position: Point, altitude: Distance) -> FlightWaypoint:
weather = self.flight.coalition.game.conditions.weather
max_alt = feet(30000)
if weather.clouds and (
weather.clouds.preset
and "overcast" in weather.clouds.preset.description.lower()
or weather.clouds.density > 5
):
max_alt = meters(
altitude = meters(
max(feet(500).meters, weather.clouds.base - feet(500).meters)
)
return FlightWaypoint(
@ -415,7 +434,7 @@ class WaypointBuilder:
position,
feet(self.flight.coalition.game.settings.heli_combat_alt_agl)
if self.is_helo
else min(meters(1000), max_alt),
else max(meters(1000), altitude),
"RADIO",
description="Provide CAS",
pretty_name="CAS",
@ -667,14 +686,13 @@ class WaypointBuilder:
This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator)
"""
heli_alt = feet(self.flight.coalition.game.settings.heli_cruise_alt_agl)
altitude = heli_alt if self.flight.is_helo else meters(0)
alt = self.get_combat_altitude if self.flight.is_helo else meters(0)
return FlightWaypoint(
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
drop_off.position,
altitude,
alt,
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop-off zone",

View File

@ -58,6 +58,7 @@ class FlightType(Enum):
FERRY = "Ferry"
AIR_ASSAULT = "Air Assault"
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
ARMED_RECON = "Armed Recon"
def __str__(self) -> str:
return self.value
@ -93,6 +94,7 @@ class FlightType(Enum):
FlightType.SEAD_ESCORT,
FlightType.AIR_ASSAULT,
FlightType.SEAD_SWEEP,
FlightType.ARMED_RECON,
}
@property
@ -104,6 +106,7 @@ class FlightType(Enum):
return {
FlightType.AEWC: AirEntity.AIRBORNE_EARLY_WARNING,
FlightType.ANTISHIP: AirEntity.ANTISURFACE_WARFARE,
FlightType.ARMED_RECON: AirEntity.ATTACK_STRIKE,
FlightType.BAI: AirEntity.ATTACK_STRIKE,
FlightType.BARCAP: AirEntity.FIGHTER,
FlightType.CAS: AirEntity.ATTACK_STRIKE,

View File

@ -51,3 +51,4 @@ class FlightWaypointType(IntEnum):
INGRESS_AIR_ASSAULT = 31
INGRESS_ANTI_SHIP = 32
INGRESS_SEAD_SWEEP = 33
INGRESS_ARMED_RECON = 34

View File

@ -208,6 +208,7 @@ class Loadout:
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
# OCA/Aircraft falls back to BAI, which falls back to CAS.
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
loadout_names[FlightType.ARMED_RECON].extend(loadout_names[FlightType.CAS])
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
# DEAD also falls back to BAI.
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])

View File

@ -183,6 +183,7 @@ class Package(RadioFrequencyContainer):
FlightType.SEAD_SWEEP,
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.ARMED_RECON,
FlightType.AEWC,
FlightType.FERRY,
FlightType.REFUELING,

View File

@ -167,24 +167,6 @@ class ObjectiveFinder:
yield cp
break
def vulnerable_enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over enemy CPs that are vulnerable to Air Assault.
Vulnerability is defined as any unit being alive in the CP's "blocking_capture" groups.
"""
for cp in self.enemy_control_points():
include = True
for tgo in cp.connected_objectives:
if tgo.distance_to(cp) > cp.CAPTURE_DISTANCE.meters:
continue
for u in tgo.units:
if u.is_vehicle and u.alive:
include = False
break
if not include:
break
if include:
yield cp
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
parking_type = ParkingType()
parking_type.include_rotary_wing = True

View File

@ -6,7 +6,7 @@ import math
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, TYPE_CHECKING, Union
from typing import Optional, TYPE_CHECKING, Union, Dict
from game.commander.battlepositions import BattlePositions
from game.commander.objectivefinder import ObjectiveFinder
@ -163,6 +163,15 @@ class TheaterState(WorldState["TheaterState"]):
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration)
battle_postitions: Dict[ControlPoint, BattlePositions] = {
cp: BattlePositions.for_control_point(cp)
for cp in ordered_capturable_points
}
vulnerable_control_points = [
cp for cp, bp in battle_postitions.items() if not bp.blocking_capture
]
return TheaterState(
context=context,
barcaps_needed={
@ -179,10 +188,7 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys=list(finder.convoys()),
enemy_shipping=list(finder.cargo_ships()),
enemy_ships=list(finder.enemy_ships()),
enemy_battle_positions={
cp: BattlePositions.for_control_point(cp)
for cp in ordered_capturable_points
},
enemy_battle_positions=battle_postitions,
oca_targets=list(
finder.oca_targets(
min_aircraft=game.settings.oca_target_autoplanner_min_aircraft_count
@ -191,5 +197,5 @@ class TheaterState(WorldState["TheaterState"]):
strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)),
threat_zones=game.threat_zone_for(not player),
vulnerable_control_points=list(finder.vulnerable_enemy_control_points()),
vulnerable_control_points=vulnerable_control_points,
)

View File

@ -241,9 +241,18 @@ class AircraftType(UnitType[Type[FlyingType]]):
def __post_init__(self) -> None:
enrich = {}
for t in self.task_priorities:
if t == FlightType.SEAD:
enrich[FlightType.SEAD_SWEEP] = self.task_priorities[t]
if FlightType.SEAD_SWEEP not in self.task_priorities:
if (value := self.task_priorities.get(FlightType.SEAD)) or (
value := self.task_priorities.get(FlightType.SEAD_ESCORT)
):
enrich[FlightType.SEAD_SWEEP] = value
if FlightType.ARMED_RECON not in self.task_priorities:
if (value := self.task_priorities.get(FlightType.CAS)) or (
value := self.task_priorities.get(FlightType.BAI)
):
enrich[FlightType.ARMED_RECON] = value
self.task_priorities.update(enrich)
@classmethod
@ -526,17 +535,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft)
from game.ato.flighttype import FlightType
task_priorities: dict[FlightType, int] = {}
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
if (
FlightType.SEAD_SWEEP not in task_priorities
and FlightType.SEAD in task_priorities
):
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
task_priorities = cls.get_task_priorities(data)
cls._custom_weapon_injections(aircraft, data)
cls._user_weapon_injections(aircraft)
@ -583,6 +582,27 @@ class AircraftType(UnitType[Type[FlyingType]]):
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
)
@classmethod
def get_task_priorities(cls, data: dict[str, Any]) -> dict[FlightType, int]:
task_priorities: dict[FlightType, int] = {}
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
if (
FlightType.SEAD_SWEEP not in task_priorities
and FlightType.SEAD in task_priorities
):
task_priorities[FlightType.SEAD_SWEEP] = task_priorities[FlightType.SEAD]
if FlightType.ARMED_RECON not in task_priorities:
if FlightType.CAS in task_priorities:
task_priorities[FlightType.ARMED_RECON] = task_priorities[
FlightType.CAS
]
elif FlightType.BAI in task_priorities:
task_priorities[FlightType.ARMED_RECON] = task_priorities[
FlightType.BAI
]
return task_priorities
@staticmethod
def _custom_weapon_injections(
aircraft: Type[FlyingType], data: Dict[str, Any]

View File

@ -57,6 +57,8 @@ class AircraftBehavior:
self.configure_refueling(group, flight)
elif self.task in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, flight)
elif self.task == FlightType.ARMED_RECON:
self.configure_armed_recon(group, flight)
elif self.task == FlightType.DEAD:
self.configure_dead(group, flight)
elif self.task in [FlightType.SEAD, FlightType.SEAD_SWEEP]:
@ -183,6 +185,17 @@ class AircraftBehavior:
restrict_jettison=True,
)
def configure_armed_recon(self, group: FlyingGroup[Any], flight: Flight) -> None:
self.configure_task(flight, group, CAS, [AFAC, AntishipStrike])
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
restrict_jettison=True,
)
def configure_dead(self, group: FlyingGroup[Any], flight: Flight) -> None:
# Only CAS and SEAD are capable of the Attack Group task. SEAD is arguably more
# appropriate but it has an extremely limited list of capable aircraft, whereas

View File

@ -0,0 +1,35 @@
from dcs.point import MovingPoint
from dcs.task import (
OptECMUsing,
ControlledTask,
Targets,
EngageTargetsInZone,
)
from game.utils import nautical_miles
from .pydcswaypointbuilder import PydcsWaypointBuilder
class ArmedReconIngressBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
self.register_special_ingress_points()
# Preemptively use ECM to better avoid getting swatted.
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
waypoint.tasks.append(ecm_option)
waypoint.add_task(
ControlledTask(
EngageTargetsInZone(
position=self.flight.flight_plan.tot_waypoint.position,
radius=int(
nautical_miles(
self.flight.coalition.game.settings.armed_recon_engagement_range_distance
).meters
),
targets=[
Targets.All.GroundUnits,
Targets.All.Air.Helicopters,
],
)
)
)

View File

@ -22,6 +22,7 @@ from game.settings import Settings
from game.utils import pairwise
from .airassaultingress import AirAssaultIngressBuilder
from .antishipingress import AntiShipIngressBuilder
from .armedreconingress import ArmedReconIngressBuilder
from .baiingress import BaiIngressBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
@ -136,6 +137,7 @@ class WaypointGenerator:
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
FlightWaypointType.INGRESS_AIR_ASSAULT: AirAssaultIngressBuilder,
FlightWaypointType.INGRESS_ANTI_SHIP: AntiShipIngressBuilder,
FlightWaypointType.INGRESS_ARMED_RECON: ArmedReconIngressBuilder,
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,

View File

@ -329,6 +329,14 @@ class Settings:
min=0,
max=100,
)
armed_recon_engagement_range_distance: int = bounded_int_option(
"Armed Recon engagement range (NM)",
page=CAMPAIGN_DOCTRINE_PAGE,
section=DOCTRINE_DISTANCES_SECTION,
default=5,
min=0,
max=25,
)
sead_sweep_engagement_range_distance: int = bounded_int_option(
"SEAD Sweep engagement range (NM)",
page=CAMPAIGN_DOCTRINE_PAGE,

View File

@ -39,6 +39,7 @@ class MissionTarget:
FlightType.TARCAP,
FlightType.SEAD_ESCORT,
FlightType.SEAD_SWEEP,
FlightType.ARMED_RECON,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,

View File

@ -161,6 +161,7 @@ class QAutoCreateDialog(QDialog):
FlightType.ANTISHIP,
FlightType.BAI,
FlightType.CAS,
FlightType.ARMED_RECON,
}
for mt in self.package.target.mission_types(self.is_ownfor):
if mt in primary_tasks: