Improve escort logic for helicopters

Babysteps #88
This commit is contained in:
Raffson 2023-08-15 00:57:47 +02:00
parent 270301958a
commit 789806637c
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
34 changed files with 417 additions and 87 deletions

View File

@ -12,6 +12,8 @@
* **[Modding]** Updated support for Su-57 mod to build-04 * **[Modding]** Updated support for Su-57 mod to build-04
* **[Radios]** Added HF-FM band for AN/ARC-222 * **[Radios]** Added HF-FM band for AN/ARC-222
* **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only) * **[Radios]** Ability to define preset channels for radios on squadron level (for human pilots only)
* **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa.
* **[Mission Planning]** Allow attack helicopters to escort other helicopters
## Fixes ## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task

View File

@ -4,12 +4,15 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.theater.controlpoint import ControlPointType from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters from game.utils import Distance, feet, meters
from ._common_ctld import generate_random_ctld_point from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder from .formationattack import (
FormationAttackLayout,
FormationAttackBuilder,
FormationAttackFlightPlan,
)
from .planningerror import PlanningError from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
@ -22,48 +25,58 @@ if TYPE_CHECKING:
@dataclass @dataclass
class AirAssaultLayout(StandardLayout): class AirAssaultLayout(FormationAttackLayout):
# The pickup point is optional because we don't always need to load the cargo. When # The pickup point is optional because we don't always need to load the cargo. When
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded. # departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
pickup: FlightWaypoint | None pickup: FlightWaypoint | None = None
nav_to_ingress: list[FlightWaypoint] drop_off: FlightWaypoint | None = None
ingress: FlightWaypoint
drop_off: FlightWaypoint | None
# This is an implementation detail used by CTLD. The aircraft will not go to this # This is an implementation detail used by CTLD. The aircraft will not go to this
# waypoint. It is used by CTLD as the destination for unloaded troops. # waypoint. It is used by CTLD as the destination for unloaded troops.
target: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
if self.pickup is not None: if self.pickup is not None:
yield self.pickup yield self.pickup
yield from self.nav_to_ingress yield from self.nav_to
yield self.ingress yield self.ingress
if self.drop_off is not None: if self.drop_off is not None:
yield self.drop_off yield self.drop_off
yield self.target yield self.targets[0]
yield from self.nav_to_home yield from self.nav_from
yield self.arrival yield self.arrival
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay): class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@staticmethod @staticmethod
def builder_type() -> Type[Builder]: def builder_type() -> Type[Builder]:
return Builder return Builder
@property
def is_airassault(self) -> bool:
return True
@property @property
def tot_waypoint(self) -> FlightWaypoint: def tot_waypoint(self) -> FlightWaypoint:
if self.flight.is_helo and self.layout.drop_off is not None: if self.flight.is_helo and self.layout.drop_off is not None:
return self.layout.drop_off return self.layout.drop_off
return self.layout.target return self.layout.target
@property
def ingress_time(self) -> timedelta:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.layout.drop_off
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint: if waypoint is self.tot_waypoint:
return self.tot return self.tot
elif waypoint is self.layout.ingress:
return self.ingress_time
return None return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
@ -84,7 +97,7 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
) )
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
def layout(self) -> AirAssaultLayout: def layout(self) -> AirAssaultLayout:
if not self.flight.is_helo and not self.flight.is_hercules: if not self.flight.is_helo and not self.flight.is_hercules:
raise PlanningError( raise PlanningError(
@ -130,7 +143,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
return AirAssaultLayout( return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
pickup=pickup, pickup=pickup,
nav_to_ingress=builder.nav_path( nav_to=builder.nav_path(
pickup_position, pickup_position,
self.package.waypoints.ingress, self.package.waypoints.ingress,
altitude, altitude,
@ -142,8 +155,8 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self.package.target, self.package.target,
), ),
drop_off=dz, drop_off=dz,
target=assault_area, targets=[assault_area],
nav_to_home=builder.nav_path( nav_from=builder.nav_path(
drop_off_zone.position, drop_off_zone.position,
self.flight.arrival.position, self.flight.arrival.position,
altitude, altitude,
@ -152,6 +165,10 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
hold=None,
join=builder.join(pickup_position),
split=builder.split(self.package.waypoints.split),
refuel=None,
) )
def build(self) -> AirAssaultFlightPlan: def build(self) -> AirAssaultFlightPlan:

View File

@ -1,16 +1,35 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from typing import Type from typing import Type
from .airassault import AirAssaultLayout
from .airlift import AirliftLayout
from .formationattack import ( from .formationattack import (
FormationAttackBuilder, FormationAttackBuilder,
FormationAttackFlightPlan, FormationAttackFlightPlan,
FormationAttackLayout, FormationAttackLayout,
) )
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ..traveltime import TravelTime, GroundSpeed
from ...utils import feet
class EscortFlightPlan(FormationAttackFlightPlan): class EscortFlightPlan(FormationAttackFlightPlan):
@property
def push_time(self) -> timedelta:
hold2join_time = (
TravelTime.between_points(
self.layout.hold.position,
self.layout.join.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
if self.layout.hold is not None
else timedelta(0)
)
return self.join_time - hold2join_time
@staticmethod @staticmethod
def builder_type() -> Type[Builder]: def builder_type() -> Type[Builder]:
return Builder return Builder
@ -26,31 +45,84 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
) )
ingress.only_for_player = True ingress.only_for_player = True
target.only_for_player = True target.only_for_player = True
if not self.primary_flight_is_air_assault:
hold = builder.hold(self._hold_point()) hold = builder.hold(self._hold_point())
elif self.package.primary_flight is not None:
fp = self.package.primary_flight.flight_plan
assert isinstance(fp.layout, AirAssaultLayout)
assert fp.layout.pickup is not None
hold = builder.hold(fp.layout.pickup.position)
join = builder.join(self.package.waypoints.join) join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split) split = builder.split(self.package.waypoints.split)
refuel = builder.refuel(self.package.waypoints.refuel)
ingress_alt = self.doctrine.ingress_altitude
initial = builder.escort_hold( initial = builder.escort_hold(
self.package.waypoints.initial, self.doctrine.ingress_altitude target.position
if builder.flight.is_helo
else self.package.waypoints.initial,
min(feet(500), ingress_alt) if builder.flight.is_helo else ingress_alt,
)
pf = self.package.primary_flight
if pf and pf.flight_type in [FlightType.AIR_ASSAULT, FlightType.TRANSPORT]:
layout = pf.flight_plan.layout
assert isinstance(layout, AirAssaultLayout) or isinstance(
layout, AirliftLayout
)
if isinstance(layout, AirliftLayout):
join = builder.join(layout.departure.position)
else:
join = builder.join(layout.ingress.position)
if layout.pickup:
join = builder.join(layout.pickup.position)
split = builder.split(layout.arrival.position)
if layout.drop_off:
initial = builder.escort_hold(
layout.drop_off.position,
min(feet(200), ingress_alt)
if builder.flight.is_helo
else ingress_alt,
)
refuel = None
if not self.flight.is_helo:
refuel = builder.refuel(self.package.waypoints.refuel)
departure = builder.takeoff(self.flight.departure)
if hold:
nav_to = builder.nav_path(
hold.position, join.position, self.doctrine.ingress_altitude
)
else:
nav_to = builder.nav_path(
departure.position, join.position, self.doctrine.ingress_altitude
)
if refuel:
nav_from = builder.nav_path(
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
)
else:
nav_from = builder.nav_path(
split.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
) )
return FormationAttackLayout( return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure), departure=departure,
hold=hold, hold=hold,
nav_to=builder.nav_path( nav_to=nav_to,
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join, join=join,
ingress=ingress, ingress=ingress,
initial=initial, initial=initial,
targets=[target], targets=[target],
split=split, split=split,
refuel=refuel, refuel=refuel,
nav_from=builder.nav_path( nav_from=nav_from,
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),

View File

@ -308,11 +308,15 @@ class FlightPlan(ABC, Generic[LayoutT]):
def estimate_ground_ops(self) -> timedelta: def estimate_ground_ops(self) -> timedelta:
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}: if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
return timedelta() return timedelta()
if self.flight.from_cp.is_fleet: if self.flight.from_cp.is_fleet or self.flight.from_cp.is_fob:
return timedelta(minutes=2) return timedelta(minutes=2)
else: else:
return timedelta(minutes=8) return timedelta(minutes=8)
@property
def is_airassault(self) -> bool:
return False
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
"""The time that the mission is complete and the flight RTBs.""" """The time that the mission is complete and the flight RTBs."""

View File

@ -187,13 +187,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
hold = None hold = None
join = None join = None
if ( if self.primary_flight_is_air_assault:
self.flight is self.package.primary_flight
or self.package.primary_flight
and isinstance(
self.package.primary_flight.flight_plan, FormationAttackFlightPlan
)
):
hold = builder.hold(self._hold_point()) hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join) join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split) split = builder.split(self.package.waypoints.split)
@ -240,6 +234,17 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
@property
def primary_flight_is_air_assault(self) -> bool:
if self.flight is self.package.primary_flight:
return True
else:
assert self.package.primary_flight is not None
fp = self.package.primary_flight.flight_plan
if fp.is_airassault:
return True
return False
@staticmethod @staticmethod
def target_waypoint( def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget flight: Flight, builder: WaypointBuilder, target: StrikeTarget

View File

@ -10,6 +10,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Union, Union,
Literal,
) )
from dcs.mapping import Point, Vector2 from dcs.mapping import Point, Vector2
@ -169,7 +170,7 @@ class WaypointBuilder:
"HOLD", "HOLD",
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
position, position,
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude, feet(1000) if self.is_helo else self.doctrine.rendezvous_altitude,
alt_type, alt_type,
description="Wait until push time", description="Wait until push time",
pretty_name="Hold", pretty_name="Hold",
@ -471,20 +472,23 @@ class WaypointBuilder:
) )
return hold return hold
@staticmethod def escort_hold(self, start: Point, altitude: Distance) -> FlightWaypoint:
def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold. """Creates custom waypoint for escort flights that need to hold.
Args: Args:
start: Position of the waypoint. start: Position of the waypoint.
altitude: Altitude of the holding pattern. altitude: Altitude of the holding pattern.
""" """
alt_type: Literal["BARO", "RADIO"] = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint( return FlightWaypoint(
"ESCORT HOLD", "ESCORT HOLD",
FlightWaypointType.CUSTOM, FlightWaypointType.CUSTOM,
start, start,
altitude, altitude,
alt_type=alt_type,
description="Anchor and hold at this point", description="Anchor and hold at this point",
pretty_name="Escort Hold", pretty_name="Escort Hold",
) )

View File

@ -39,8 +39,8 @@ class GroundSpeed:
# as it can at sea level. This probably isn't great assumption, but # as it can at sea level. This probably isn't great assumption, but
# might. be sufficient given the wiggle room. We can come up with # might. be sufficient given the wiggle room. We can come up with
# another heuristic if needed. # another heuristic if needed.
cruise_mach = max_speed.mach() * (0.65 if flight.is_helo else 0.85) cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
return mach(cruise_mach, altitude) return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
class TravelTime: class TravelTime:

View File

@ -43,8 +43,10 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory caller should return any previously planned flights to the inventory
using release_planned_aircraft. using release_planned_aircraft.
""" """
pf = self.package.primary_flight
heli = pf.is_helo if pf else False
squadron = self.air_wing.best_squadron_for( squadron = self.air_wing.best_squadron_for(
self.package.target, plan.task, plan.num_aircraft, this_turn=True self.package.target, plan.task, plan.num_aircraft, heli, this_turn=True
) )
if squadron is None: if squadron is None:
return False return False

View File

@ -82,11 +82,14 @@ class PackageFulfiller:
purchase_multiplier: int, purchase_multiplier: int,
) -> None: ) -> None:
if not builder.plan_flight(flight): if not builder.plan_flight(flight):
pf = builder.package.primary_flight
heli = pf.is_helo if pf else False
missing_types.add(flight.task) missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest( purchase_order = AircraftProcurementRequest(
near=mission.location, near=mission.location,
task_capability=flight.task, task_capability=flight.task,
number=flight.num_aircraft * purchase_multiplier, number=flight.num_aircraft * purchase_multiplier,
heli=heli,
) )
# Reserves are planned for critical missions, so prioritize those orders # Reserves are planned for critical missions, so prioritize those orders
# over aircraft needed for non-critical missions. # over aircraft needed for non-critical missions.

View File

@ -4,14 +4,22 @@ from dcs.point import MovingPoint
from dcs.task import ControlledTask, OptFormation, OrbitAction from dcs.task import ControlledTask, OptFormation, OrbitAction
from game.ato.flightplans.loiter import LoiterFlightPlan from game.ato.flightplans.loiter import LoiterFlightPlan
from game.utils import meters
from ._helper import create_stop_orbit_trigger from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder from .pydcswaypointbuilder import PydcsWaypointBuilder
class HoldPointBuilder(PydcsWaypointBuilder): class HoldPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None: def add_tasks(self, waypoint: MovingPoint) -> None:
speed = self.flight.squadron.aircraft.preferred_patrol_speed(
meters(waypoint.alt)
)
loiter = ControlledTask( loiter = ControlledTask(
OrbitAction(altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.Circle) OrbitAction(
altitude=waypoint.alt,
speed=speed.meters_per_second,
pattern=OrbitAction.OrbitPattern.Circle,
)
) )
if not isinstance(self.flight.flight_plan, LoiterFlightPlan): if not isinstance(self.flight.flight_plan, LoiterFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__ flight_plan_type = self.flight.flight_plan.__class__.__name__
@ -29,4 +37,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed) create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)
# end of hotfix # end of hotfix
waypoint.add_task(loiter) waypoint.add_task(loiter)
if self.flight.is_helo:
waypoint.add_task(OptFormation.rotary_column())
else:
waypoint.add_task(OptFormation.finger_four_close()) waypoint.add_task(OptFormation.finger_four_close())

View File

@ -8,6 +8,7 @@ from dcs.task import (
OptECMUsing, OptECMUsing,
OptFormation, OptFormation,
Targets, Targets,
OptROE,
) )
from game.ato import FlightType from game.ato import FlightType
@ -18,17 +19,29 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class JoinPointBuilder(PydcsWaypointBuilder): class JoinPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None: def add_tasks(self, waypoint: MovingPoint) -> None:
if self.flight.is_helo:
waypoint.tasks.append(OptFormation.rotary_wedge())
else:
waypoint.tasks.append(OptFormation.finger_four_open()) waypoint.tasks.append(OptFormation.finger_four_open())
doctrine = self.flight.coalition.doctrine doctrine = self.flight.coalition.doctrine
if self.flight.flight_type == FlightType.ESCORT: if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks( targets = [
waypoint,
[
Targets.All.Air.Planes.Fighters.id, Targets.All.Air.Planes.Fighters.id,
Targets.All.Air.Planes.MultiroleFighters.id, Targets.All.Air.Planes.MultiroleFighters.id,
], ]
if self.flight.is_helo:
targets = [
Targets.All.Air.Helicopters.id,
Targets.All.GroundUnits.AirDefence.id,
Targets.All.GroundUnits.GroundVehicles.UnarmedVehicles.id,
Targets.All.GroundUnits.GroundVehicles.ArmoredVehicles.id,
Targets.All.Naval.Ships.ArmedShips.LightArmedShips.id,
]
self.configure_escort_tasks(
waypoint,
targets,
max_dist=doctrine.escort_engagement_range.nautical_miles, max_dist=doctrine.escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.escort_spacing.feet, vertical_spacing=doctrine.escort_spacing.feet,
) )
@ -71,10 +84,18 @@ class JoinPointBuilder(PydcsWaypointBuilder):
max_dist: float = 30.0, max_dist: float = 30.0,
vertical_spacing: float = 2000.0, vertical_spacing: float = 2000.0,
) -> None: ) -> None:
waypoint.tasks.append(OptROE(value=OptROE.Values.OpenFireWeaponFree))
rx = (random.random() + 0.1) * 333 rx = (random.random() + 0.1) * 333
ry = feet(vertical_spacing).meters ry = feet(vertical_spacing).meters
rz = (random.random() + 0.1) * 166 * random.choice([-1, 1]) rz = (random.random() + 0.1) * 166 * random.choice([-1, 1])
pos = {"x": rx, "y": ry, "z": rz} pos = {"x": rx, "y": ry, "z": rz}
engage_dist = int(nautical_miles(max_dist).meters)
if self.flight.is_helo:
for key in pos:
pos[key] *= 0.25
engage_dist = int(engage_dist * 0.25)
group_id = None group_id = None
if self.package.primary_flight is not None: if self.package.primary_flight is not None:
@ -83,7 +104,7 @@ class JoinPointBuilder(PydcsWaypointBuilder):
escort = ControlledTask( escort = ControlledTask(
EscortTaskAction( EscortTaskAction(
group_id=group_id, group_id=group_id,
engagement_max_dist=int(nautical_miles(max_dist).meters), engagement_max_dist=engage_dist,
targets=target_types, targets=target_types,
position=pos, position=pos,
) )

View File

@ -1,6 +1,5 @@
from dcs.point import MovingPoint from dcs.point import MovingPoint
from dcs.task import Land from dcs.task import Land, RunScript
from .pydcswaypointbuilder import PydcsWaypointBuilder from .pydcswaypointbuilder import PydcsWaypointBuilder
@ -14,4 +13,9 @@ class LandingZoneBuilder(PydcsWaypointBuilder):
landing_point = waypoint.position.random_point_within(15, 5) landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos # Use Land Task with 30s duration for helos
waypoint.add_task(Land(landing_point, duration=30)) waypoint.add_task(Land(landing_point, duration=30))
if waypoint.name == "DROPOFFZONE":
script = RunScript(
f'trigger.action.setUserFlag("split-{id(self.package)}", true)'
)
waypoint.tasks.append(script)
return waypoint return waypoint

View File

@ -15,6 +15,9 @@ class SplitPointBuilder(PydcsWaypointBuilder):
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar) ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
waypoint.tasks.append(ecm_option) waypoint.tasks.append(ecm_option)
if self.flight.is_helo:
waypoint.tasks.append(OptFormation.rotary_wedge())
else:
waypoint.tasks.append(OptFormation.finger_four_close()) waypoint.tasks.append(OptFormation.finger_four_close())
waypoint.speed_locked = True waypoint.speed_locked = True
waypoint.speed = self.flight.coalition.doctrine.rtb_speed.meters_per_second waypoint.speed = self.flight.coalition.doctrine.rtb_speed.meters_per_second

View File

@ -45,7 +45,7 @@ class LogisticsGenerator:
# Create the Waypoint Zone used by CTLD # Create the Waypoint Zone used by CTLD
target_zone = f"{self.group.name} TARGET_ZONE" target_zone = f"{self.group.name} TARGET_ZONE"
self.mission.triggers.add_triggerzone( self.mission.triggers.add_triggerzone(
self.flight.flight_plan.layout.target.position, self.flight.flight_plan.layout.targets[0].position,
self.flight.flight_plan.ctld_target_zone_radius.meters, self.flight.flight_plan.ctld_target_zone_radius.meters,
False, False,
target_zone, target_zone,

View File

@ -22,11 +22,12 @@ class AircraftProcurementRequest:
near: MissionTarget near: MissionTarget
task_capability: FlightType task_capability: FlightType
number: int number: int
heli: bool = False
def __str__(self) -> str: def __str__(self) -> str:
task = self.task_capability.value task = self.task_capability.value
target = self.near.name target = self.near.name
return f"{self.number} ship {task} near {target}" return f"{self.number} ship {task} near {target} (heli={self.heli})"
class ProcurementAi: class ProcurementAi:
@ -234,7 +235,11 @@ class ProcurementAi:
) -> Iterator[Squadron]: ) -> Iterator[Squadron]:
threatened = [] threatened = []
for squadron in self.air_wing.best_squadrons_for( for squadron in self.air_wing.best_squadrons_for(
request.near, request.task_capability, request.number, this_turn=False request.near,
request.task_capability,
request.number,
request.heli,
this_turn=False,
): ):
parking_type = ParkingType().from_squadron(squadron) parking_type = ParkingType().from_squadron(squadron)

View File

@ -45,7 +45,12 @@ class AirWing:
return False return False
def best_squadrons_for( def best_squadrons_for(
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool self,
location: MissionTarget,
task: FlightType,
size: int,
heli: bool,
this_turn: bool,
) -> list[Squadron]: ) -> list[Squadron]:
airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location) airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location)
best_aircraft = AircraftType.priority_list_for_task(task) best_aircraft = AircraftType.priority_list_for_task(task)
@ -55,7 +60,9 @@ class AirWing:
continue continue
capable_at_base = [] capable_at_base = []
for squadron in control_point.squadrons: for squadron in control_point.squadrons:
if squadron.can_auto_assign_mission(location, task, size, this_turn): if squadron.can_auto_assign_mission(
location, task, size, heli, this_turn
):
capable_at_base.append(squadron) capable_at_base.append(squadron)
if squadron.aircraft not in best_aircraft: if squadron.aircraft not in best_aircraft:
# If it is not already in the list it should be the last one # If it is not already in the list it should be the last one
@ -79,9 +86,14 @@ class AirWing:
) )
def best_squadron_for( def best_squadron_for(
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool self,
location: MissionTarget,
task: FlightType,
size: int,
heli: bool,
this_turn: bool,
) -> Optional[Squadron]: ) -> Optional[Squadron]:
for squadron in self.best_squadrons_for(location, task, size, this_turn): for squadron in self.best_squadrons_for(location, task, size, heli, this_turn):
return squadron return squadron
return None return None

View File

@ -273,7 +273,12 @@ class Squadron:
return task in self.auto_assignable_mission_types return task in self.auto_assignable_mission_types
def can_auto_assign_mission( def can_auto_assign_mission(
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool self,
location: MissionTarget,
task: FlightType,
size: int,
heli: bool,
this_turn: bool,
) -> bool: ) -> bool:
if ( if (
self.location.cptype.name in ["FOB", "FARP"] self.location.cptype.name in ["FOB", "FARP"]
@ -288,6 +293,15 @@ class Squadron:
if this_turn and not self.can_fulfill_flight(size): if this_turn and not self.can_fulfill_flight(size):
return False return False
if task in [FlightType.ESCORT, FlightType.SEAD_ESCORT]:
if heli and not self.aircraft.helicopter and not self.aircraft.lha_capable:
return False
if not heli and self.aircraft.helicopter:
return False
if heli and task == FlightType.REFUELING:
return False
distance_to_target = meters(location.distance_to(self.location)) distance_to_target = meters(location.distance_to(self.location))
return distance_to_target <= self.aircraft.max_mission_range return distance_to_target <= self.aircraft.max_mission_range

View File

@ -626,6 +626,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
""" """
return False return False
@property
def is_fob(self) -> bool:
"""
:return: Whether this control point is a FOB
"""
return False
@property @property
def moveable(self) -> bool: def moveable(self) -> bool:
""" """
@ -1605,6 +1612,13 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
return 10 return 10
@property
def is_fob(self) -> bool:
"""
:return: Whether this control point is a FOB
"""
return True
@property @property
def category(self) -> str: def category(self) -> str:
return "fob" return "fob"

View File

@ -63,6 +63,24 @@ local unitPayloads = {
[3] = 32, [3] = 32,
}, },
}, },
[4] = {
["name"] = "Retribution Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 4,
},
},
["tasks"] = {
[1] = 18,
[2] = 31,
[3] = 32,
},
},
}, },
["unitType"] = "AH-1W", ["unitType"] = "AH-1W",
} }

View File

@ -74,6 +74,31 @@ local unitPayloads = {
[1] = 31, [1] = 31,
}, },
}, },
[4] = {
["displayName"] = "Retribution Escort",
["name"] = "Retribution Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 31,
},
},
}, },
["unitType"] = "AH-64D_BLK_II", ["unitType"] = "AH-64D_BLK_II",
} }

View File

@ -67,6 +67,38 @@ local unitPayloads = {
[2] = 32, [2] = 32,
}, },
}, },
[3] = {
["name"] = "Retribution Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{9S846_2xIGLA}",
["num"] = 6,
},
[2] = {
["CLSID"] = "{9S846_2xIGLA}",
["num"] = 5,
},
[3] = {
["CLSID"] = "{A6FD14D3-6D30-4C85-88A7-8D17BEE120E2}",
["num"] = 4,
},
[4] = {
["CLSID"] = "{A6FD14D3-6D30-4C85-88A7-8D17BEE120E2}",
["num"] = 1,
},
[5] = {
["CLSID"] = "B_8V20A_OFP2",
["num"] = 3,
},
[6] = {
["CLSID"] = "B_8V20A_OFP2",
["num"] = 2,
},
},
["tasks"] = {
[1] = 31,
},
},
}, },
["tasks"] = { ["tasks"] = {
}, },

View File

@ -6,11 +6,11 @@ local unitPayloads = {
["name"] = "Retribution CAS", ["name"] = "Retribution CAS",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
[2] = { [2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[3] = { [3] = {
@ -22,11 +22,11 @@ local unitPayloads = {
["num"] = 3, ["num"] = 3,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5, ["num"] = 5,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2, ["num"] = 2,
}, },
}, },
@ -41,11 +41,11 @@ local unitPayloads = {
["name"] = "Retribution BAI", ["name"] = "Retribution BAI",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
[2] = { [2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[3] = { [3] = {
@ -57,11 +57,11 @@ local unitPayloads = {
["num"] = 3, ["num"] = 3,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5, ["num"] = 5,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2, ["num"] = 2,
}, },
}, },
@ -92,11 +92,11 @@ local unitPayloads = {
["num"] = 5, ["num"] = 5,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
}, },
@ -109,11 +109,11 @@ local unitPayloads = {
["name"] = "Retribution Antiship", ["name"] = "Retribution Antiship",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
[2] = { [2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[3] = { [3] = {
@ -125,11 +125,11 @@ local unitPayloads = {
["num"] = 3, ["num"] = 3,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5, ["num"] = 5,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2, ["num"] = 2,
}, },
}, },
@ -144,11 +144,11 @@ local unitPayloads = {
["name"] = "Retribution SEAD", ["name"] = "Retribution SEAD",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
[2] = { [2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[3] = { [3] = {
@ -160,11 +160,11 @@ local unitPayloads = {
["num"] = 3, ["num"] = 3,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5, ["num"] = 5,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2, ["num"] = 2,
}, },
}, },
@ -179,11 +179,11 @@ local unitPayloads = {
["name"] = "Retribution DEAD", ["name"] = "Retribution DEAD",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 6, ["num"] = 6,
}, },
[2] = { [2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 1, ["num"] = 1,
}, },
[3] = { [3] = {
@ -195,11 +195,11 @@ local unitPayloads = {
["num"] = 3, ["num"] = 3,
}, },
[5] = { [5] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5, ["num"] = 5,
}, },
[6] = { [6] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}", ["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2, ["num"] = 2,
}, },
}, },
@ -209,6 +209,31 @@ local unitPayloads = {
[3] = 32, [3] = 32,
}, },
}, },
[7] = {
["displayName"] = "Retribution Escort",
["name"] = "Retribution Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{2x9M220_Ataka_V}",
["num"] = 6,
},
[2] = {
["CLSID"] = "{2x9M220_Ataka_V}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 5,
},
[4] = {
["CLSID"] = "{2x9M120_Ataka_V}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 31,
},
},
}, },
["tasks"] = { ["tasks"] = {
}, },

View File

@ -142,6 +142,32 @@ local unitPayloads = {
[3] = 18, [3] = 18,
}, },
}, },
[6] = {
["name"] = "Retribution Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}",
["num"] = 5,
},
[4] = {
["CLSID"] = "{B919B0F4-7C25-455E-9A02-CEA51DB895E3}",
["num"] = 6,
},
},
["tasks"] = {
[1] = 31,
[2] = 32,
[3] = 18,
},
},
}, },
["unitType"] = "Mi-24V", ["unitType"] = "Mi-24V",
} }

View File

@ -22,4 +22,5 @@ variants:
tasks: tasks:
BAI: 480 BAI: 480
CAS: 480 CAS: 480
Escort: 100
OCA/Aircraft: 480 OCA/Aircraft: 480

View File

@ -23,4 +23,5 @@ variants:
tasks: tasks:
BAI: 490 BAI: 490
CAS: 490 CAS: 490
Escort: 80
OCA/Aircraft: 490 OCA/Aircraft: 490

View File

@ -24,4 +24,5 @@ variants:
tasks: tasks:
BAI: 500 BAI: 500
CAS: 500 CAS: 500
Escort: 90
OCA/Aircraft: 500 OCA/Aircraft: 500

View File

@ -37,4 +37,5 @@ radios:
tasks: tasks:
BAI: 510 BAI: 510
CAS: 510 CAS: 510
Escort: 100
OCA/Aircraft: 510 OCA/Aircraft: 510

View File

@ -27,4 +27,5 @@ kneeboard_units: "metric"
tasks: tasks:
BAI: 430 BAI: 430
CAS: 430 CAS: 430
Escort: 90
OCA/Aircraft: 430 OCA/Aircraft: 430

View File

@ -28,4 +28,5 @@ kneeboard_units: "metric"
tasks: tasks:
BAI: 440 BAI: 440
CAS: 440 CAS: 440
Escort: 100
OCA/Aircraft: 440 OCA/Aircraft: 440

View File

@ -39,4 +39,5 @@ tasks:
Air Assault: 20 Air Assault: 20
BAI: 410 BAI: 410
CAS: 410 CAS: 410
Escort: 100
OCA/Aircraft: 410 OCA/Aircraft: 410

View File

@ -29,4 +29,5 @@ tasks:
Air Assault: 10 Air Assault: 10
BAI: 400 BAI: 400
CAS: 400 CAS: 400
Escort: 100
OCA/Aircraft: 400 OCA/Aircraft: 400

View File

@ -18,4 +18,5 @@ variants:
tasks: tasks:
BAI: 420 BAI: 420
CAS: 420 CAS: 420
Escort: 100
OCA/Aircraft: 420 OCA/Aircraft: 420

View File

@ -23,4 +23,5 @@ kneeboard_units: "metric"
tasks: tasks:
BAI: 450 BAI: 450
CAS: 450 CAS: 450
Escort: 100
OCA/Aircraft: 450 OCA/Aircraft: 450

View File

@ -26,4 +26,5 @@ kneeboard_units: "metric"
tasks: tasks:
BAI: 460 BAI: 460
CAS: 460 CAS: 460
Escort: 90
OCA/Aircraft: 460 OCA/Aircraft: 460