From 789806637ceb6da31f6489b94b630fd3c72cbb06 Mon Sep 17 00:00:00 2001 From: Raffson Date: Tue, 15 Aug 2023 00:57:47 +0200 Subject: [PATCH] Improve escort logic for helicopters Babysteps #88 --- changelog.md | 2 + game/ato/flightplans/airassault.py | 53 ++++++---- game/ato/flightplans/escort.py | 98 ++++++++++++++++--- game/ato/flightplans/flightplan.py | 6 +- game/ato/flightplans/formationattack.py | 19 ++-- game/ato/flightplans/waypointbuilder.py | 10 +- game/ato/traveltime.py | 4 +- game/commander/packagebuilder.py | 4 +- game/commander/packagefulfiller.py | 3 + .../aircraft/waypoints/holdpoint.py | 15 ++- .../aircraft/waypoints/joinpoint.py | 33 +++++-- .../aircraft/waypoints/landingzone.py | 8 +- .../aircraft/waypoints/splitpoint.py | 5 +- game/missiongenerator/logisticsgenerator.py | 4 +- game/procurement.py | 9 +- game/squadrons/airwing.py | 20 +++- game/squadrons/squadron.py | 16 ++- game/theater/controlpoint.py | 14 +++ resources/customized_payloads/AH-1W.lua | 18 ++++ .../customized_payloads/AH-64D_BLK_II.lua | 25 +++++ resources/customized_payloads/Ka-50_3.lua | 32 ++++++ resources/customized_payloads/Mi-24P.lua | 69 ++++++++----- resources/customized_payloads/Mi-24V.lua | 26 +++++ resources/units/aircraft/AH-1W.yaml | 1 + resources/units/aircraft/AH-64A.yaml | 1 + resources/units/aircraft/AH-64D.yaml | 1 + resources/units/aircraft/AH-64D_BLK_II.yaml | 1 + resources/units/aircraft/Ka-50.yaml | 1 + resources/units/aircraft/Ka-50_3.yaml | 1 + resources/units/aircraft/Mi-24P.yaml | 1 + resources/units/aircraft/Mi-24V.yaml | 1 + resources/units/aircraft/Mi-28N.yaml | 1 + resources/units/aircraft/SA342L.yaml | 1 + resources/units/aircraft/SA342M.yaml | 1 + 34 files changed, 417 insertions(+), 87 deletions(-) diff --git a/changelog.md b/changelog.md index 456d99c4..76659847 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index 2a85b698..257cde9e 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -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: diff --git a/game/ato/flightplans/escort.py b/game/ato/flightplans/escort.py index 9799d591..999fa805 100644 --- a/game/ato/flightplans/escort.py +++ b/game/ato/flightplans/escort.py @@ -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(), diff --git a/game/ato/flightplans/flightplan.py b/game/ato/flightplans/flightplan.py index c21844b4..4a2ac509 100644 --- a/game/ato/flightplans/flightplan.py +++ b/game/ato/flightplans/flightplan.py @@ -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.""" diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py index 8af05c1f..35cc667c 100644 --- a/game/ato/flightplans/formationattack.py +++ b/game/ato/flightplans/formationattack.py @@ -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 diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index cd126161..9cd843b2 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -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", ) diff --git a/game/ato/traveltime.py b/game/ato/traveltime.py index e48731c4..6f984904 100644 --- a/game/ato/traveltime.py +++ b/game/ato/traveltime.py @@ -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: diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index 101c00d8..be6fb785 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -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 diff --git a/game/commander/packagefulfiller.py b/game/commander/packagefulfiller.py index 9c067bb9..c4dcd6d2 100644 --- a/game/commander/packagefulfiller.py +++ b/game/commander/packagefulfiller.py @@ -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. diff --git a/game/missiongenerator/aircraft/waypoints/holdpoint.py b/game/missiongenerator/aircraft/waypoints/holdpoint.py index 67fc81a2..bead0c1a 100644 --- a/game/missiongenerator/aircraft/waypoints/holdpoint.py +++ b/game/missiongenerator/aircraft/waypoints/holdpoint.py @@ -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()) diff --git a/game/missiongenerator/aircraft/waypoints/joinpoint.py b/game/missiongenerator/aircraft/waypoints/joinpoint.py index 5a91588e..fde06286 100644 --- a/game/missiongenerator/aircraft/waypoints/joinpoint.py +++ b/game/missiongenerator/aircraft/waypoints/joinpoint.py @@ -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, ) diff --git a/game/missiongenerator/aircraft/waypoints/landingzone.py b/game/missiongenerator/aircraft/waypoints/landingzone.py index 338bb97c..2e91189d 100644 --- a/game/missiongenerator/aircraft/waypoints/landingzone.py +++ b/game/missiongenerator/aircraft/waypoints/landingzone.py @@ -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 diff --git a/game/missiongenerator/aircraft/waypoints/splitpoint.py b/game/missiongenerator/aircraft/waypoints/splitpoint.py index 8b168e95..74cc5dd0 100644 --- a/game/missiongenerator/aircraft/waypoints/splitpoint.py +++ b/game/missiongenerator/aircraft/waypoints/splitpoint.py @@ -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 diff --git a/game/missiongenerator/logisticsgenerator.py b/game/missiongenerator/logisticsgenerator.py index b73c0ca4..967fd5e0 100644 --- a/game/missiongenerator/logisticsgenerator.py +++ b/game/missiongenerator/logisticsgenerator.py @@ -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, diff --git a/game/procurement.py b/game/procurement.py index ce7015f0..d87ac6dd 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -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) diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index b903bfe6..4d50b78e 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -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 diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 08a66861..b8987493 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -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 diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 3ee1b3cc..6b7879a3 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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" diff --git a/resources/customized_payloads/AH-1W.lua b/resources/customized_payloads/AH-1W.lua index a8fb30b3..24cfd151 100644 --- a/resources/customized_payloads/AH-1W.lua +++ b/resources/customized_payloads/AH-1W.lua @@ -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", } diff --git a/resources/customized_payloads/AH-64D_BLK_II.lua b/resources/customized_payloads/AH-64D_BLK_II.lua index 00af78f4..4cbb8c34 100644 --- a/resources/customized_payloads/AH-64D_BLK_II.lua +++ b/resources/customized_payloads/AH-64D_BLK_II.lua @@ -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", } diff --git a/resources/customized_payloads/Ka-50_3.lua b/resources/customized_payloads/Ka-50_3.lua index db5b1a49..943afe2d 100644 --- a/resources/customized_payloads/Ka-50_3.lua +++ b/resources/customized_payloads/Ka-50_3.lua @@ -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"] = { }, diff --git a/resources/customized_payloads/Mi-24P.lua b/resources/customized_payloads/Mi-24P.lua index 5ad7b1b2..1c4708d9 100644 --- a/resources/customized_payloads/Mi-24P.lua +++ b/resources/customized_payloads/Mi-24P.lua @@ -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"] = { }, diff --git a/resources/customized_payloads/Mi-24V.lua b/resources/customized_payloads/Mi-24V.lua index c5cbd5ba..5d030e49 100644 --- a/resources/customized_payloads/Mi-24V.lua +++ b/resources/customized_payloads/Mi-24V.lua @@ -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", } diff --git a/resources/units/aircraft/AH-1W.yaml b/resources/units/aircraft/AH-1W.yaml index 4e71fbad..596c654b 100644 --- a/resources/units/aircraft/AH-1W.yaml +++ b/resources/units/aircraft/AH-1W.yaml @@ -22,4 +22,5 @@ variants: tasks: BAI: 480 CAS: 480 + Escort: 100 OCA/Aircraft: 480 diff --git a/resources/units/aircraft/AH-64A.yaml b/resources/units/aircraft/AH-64A.yaml index 73cbbfac..b10a1089 100644 --- a/resources/units/aircraft/AH-64A.yaml +++ b/resources/units/aircraft/AH-64A.yaml @@ -23,4 +23,5 @@ variants: tasks: BAI: 490 CAS: 490 + Escort: 80 OCA/Aircraft: 490 diff --git a/resources/units/aircraft/AH-64D.yaml b/resources/units/aircraft/AH-64D.yaml index 9a69a98e..6696e09b 100644 --- a/resources/units/aircraft/AH-64D.yaml +++ b/resources/units/aircraft/AH-64D.yaml @@ -24,4 +24,5 @@ variants: tasks: BAI: 500 CAS: 500 + Escort: 90 OCA/Aircraft: 500 diff --git a/resources/units/aircraft/AH-64D_BLK_II.yaml b/resources/units/aircraft/AH-64D_BLK_II.yaml index df4f9027..74e7266c 100644 --- a/resources/units/aircraft/AH-64D_BLK_II.yaml +++ b/resources/units/aircraft/AH-64D_BLK_II.yaml @@ -37,4 +37,5 @@ radios: tasks: BAI: 510 CAS: 510 + Escort: 100 OCA/Aircraft: 510 diff --git a/resources/units/aircraft/Ka-50.yaml b/resources/units/aircraft/Ka-50.yaml index 1d5b8bd1..163371c2 100644 --- a/resources/units/aircraft/Ka-50.yaml +++ b/resources/units/aircraft/Ka-50.yaml @@ -27,4 +27,5 @@ kneeboard_units: "metric" tasks: BAI: 430 CAS: 430 + Escort: 90 OCA/Aircraft: 430 diff --git a/resources/units/aircraft/Ka-50_3.yaml b/resources/units/aircraft/Ka-50_3.yaml index be6513fb..2a1752cb 100644 --- a/resources/units/aircraft/Ka-50_3.yaml +++ b/resources/units/aircraft/Ka-50_3.yaml @@ -28,4 +28,5 @@ kneeboard_units: "metric" tasks: BAI: 440 CAS: 440 + Escort: 100 OCA/Aircraft: 440 diff --git a/resources/units/aircraft/Mi-24P.yaml b/resources/units/aircraft/Mi-24P.yaml index 9ed5449e..f60d27c6 100644 --- a/resources/units/aircraft/Mi-24P.yaml +++ b/resources/units/aircraft/Mi-24P.yaml @@ -39,4 +39,5 @@ tasks: Air Assault: 20 BAI: 410 CAS: 410 + Escort: 100 OCA/Aircraft: 410 diff --git a/resources/units/aircraft/Mi-24V.yaml b/resources/units/aircraft/Mi-24V.yaml index 676ba681..2ee52fb0 100644 --- a/resources/units/aircraft/Mi-24V.yaml +++ b/resources/units/aircraft/Mi-24V.yaml @@ -29,4 +29,5 @@ tasks: Air Assault: 10 BAI: 400 CAS: 400 + Escort: 100 OCA/Aircraft: 400 diff --git a/resources/units/aircraft/Mi-28N.yaml b/resources/units/aircraft/Mi-28N.yaml index 3e5a676d..43f5ced0 100644 --- a/resources/units/aircraft/Mi-28N.yaml +++ b/resources/units/aircraft/Mi-28N.yaml @@ -18,4 +18,5 @@ variants: tasks: BAI: 420 CAS: 420 + Escort: 100 OCA/Aircraft: 420 diff --git a/resources/units/aircraft/SA342L.yaml b/resources/units/aircraft/SA342L.yaml index 65227f9a..35f8cad5 100644 --- a/resources/units/aircraft/SA342L.yaml +++ b/resources/units/aircraft/SA342L.yaml @@ -23,4 +23,5 @@ kneeboard_units: "metric" tasks: BAI: 450 CAS: 450 + Escort: 100 OCA/Aircraft: 450 diff --git a/resources/units/aircraft/SA342M.yaml b/resources/units/aircraft/SA342M.yaml index f6f4ecc7..7421180c 100644 --- a/resources/units/aircraft/SA342M.yaml +++ b/resources/units/aircraft/SA342M.yaml @@ -26,4 +26,5 @@ kneeboard_units: "metric" tasks: BAI: 460 CAS: 460 + Escort: 90 OCA/Aircraft: 460