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
* **[Radios]** Added HF-FM band for AN/ARC-222
* **[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
* **[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 typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .formationattack import (
FormationAttackLayout,
FormationAttackBuilder,
FormationAttackFlightPlan,
)
from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
@ -22,48 +25,58 @@ if TYPE_CHECKING:
@dataclass
class AirAssaultLayout(StandardLayout):
class AirAssaultLayout(FormationAttackLayout):
# 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.
pickup: FlightWaypoint | None
nav_to_ingress: list[FlightWaypoint]
ingress: FlightWaypoint
drop_off: FlightWaypoint | None
pickup: FlightWaypoint | None = None
drop_off: FlightWaypoint | None = None
# 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.
target: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_ingress
yield from self.nav_to
yield self.ingress
if self.drop_off is not None:
yield self.drop_off
yield self.target
yield from self.nav_to_home
yield self.targets[0]
yield from self.nav_from
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
class AirAssaultFlightPlan(FormationAttackFlightPlan, UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@property
def is_airassault(self) -> bool:
return True
@property
def tot_waypoint(self) -> FlightWaypoint:
if self.flight.is_helo and self.layout.drop_off is not None:
return self.layout.drop_off
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:
if waypoint == self.tot_waypoint:
if waypoint is self.tot_waypoint:
return self.tot
elif waypoint is self.layout.ingress:
return self.ingress_time
return 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:
if not self.flight.is_helo and not self.flight.is_hercules:
raise PlanningError(
@ -130,7 +143,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
pickup=pickup,
nav_to_ingress=builder.nav_path(
nav_to=builder.nav_path(
pickup_position,
self.package.waypoints.ingress,
altitude,
@ -142,8 +155,8 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
self.package.target,
),
drop_off=dz,
target=assault_area,
nav_to_home=builder.nav_path(
targets=[assault_area],
nav_from=builder.nav_path(
drop_off_zone.position,
self.flight.arrival.position,
altitude,
@ -152,6 +165,10 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
hold=None,
join=builder.join(pickup_position),
split=builder.split(self.package.waypoints.split),
refuel=None,
)
def build(self) -> AirAssaultFlightPlan:

View File

@ -1,16 +1,35 @@
from __future__ import annotations
from datetime import timedelta
from typing import Type
from .airassault import AirAssaultLayout
from .airlift import AirliftLayout
from .formationattack import (
FormationAttackBuilder,
FormationAttackFlightPlan,
FormationAttackLayout,
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ..traveltime import TravelTime, GroundSpeed
from ...utils import feet
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
def builder_type() -> Type[Builder]:
return Builder
@ -26,31 +45,84 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
)
ingress.only_for_player = True
target.only_for_player = True
hold = builder.hold(self._hold_point())
if not self.primary_flight_is_air_assault:
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)
split = builder.split(self.package.waypoints.split)
refuel = builder.refuel(self.package.waypoints.refuel)
ingress_alt = self.doctrine.ingress_altitude
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,
)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
nav_to=builder.nav_path(
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(
departure=departure,
hold=hold,
nav_to=nav_to,
join=join,
ingress=ingress,
initial=initial,
targets=[target],
split=split,
refuel=refuel,
nav_from=builder.nav_path(
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),
nav_from=nav_from,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),

View File

@ -308,11 +308,15 @@ class FlightPlan(ABC, Generic[LayoutT]):
def estimate_ground_ops(self) -> timedelta:
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
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)
else:
return timedelta(minutes=8)
@property
def is_airassault(self) -> bool:
return False
@property
def mission_departure_time(self) -> timedelta:
"""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
join = None
if (
self.flight is self.package.primary_flight
or self.package.primary_flight
and isinstance(
self.package.primary_flight.flight_plan, FormationAttackFlightPlan
)
):
if self.primary_flight_is_air_assault:
hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
@ -240,6 +234,17 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
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
def target_waypoint(
flight: Flight, builder: WaypointBuilder, target: StrikeTarget

View File

@ -10,6 +10,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
Union,
Literal,
)
from dcs.mapping import Point, Vector2
@ -169,7 +170,7 @@ class WaypointBuilder:
"HOLD",
FlightWaypointType.LOITER,
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,
description="Wait until push time",
pretty_name="Hold",
@ -471,20 +472,23 @@ class WaypointBuilder:
)
return hold
@staticmethod
def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint:
def escort_hold(self, start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold.
Args:
start: Position of the waypoint.
altitude: Altitude of the holding pattern.
"""
alt_type: Literal["BARO", "RADIO"] = "BARO"
if self.is_helo:
alt_type = "RADIO"
return FlightWaypoint(
"ESCORT HOLD",
FlightWaypointType.CUSTOM,
start,
altitude,
alt_type=alt_type,
description="Anchor and hold at this point",
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
# might. be sufficient given the wiggle room. We can come up with
# another heuristic if needed.
cruise_mach = max_speed.mach() * (0.65 if flight.is_helo else 0.85)
return mach(cruise_mach, altitude)
cruise_mach = max_speed.mach() * (0.60 if flight.is_helo else 0.85)
return mach(cruise_mach, altitude if not flight.is_helo else meters(0))
class TravelTime:

View File

@ -43,8 +43,10 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory
using release_planned_aircraft.
"""
pf = self.package.primary_flight
heli = pf.is_helo if pf else False
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:
return False

View File

@ -82,11 +82,14 @@ class PackageFulfiller:
purchase_multiplier: int,
) -> None:
if not builder.plan_flight(flight):
pf = builder.package.primary_flight
heli = pf.is_helo if pf else False
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
task_capability=flight.task,
number=flight.num_aircraft * purchase_multiplier,
heli=heli,
)
# Reserves are planned for critical missions, so prioritize those orders
# 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 game.ato.flightplans.loiter import LoiterFlightPlan
from game.utils import meters
from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder
class HoldPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
speed = self.flight.squadron.aircraft.preferred_patrol_speed(
meters(waypoint.alt)
)
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):
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)
# end of hotfix
waypoint.add_task(loiter)
waypoint.add_task(OptFormation.finger_four_close())
if self.flight.is_helo:
waypoint.add_task(OptFormation.rotary_column())
else:
waypoint.add_task(OptFormation.finger_four_close())

View File

@ -8,6 +8,7 @@ from dcs.task import (
OptECMUsing,
OptFormation,
Targets,
OptROE,
)
from game.ato import FlightType
@ -18,17 +19,29 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class JoinPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
waypoint.tasks.append(OptFormation.finger_four_open())
if self.flight.is_helo:
waypoint.tasks.append(OptFormation.rotary_wedge())
else:
waypoint.tasks.append(OptFormation.finger_four_open())
doctrine = self.flight.coalition.doctrine
if self.flight.flight_type == FlightType.ESCORT:
targets = [
Targets.All.Air.Planes.Fighters.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.All.Air.Planes.Fighters.id,
Targets.All.Air.Planes.MultiroleFighters.id,
],
targets,
max_dist=doctrine.escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.escort_spacing.feet,
)
@ -71,10 +84,18 @@ class JoinPointBuilder(PydcsWaypointBuilder):
max_dist: float = 30.0,
vertical_spacing: float = 2000.0,
) -> None:
waypoint.tasks.append(OptROE(value=OptROE.Values.OpenFireWeaponFree))
rx = (random.random() + 0.1) * 333
ry = feet(vertical_spacing).meters
rz = (random.random() + 0.1) * 166 * random.choice([-1, 1])
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
if self.package.primary_flight is not None:
@ -83,7 +104,7 @@ class JoinPointBuilder(PydcsWaypointBuilder):
escort = ControlledTask(
EscortTaskAction(
group_id=group_id,
engagement_max_dist=int(nautical_miles(max_dist).meters),
engagement_max_dist=engage_dist,
targets=target_types,
position=pos,
)

View File

@ -1,6 +1,5 @@
from dcs.point import MovingPoint
from dcs.task import Land
from dcs.task import Land, RunScript
from .pydcswaypointbuilder import PydcsWaypointBuilder
@ -14,4 +13,9 @@ class LandingZoneBuilder(PydcsWaypointBuilder):
landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos
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

View File

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

View File

@ -43,9 +43,9 @@ class LogisticsGenerator:
# Preload fixed wing as they do not have a pickup zone
logistics_info.preload = logistics_info.preload or not self.flight.is_helo
# 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.flight.flight_plan.layout.target.position,
self.flight.flight_plan.layout.targets[0].position,
self.flight.flight_plan.ctld_target_zone_radius.meters,
False,
target_zone,

View File

@ -22,11 +22,12 @@ class AircraftProcurementRequest:
near: MissionTarget
task_capability: FlightType
number: int
heli: bool = False
def __str__(self) -> str:
task = self.task_capability.value
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:
@ -234,7 +235,11 @@ class ProcurementAi:
) -> Iterator[Squadron]:
threatened = []
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)

View File

@ -45,7 +45,12 @@ class AirWing:
return False
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]:
airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location)
best_aircraft = AircraftType.priority_list_for_task(task)
@ -55,7 +60,9 @@ class AirWing:
continue
capable_at_base = []
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)
if squadron.aircraft not in best_aircraft:
# If it is not already in the list it should be the last one
@ -79,9 +86,14 @@ class AirWing:
)
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]:
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 None

View File

@ -273,7 +273,12 @@ class Squadron:
return task in self.auto_assignable_mission_types
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:
if (
self.location.cptype.name in ["FOB", "FARP"]
@ -288,6 +293,15 @@ class Squadron:
if this_turn and not self.can_fulfill_flight(size):
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))
return distance_to_target <= self.aircraft.max_mission_range

View File

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

View File

@ -63,6 +63,24 @@ local unitPayloads = {
[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",
}

View File

@ -74,6 +74,31 @@ local unitPayloads = {
[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",
}

View File

@ -67,6 +67,38 @@ local unitPayloads = {
[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"] = {
},

View File

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

View File

@ -142,6 +142,32 @@ local unitPayloads = {
[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",
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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