mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
parent
270301958a
commit
789806637c
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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"] = {
|
||||
},
|
||||
|
||||
@ -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"] = {
|
||||
},
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -22,4 +22,5 @@ variants:
|
||||
tasks:
|
||||
BAI: 480
|
||||
CAS: 480
|
||||
Escort: 100
|
||||
OCA/Aircraft: 480
|
||||
|
||||
@ -23,4 +23,5 @@ variants:
|
||||
tasks:
|
||||
BAI: 490
|
||||
CAS: 490
|
||||
Escort: 80
|
||||
OCA/Aircraft: 490
|
||||
|
||||
@ -24,4 +24,5 @@ variants:
|
||||
tasks:
|
||||
BAI: 500
|
||||
CAS: 500
|
||||
Escort: 90
|
||||
OCA/Aircraft: 500
|
||||
|
||||
@ -37,4 +37,5 @@ radios:
|
||||
tasks:
|
||||
BAI: 510
|
||||
CAS: 510
|
||||
Escort: 100
|
||||
OCA/Aircraft: 510
|
||||
|
||||
@ -27,4 +27,5 @@ kneeboard_units: "metric"
|
||||
tasks:
|
||||
BAI: 430
|
||||
CAS: 430
|
||||
Escort: 90
|
||||
OCA/Aircraft: 430
|
||||
|
||||
@ -28,4 +28,5 @@ kneeboard_units: "metric"
|
||||
tasks:
|
||||
BAI: 440
|
||||
CAS: 440
|
||||
Escort: 100
|
||||
OCA/Aircraft: 440
|
||||
|
||||
@ -39,4 +39,5 @@ tasks:
|
||||
Air Assault: 20
|
||||
BAI: 410
|
||||
CAS: 410
|
||||
Escort: 100
|
||||
OCA/Aircraft: 410
|
||||
|
||||
@ -29,4 +29,5 @@ tasks:
|
||||
Air Assault: 10
|
||||
BAI: 400
|
||||
CAS: 400
|
||||
Escort: 100
|
||||
OCA/Aircraft: 400
|
||||
|
||||
@ -18,4 +18,5 @@ variants:
|
||||
tasks:
|
||||
BAI: 420
|
||||
CAS: 420
|
||||
Escort: 100
|
||||
OCA/Aircraft: 420
|
||||
|
||||
@ -23,4 +23,5 @@ kneeboard_units: "metric"
|
||||
tasks:
|
||||
BAI: 450
|
||||
CAS: 450
|
||||
Escort: 100
|
||||
OCA/Aircraft: 450
|
||||
|
||||
@ -26,4 +26,5 @@ kneeboard_units: "metric"
|
||||
tasks:
|
||||
BAI: 460
|
||||
CAS: 460
|
||||
Escort: 90
|
||||
OCA/Aircraft: 460
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user