diff --git a/changelog.md b/changelog.md index be1d277d..9656779b 100644 --- a/changelog.md +++ b/changelog.md @@ -14,13 +14,15 @@ Saves from 5.x are not compatible with 6.0. * **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan. * **[Flight Planning]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress. * **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle. +* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it. +* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights. * **[Modding]** Updated UH-60L mod version support to 1.3.1 * **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly. * **[UI]** Added options to the loadout editor for setting properties such as HMD choice. * **[UI]** Added separate images for the different carrier types. * **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier). * **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it. -* **[Plugins]** Added support for the CTLD script by ciribob and updated the JTAC Autolase +* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script. ## Fixes diff --git a/game/ato/ai_flight_planner_db.py b/game/ato/ai_flight_planner_db.py index 935abb2a..dac10283 100644 --- a/game/ato/ai_flight_planner_db.py +++ b/game/ato/ai_flight_planner_db.py @@ -483,6 +483,20 @@ TRANSPORT_CAPABLE = [ Mi_26, ] +AIR_ASSAULT_CAPABLE = [ + CH_53E, + CH_47D, + UH_60L, + SH_60B, + UH_60A, + UH_1H, + Mi_8MT, + Mi_26, + Mi_24P, + Mi_24V, + Hercules, +] + DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I] AEWC_CAPABLE = [ @@ -538,6 +552,8 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]: return REFUELING_CAPABALE elif task == FlightType.TRANSPORT: return TRANSPORT_CAPABLE + elif task == FlightType.AIR_ASSAULT: + return AIR_ASSAULT_CAPABLE else: logging.error(f"Unplannable flight type: {task}") return [] diff --git a/game/ato/flight.py b/game/ato/flight.py index cc46deac..71f77534 100644 --- a/game/ato/flight.py +++ b/game/ato/flight.py @@ -139,6 +139,10 @@ class Flight(SidcDescribable): def unit_type(self) -> AircraftType: return self.squadron.aircraft + @property + def is_helo(self) -> bool: + return self.unit_type.dcs_unit_type.helicopter + @property def from_cp(self) -> ControlPoint: return self.departure diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py new file mode 100644 index 00000000..7b7c9737 --- /dev/null +++ b/game/ato/flightplans/airassault.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from typing import TYPE_CHECKING, Iterator, Type +from game.ato.flightplans.airlift import AirliftLayout +from game.ato.flightplans.standard import StandardFlightPlan +from game.theater.controlpoint import ControlPointType +from game.theater.missiontarget import MissionTarget +from game.utils import Distance, feet, meters +from .ibuilder import IBuilder +from .waypointbuilder import WaypointBuilder + +if TYPE_CHECKING: + from ..flight import Flight + from ..flightwaypoint import FlightWaypoint + + +class Builder(IBuilder): + def build(self) -> AirAssaultLayout: + + altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude + altitude_is_agl = self.flight.is_helo + + builder = WaypointBuilder(self.flight, self.coalition) + + if not self.flight.is_helo or self.flight.departure.cptype in [ + ControlPointType.AIRCRAFT_CARRIER_GROUP, + ControlPointType.LHA_GROUP, + ControlPointType.OFF_MAP, + ]: + # Non-Helo flights or Off_Map will be preloaded + # Carrier operations load the logistics directly from the carrier + pickup = None + pickup_position = self.flight.departure.position + else: + # Create a special pickup zone for Helos from Airbase / FOB + pickup = builder.pickup( + MissionTarget( + "Pickup Zone", + self.flight.departure.position.random_point_within(1200, 600), + ) + ) + pickup_position = pickup.position + assault_area = builder.assault_area(self.package.target) + heading = self.package.target.position.heading_between_point(pickup_position) + drop_off_zone = MissionTarget( + "Dropoff zone", + self.package.target.position.point_from_heading(heading, 1200), + ) + + return AirAssaultLayout( + departure=builder.takeoff(self.flight.departure), + nav_to_pickup=builder.nav_path( + self.flight.departure.position, + pickup_position, + altitude, + altitude_is_agl, + ), + pickup=pickup, + nav_to_drop_off=builder.nav_path( + pickup_position, + drop_off_zone.position, + altitude, + altitude_is_agl, + ), + drop_off=builder.drop_off(drop_off_zone), + stopover=None, + target=assault_area, + nav_to_home=builder.nav_path( + drop_off_zone.position, + self.flight.arrival.position, + altitude, + altitude_is_agl, + ), + arrival=builder.land(self.flight.arrival), + divert=builder.divert(self.flight.divert), + bullseye=builder.bullseye(), + ) + + +@dataclass(frozen=True) +class AirAssaultLayout(AirliftLayout): + target: FlightWaypoint + + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.departure + yield from self.nav_to_pickup + if self.pickup: + yield self.pickup + yield from self.nav_to_drop_off + yield self.drop_off + yield self.target + yield from self.nav_to_home + yield self.arrival + if self.divert is not None: + yield self.divert + yield self.bullseye + + +class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]): + def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None: + super().__init__(flight, layout) + + @staticmethod + def builder_type() -> Type[Builder]: + return Builder + + @property + def tot_waypoint(self) -> FlightWaypoint | None: + return self.layout.drop_off + + def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: + if waypoint == self.tot_waypoint: + return self.tot + return None + + def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None: + return None + + @property + def engagement_distance(self) -> Distance: + # The radius of the WaypointZone created at the target location + return meters(2500) + + @property + def mission_departure_time(self) -> timedelta: + return self.package.time_over_target diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index f097d550..a0513da0 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -4,6 +4,7 @@ from collections.abc import Iterator from dataclasses import dataclass from datetime import timedelta from typing import TYPE_CHECKING, Type +from game.theater.missiontarget import MissionTarget from game.utils import feet from .ibuilder import IBuilder @@ -30,15 +31,48 @@ class Builder(IBuilder): builder = WaypointBuilder(self.flight, self.coalition) pickup = None - nav_to_pickup = [] - if cargo.origin != self.flight.departure: - pickup = builder.pickup(cargo.origin) - nav_to_pickup = builder.nav_path( - self.flight.departure.position, - cargo.origin.position, - altitude, - altitude_is_agl, + stopover = None + if self.flight.is_helo: + # Create a pickupzone where the cargo will be spawned + pickup_zone = MissionTarget( + "Pickup Zone", cargo.origin.position.random_point_within(1000, 200) ) + pickup = builder.pickup(pickup_zone) + # If The cargo is at the departure controlpoint, the pickup waypoint should + # only be created for client flights + pickup.only_for_player = cargo.origin == self.flight.departure + + # Create a dropoff zone where the cargo should be dropped + drop_off_zone = MissionTarget( + "Dropoff zone", + cargo.next_stop.position.random_point_within(1000, 200), + ) + drop_off = builder.drop_off(drop_off_zone) + + # Add an additional stopover point so that the flight can refuel + stopover = builder.stopover(cargo.next_stop) + else: + # Fixed Wing will get stopover points for pickup and dropoff + if cargo.origin != self.flight.departure: + pickup = builder.stopover(cargo.origin, "PICKUP") + drop_off = builder.stopover(cargo.next_stop, "DROP OFF") + + nav_to_pickup = builder.nav_path( + self.flight.departure.position, + cargo.origin.position, + altitude, + altitude_is_agl, + ) + + if self.flight.client_count > 0: + # Normal Landing Waypoint + arrival = builder.land(self.flight.arrival) + else: + # The AI Needs another Stopover point to actually fly back to the original + # base. Otherwise the Cargo drop will be the new Landing Waypoint and the + # AI will end its mission there instead of flying back. + # https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/ + arrival = builder.stopover(self.flight.arrival, "LANDING") return AirliftLayout( departure=builder.takeoff(self.flight.departure), @@ -50,14 +84,15 @@ class Builder(IBuilder): altitude, altitude_is_agl, ), - drop_off=builder.drop_off(cargo.next_stop), + drop_off=drop_off, + stopover=stopover, nav_to_home=builder.nav_path( cargo.origin.position, self.flight.arrival.position, altitude, altitude_is_agl, ), - arrival=builder.land(self.flight.arrival), + arrival=arrival, divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), ) @@ -69,15 +104,18 @@ class AirliftLayout(StandardLayout): pickup: FlightWaypoint | None nav_to_drop_off: list[FlightWaypoint] drop_off: FlightWaypoint + stopover: FlightWaypoint | None nav_to_home: list[FlightWaypoint] def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to_pickup - if self.pickup: + if self.pickup is not None: yield self.pickup yield from self.nav_to_drop_off yield self.drop_off + if self.stopover is not None: + yield self.stopover yield from self.nav_to_home yield self.arrival if self.divert is not None: diff --git a/game/ato/flightplans/flightplanbuilder.py b/game/ato/flightplans/flightplanbuilder.py index 5e93c928..c8028239 100644 --- a/game/ato/flightplans/flightplanbuilder.py +++ b/game/ato/flightplans/flightplanbuilder.py @@ -8,6 +8,7 @@ from game.data.doctrine import Doctrine from game.flightplan import IpZoneGeometry, JoinZoneGeometry from game.flightplan.refuelzonegeometry import RefuelZoneGeometry from .aewc import AewcFlightPlan +from .airassault import AirAssaultFlightPlan from .airlift import AirliftFlightPlan from .antiship import AntiShipFlightPlan from .bai import BaiFlightPlan @@ -108,6 +109,7 @@ class FlightPlanBuilder: FlightType.AEWC: AewcFlightPlan, FlightType.TRANSPORT: AirliftFlightPlan, FlightType.FERRY: FerryFlightPlan, + FlightType.AIR_ASSAULT: AirAssaultFlightPlan, } return plan_dict.get(task) diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py index a0280f00..232d48a4 100644 --- a/game/ato/flightplans/waypointbuilder.py +++ b/game/ato/flightplans/waypointbuilder.py @@ -54,7 +54,7 @@ class WaypointBuilder: @property def is_helo(self) -> bool: - return self.flight.unit_type.dcs_unit_type.helicopter + return self.flight.is_helo def takeoff(self, departure: ControlPoint) -> FlightWaypoint: """Create takeoff waypoint for the given arrival airfield or carrier. @@ -303,6 +303,9 @@ class WaypointBuilder: def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint: return self._target_area(f"ATTACK {target.name}", target, flyover=True) + def assault_area(self, target: MissionTarget) -> FlightWaypoint: + return self._target_area(f"ASSAULT {target.name}", target) + @staticmethod def _target_area( name: str, @@ -491,36 +494,57 @@ class WaypointBuilder: ) @staticmethod - def pickup(control_point: ControlPoint) -> FlightWaypoint: - """Creates a cargo pickup waypoint. + def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint: + """Creates a stopover waypoint. Args: control_point: Pick up location. """ return FlightWaypoint( - "PICKUP", - FlightWaypointType.PICKUP, - control_point.position, + name, + FlightWaypointType.STOPOVER, + stopover.position, meters(0), "RADIO", - description=f"Pick up cargo from {control_point}", - pretty_name="Pick up location", + description=f"Stopover at {stopover}", + pretty_name="Stopover location", + control_point=stopover, ) @staticmethod - def drop_off(control_point: ControlPoint) -> FlightWaypoint: + def pickup(pick_up: MissionTarget) -> FlightWaypoint: + """Creates a cargo pickup waypoint. + + Args: + control_point: Pick up location. + """ + control_point = pick_up if isinstance(pick_up, ControlPoint) else None + return FlightWaypoint( + "PICKUP", + FlightWaypointType.PICKUP, + pick_up.position, + meters(0), + "RADIO", + description=f"Pick up cargo from {pick_up.name}", + pretty_name="Pick up location", + control_point=control_point, + ) + + @staticmethod + def drop_off(drop_off: MissionTarget) -> FlightWaypoint: """Creates a cargo drop-off waypoint. Args: control_point: Drop-off location. """ + control_point = drop_off if isinstance(drop_off, ControlPoint) else None return FlightWaypoint( "DROP OFF", - FlightWaypointType.PICKUP, - control_point.position, + FlightWaypointType.DROP_OFF, + drop_off.position, meters(0), "RADIO", - description=f"Drop off cargo at {control_point}", + description=f"Drop off cargo at {drop_off.name}", pretty_name="Drop off location", control_point=control_point, ) diff --git a/game/ato/flighttype.py b/game/ato/flighttype.py index 87c080d7..9660f3f2 100644 --- a/game/ato/flighttype.py +++ b/game/ato/flighttype.py @@ -56,6 +56,7 @@ class FlightType(Enum): SEAD_ESCORT = "SEAD Escort" REFUELING = "Refueling" FERRY = "Ferry" + AIR_ASSAULT = "Air Assault" def __str__(self) -> str: return self.value @@ -89,6 +90,7 @@ class FlightType(Enum): FlightType.OCA_RUNWAY, FlightType.OCA_AIRCRAFT, FlightType.SEAD_ESCORT, + FlightType.AIR_ASSAULT, } @property @@ -112,4 +114,5 @@ class FlightType(Enum): FlightType.SWEEP: AirEntity.FIGHTER, FlightType.TARCAP: AirEntity.FIGHTER, FlightType.TRANSPORT: AirEntity.UTILITY, + FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING, }.get(self, AirEntity.UNSPECIFIED) diff --git a/game/ato/flightwaypointtype.py b/game/ato/flightwaypointtype.py index 03199389..002c2d0c 100644 --- a/game/ato/flightwaypointtype.py +++ b/game/ato/flightwaypointtype.py @@ -47,3 +47,4 @@ class FlightWaypointType(IntEnum): DROP_OFF = 27 BULLSEYE = 28 REFUEL = 29 # Should look for nearby tanker to refuel from. + STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type diff --git a/game/ato/package.py b/game/ato/package.py index 9ae6a740..b36cd37b 100644 --- a/game/ato/package.py +++ b/game/ato/package.py @@ -161,6 +161,7 @@ class Package: FlightType.BAI, FlightType.DEAD, FlightType.TRANSPORT, + FlightType.AIR_ASSAULT, FlightType.SEAD, FlightType.TARCAP, FlightType.BARCAP, diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index f7013073..4ca1c61b 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -139,6 +139,14 @@ class ObjectiveFinder: """Iterates over all active front lines in the theater.""" yield from self.game.theater.conflicts() + def air_assault_targets(self) -> Iterator[ControlPoint]: + """Iterates over all capturable controlpoints for all active front lines""" + if not self.game.settings.plugin_option("ctld"): + # Air Assault should only be tasked with CTLD enabled + return + for front_line in self.front_lines(): + yield front_line.control_point_hostile_to(self.is_player) + def vulnerable_control_points(self) -> Iterator[ControlPoint]: """Iterates over friendly CPs that are vulnerable to enemy CPs. diff --git a/game/commander/tasks/compound/capturebase.py b/game/commander/tasks/compound/capturebase.py index 378cb13e..e8a71846 100644 --- a/game/commander/tasks/compound/capturebase.py +++ b/game/commander/tasks/compound/capturebase.py @@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import ( from game.commander.tasks.compound.reduceenemyfrontlinecapacity import ( ReduceEnemyFrontLineCapacity, ) +from game.commander.tasks.primitive.airassault import PlanAirAssault from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method @@ -18,6 +19,7 @@ class CaptureBase(CompoundTask[TheaterState]): front_line: FrontLine def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: + yield [PlanAirAssault(self.enemy_cp(state))] yield [BreakthroughAttack(self.front_line, state.context.coalition.player)] yield [DestroyEnemyGroundUnits(self.front_line)] if self.worth_destroying_ammo_depots(state): diff --git a/game/commander/tasks/primitive/airassault.py b/game/commander/tasks/primitive/airassault.py new file mode 100644 index 00000000..575536bf --- /dev/null +++ b/game/commander/tasks/primitive/airassault.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from game.commander.tasks.packageplanningtask import PackagePlanningTask +from game.commander.theaterstate import TheaterState +from game.theater import ControlPoint +from game.ato.flighttype import FlightType + + +@dataclass +class PlanAirAssault(PackagePlanningTask[ControlPoint]): + def preconditions_met(self, state: TheaterState) -> bool: + if self.target not in state.air_assault_targets: + return False + if self.capture_blocked(state): + # Do not task if there are enemy garrisons blocking the capture + return False + if not self.target_area_preconditions_met(state): + # Do not task if air defense is present in the target area + return False + return super().preconditions_met(state) + + def capture_blocked(self, state: TheaterState) -> bool: + garrisons = state.enemy_garrisons[self.target] + return len(garrisons.blocking_capture) > 0 + + def apply_effects(self, state: TheaterState) -> None: + state.air_assault_targets.remove(self.target) + + def propose_flights(self) -> None: + self.propose_flight(FlightType.AIR_ASSAULT, 2) + # TODO Validate this.. / is Heli escort possible? + self.propose_flight(FlightType.TARCAP, 2) diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index 07abc39a..765b21b8 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -45,6 +45,7 @@ class TheaterState(WorldState["TheaterState"]): context: PersistentContext barcaps_needed: dict[ControlPoint, int] active_front_lines: list[FrontLine] + air_assault_targets: list[ControlPoint] front_line_stances: dict[FrontLine, Optional[CombatStance]] vulnerable_front_lines: list[FrontLine] aewc_targets: list[MissionTarget] @@ -109,6 +110,7 @@ class TheaterState(WorldState["TheaterState"]): context=self.context, barcaps_needed=dict(self.barcaps_needed), active_front_lines=list(self.active_front_lines), + air_assault_targets=list(self.air_assault_targets), front_line_stances=dict(self.front_line_stances), vulnerable_front_lines=list(self.vulnerable_front_lines), aewc_targets=list(self.aewc_targets), @@ -158,6 +160,7 @@ class TheaterState(WorldState["TheaterState"]): cp: barcap_rounds for cp in finder.vulnerable_control_points() }, active_front_lines=list(finder.front_lines()), + air_assault_targets=list(finder.air_assault_targets()), front_line_stances={f: None for f in finder.front_lines()}, vulnerable_front_lines=list(finder.front_lines()), aewc_targets=[finder.farthest_friendly_control_point()], diff --git a/game/data/units.py b/game/data/units.py index a03b64d7..20b5b21b 100644 --- a/game/data/units.py +++ b/game/data/units.py @@ -18,6 +18,7 @@ class UnitClass(Enum): EARLY_WARNING_RADAR = "EarlyWarningRadar" FORTIFICATION = "Fortification" FRIGATE = "Frigate" + HELICOPTER = "Helicopter" HELICOPTER_CARRIER = "HelicopterCarrier" IFV = "IFV" INFANTRY = "Infantry" diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index cf537a20..545ee589 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -47,7 +47,7 @@ from game.utils import ( if TYPE_CHECKING: from game.missiongenerator.aircraft.flightdata import FlightData - from game.missiongenerator.airsupport import AirSupport + from game.missiongenerator.missiondata import MissionData from game.radio.radios import Radio, RadioFrequency, RadioRegistry @@ -182,6 +182,14 @@ class AircraftType(UnitType[Type[FlyingType]]): channel_allocator: Optional[RadioChannelAllocator] channel_namer: Type[ChannelNamer] + # Logisitcs info + # cabin_size defines how many troops can be loaded. 0 means the aircraft can not + # transport any troops. Default for helos is 10, non helos will have 0. + cabin_size: int + # If the aircraft can carry crates can_carry_crates should be set to true which + # will be set to true for helos by default + can_carry_crates: bool + @property def flyable(self) -> bool: return self.dcs_unit_type.flyable @@ -281,10 +289,10 @@ class AircraftType(UnitType[Type[FlyingType]]): return freq def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: if self.channel_allocator is not None: - self.channel_allocator.assign_channels_for_flight(flight, air_support) + self.channel_allocator.assign_channels_for_flight(flight, mission_data) def channel_name(self, radio_id: int, channel_id: int) -> str: return self.channel_namer.channel_name(radio_id, channel_id) @@ -387,6 +395,9 @@ class AircraftType(UnitType[Type[FlyingType]]): if units_data == "metric": units = MetricUnits() + class_name = data.get("class") + unit_class = UnitClass.PLANE if class_name is None else UnitClass(class_name) + prop_overrides = data.get("default_overrides") if prop_overrides is not None: cls._set_props_overrides(prop_overrides, aircraft, data_path) @@ -419,5 +430,7 @@ class AircraftType(UnitType[Type[FlyingType]]): channel_namer=radio_config.channel_namer, kneeboard_units=units, utc_kneeboard=data.get("utc_kneeboard", False), - unit_class=UnitClass.PLANE, + unit_class=unit_class, + cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0), + can_carry_crates=data.get("can_carry_crates", aircraft.helicopter), ) diff --git a/game/missiongenerator/aircraft/aircraftbehavior.py b/game/missiongenerator/aircraft/aircraftbehavior.py index 45cf69b6..89d3eeeb 100644 --- a/game/missiongenerator/aircraft/aircraftbehavior.py +++ b/game/missiongenerator/aircraft/aircraftbehavior.py @@ -62,7 +62,10 @@ class AircraftBehavior: self.configure_runway_attack(group, flight) elif self.task == FlightType.OCA_AIRCRAFT: self.configure_oca_strike(group, flight) - elif self.task == FlightType.TRANSPORT: + elif self.task in [ + FlightType.TRANSPORT, + FlightType.AIR_ASSAULT, + ]: self.configure_transport(group, flight) elif self.task == FlightType.FERRY: self.configure_ferry(group, flight) diff --git a/game/missiongenerator/aircraft/aircraftgenerator.py b/game/missiongenerator/aircraft/aircraftgenerator.py index 6868f981..223bc00e 100644 --- a/game/missiongenerator/aircraft/aircraftgenerator.py +++ b/game/missiongenerator/aircraft/aircraftgenerator.py @@ -17,7 +17,7 @@ from game.ato.flighttype import FlightType from game.ato.package import Package from game.ato.starttype import StartType from game.factions.faction import Faction -from game.missiongenerator.airsupport import AirSupport +from game.missiongenerator.missiondata import MissionData from game.missiongenerator.lasercoderegistry import LaserCodeRegistry from game.radio.radios import RadioRegistry from game.radio.tacan import TacanRegistry @@ -49,7 +49,7 @@ class AircraftGenerator: tacan_registry: TacanRegistry, laser_code_registry: LaserCodeRegistry, unit_map: UnitMap, - air_support: AirSupport, + mission_data: MissionData, helipads: dict[ControlPoint, StaticGroup], ) -> None: self.mission = mission @@ -61,7 +61,7 @@ class AircraftGenerator: self.laser_code_registry = laser_code_registry self.unit_map = unit_map self.flights: List[FlightData] = [] - self.air_support = air_support + self.mission_data = mission_data self.helipads = helipads @cached_property @@ -174,7 +174,7 @@ class AircraftGenerator: self.radio_registry, self.tacan_registy, self.laser_code_registry, - self.air_support, + self.mission_data, dynamic_runways, self.use_client, ).configure() diff --git a/game/missiongenerator/aircraft/flightgroupconfigurator.py b/game/missiongenerator/aircraft/flightgroupconfigurator.py index 3e244c0c..04244282 100644 --- a/game/missiongenerator/aircraft/flightgroupconfigurator.py +++ b/game/missiongenerator/aircraft/flightgroupconfigurator.py @@ -12,8 +12,9 @@ from dcs.unitgroup import FlyingGroup from game.ato import Flight, FlightType from game.callsigns import callsign_for_support_unit from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum -from game.missiongenerator.airsupport import AirSupport, AwacsInfo, TankerInfo +from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo from game.missiongenerator.lasercoderegistry import LaserCodeRegistry +from game.missiongenerator.logisticsgenerator import LogisticsGenerator from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage from game.runways import RunwayData @@ -40,7 +41,7 @@ class FlightGroupConfigurator: radio_registry: RadioRegistry, tacan_registry: TacanRegistry, laser_code_registry: LaserCodeRegistry, - air_support: AirSupport, + mission_data: MissionData, dynamic_runways: dict[str, RunwayData], use_client: bool, ) -> None: @@ -52,7 +53,7 @@ class FlightGroupConfigurator: self.radio_registry = radio_registry self.tacan_registry = tacan_registry self.laser_code_registry = laser_code_registry - self.air_support = air_support + self.mission_data = mission_data self.dynamic_runways = dynamic_runways self.use_client = use_client @@ -74,6 +75,20 @@ class FlightGroupConfigurator: self.game.theater, self.game.conditions, self.dynamic_runways ) + if self.flight.flight_type in [ + FlightType.TRANSPORT, + FlightType.AIR_ASSAULT, + ] and self.game.settings.plugin_option("ctld"): + transfer = None + if self.flight.flight_type == FlightType.TRANSPORT: + coalition = self.game.coalition_for(player=self.flight.blue) + transfer = coalition.transfers.transfer_for_flight(self.flight) + self.mission_data.logistics.append( + LogisticsGenerator( + self.flight, self.group, self.mission, self.game.settings, transfer + ).generate_logistics() + ) + mission_start_time, waypoints = WaypointGenerator( self.flight, self.group, @@ -81,7 +96,7 @@ class FlightGroupConfigurator: self.game.conditions.start_time, self.time, self.game.settings, - self.air_support, + self.mission_data, ).create_waypoints() return FlightData( @@ -130,7 +145,7 @@ class FlightGroupConfigurator: def register_air_support(self, channel: RadioFrequency) -> None: callsign = callsign_for_support_unit(self.group) if isinstance(self.flight.flight_plan, AewcFlightPlan): - self.air_support.awacs.append( + self.mission_data.awacs.append( AwacsInfo( group_name=str(self.group.name), callsign=callsign, @@ -143,7 +158,7 @@ class FlightGroupConfigurator: ) elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan): tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir) - self.air_support.tankers.append( + self.mission_data.tankers.append( TankerInfo( group_name=str(self.group.name), callsign=callsign, diff --git a/game/missiongenerator/aircraft/waypoints/cargostop.py b/game/missiongenerator/aircraft/waypoints/cargostop.py index f8aa06ef..ea0302b7 100644 --- a/game/missiongenerator/aircraft/waypoints/cargostop.py +++ b/game/missiongenerator/aircraft/waypoints/cargostop.py @@ -1,4 +1,9 @@ -from dcs.point import MovingPoint, PointAction +from dcs.point import MovingPoint +from dcs.task import Land + +from game.utils import feet +from dcs.point import PointAction + from .pydcswaypointbuilder import PydcsWaypointBuilder @@ -6,9 +11,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder class CargoStopBuilder(PydcsWaypointBuilder): def build(self) -> MovingPoint: waypoint = super().build() - waypoint.type = "LandingReFuAr" - waypoint.action = PointAction.LandingReFuAr - waypoint.landing_refuel_rearm_time = 2 # Minutes. - if (control_point := self.waypoint.control_point) is not None: - waypoint.airdrome_id = control_point.airdrome_id_for_landing + # Create a landing task, currently only for Helos! + if self.flight.is_helo: + # Calculate a landing point with a small buffer to prevent AI from landing + # directly at the static ammo depot and exploding + 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)) + else: + # Fixed wing will drop the cargo at the waypoint so we set a lower altitude + waypoint.alt = int(feet(10000).meters) + waypoint.alt_type = "BARO" + waypoint.action = PointAction.FlyOverPoint return waypoint diff --git a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py index 3df2ffab..8cc0ed76 100644 --- a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py +++ b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py @@ -10,7 +10,7 @@ from dcs.unitgroup import FlyingGroup from game.ato import Flight, FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType -from game.missiongenerator.airsupport import AirSupport +from game.missiongenerator.missiondata import MissionData from game.theater import MissionTarget, TheaterUnit TARGET_WAYPOINTS = ( @@ -28,7 +28,7 @@ class PydcsWaypointBuilder: flight: Flight, mission: Mission, elapsed_mission_time: timedelta, - air_support: AirSupport, + mission_data: MissionData, ) -> None: self.waypoint = waypoint self.group = group @@ -36,7 +36,7 @@ class PydcsWaypointBuilder: self.flight = flight self.mission = mission self.elapsed_mission_time = elapsed_mission_time - self.air_support = air_support + self.mission_data = mission_data def build(self) -> MovingPoint: waypoint = self.group.add_waypoint( diff --git a/game/missiongenerator/aircraft/waypoints/racetrack.py b/game/missiongenerator/aircraft/waypoints/racetrack.py index 6afc05e0..2013e0ec 100644 --- a/game/missiongenerator/aircraft/waypoints/racetrack.py +++ b/game/missiongenerator/aircraft/waypoints/racetrack.py @@ -64,7 +64,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder): waypoint.add_task(Tanker()) if self.flight.unit_type.dcs_unit_type.tacan: - tanker_info = self.air_support.tankers[-1] + tanker_info = self.mission_data.tankers[-1] tacan = tanker_info.tacan tacan_callsign = { "Texaco": "TEX", diff --git a/game/missiongenerator/aircraft/waypoints/stopover.py b/game/missiongenerator/aircraft/waypoints/stopover.py new file mode 100644 index 00000000..9c292881 --- /dev/null +++ b/game/missiongenerator/aircraft/waypoints/stopover.py @@ -0,0 +1,15 @@ +from dcs.point import MovingPoint, PointAction +from dcs.task import Land + +from .pydcswaypointbuilder import PydcsWaypointBuilder + + +class StopoverBuilder(PydcsWaypointBuilder): + def build(self) -> MovingPoint: + waypoint = super().build() + waypoint.type = "LandingReFuAr" + waypoint.action = PointAction.LandingReFuAr + waypoint.landing_refuel_rearm_time = 2 # Minutes. + if (control_point := self.waypoint.control_point) is not None: + waypoint.airdrome_id = control_point.airdrome_id_for_landing + return waypoint diff --git a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py index 03c34e75..ffeecf7f 100644 --- a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py +++ b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py @@ -16,7 +16,8 @@ from game.ato import Flight, FlightWaypoint from game.ato.flightstate import InFlight, WaitingForStart from game.ato.flightwaypointtype import FlightWaypointType from game.ato.starttype import StartType -from game.missiongenerator.airsupport import AirSupport +from game.missiongenerator.aircraft.waypoints.stopover import StopoverBuilder +from game.missiongenerator.missiondata import MissionData from game.settings import Settings from game.utils import pairwise from .baiingress import BaiIngressBuilder @@ -48,7 +49,7 @@ class WaypointGenerator: turn_start_time: datetime, time: datetime, settings: Settings, - air_support: AirSupport, + mission_data: MissionData, ) -> None: self.flight = flight self.group = group @@ -56,7 +57,7 @@ class WaypointGenerator: self.elapsed_mission_time = time - turn_start_time self.time = time self.settings = settings - self.air_support = air_support + self.mission_data = mission_data def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]: for waypoint in self.flight.points: @@ -134,6 +135,7 @@ class WaypointGenerator: FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PICKUP: CargoStopBuilder, FlightWaypointType.REFUEL: RefuelPointBuilder, + FlightWaypointType.STOPOVER: StopoverBuilder, } builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) return builder( @@ -142,7 +144,7 @@ class WaypointGenerator: self.flight, self.mission, self.elapsed_mission_time, - self.air_support, + self.mission_data, ) def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None: diff --git a/game/missiongenerator/airsupport.py b/game/missiongenerator/airsupport.py deleted file mode 100644 index e2c61fa0..00000000 --- a/game/missiongenerator/airsupport.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import timedelta -from typing import Optional, TYPE_CHECKING - -if TYPE_CHECKING: - from game.radio.radios import RadioFrequency - from game.radio.tacan import TacanChannel - - -@dataclass -class AwacsInfo: - """AWACS information for the kneeboard.""" - - group_name: str - callsign: str - freq: RadioFrequency - depature_location: Optional[str] - start_time: Optional[timedelta] - end_time: Optional[timedelta] - blue: bool - - -@dataclass -class TankerInfo: - """Tanker information for the kneeboard.""" - - group_name: str - callsign: str - variant: str - freq: RadioFrequency - tacan: TacanChannel - start_time: Optional[timedelta] - end_time: Optional[timedelta] - blue: bool - - -@dataclass(frozen=True) -class JtacInfo: - """JTAC information.""" - - group_name: str - unit_name: str - callsign: str - region: str - code: str - blue: bool - freq: RadioFrequency - - -@dataclass -class AirSupport: - awacs: list[AwacsInfo] = field(default_factory=list) - tankers: list[TankerInfo] = field(default_factory=list) - jtacs: list[JtacInfo] = field(default_factory=list) diff --git a/game/missiongenerator/airsupportgenerator.py b/game/missiongenerator/airsupportgenerator.py index b809dd76..a605ff55 100644 --- a/game/missiongenerator/airsupportgenerator.py +++ b/game/missiongenerator/airsupportgenerator.py @@ -21,7 +21,7 @@ from game.radio.radios import RadioRegistry from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage from game.utils import Heading from game.ato.ai_flight_planner_db import AEWC_CAPABLE -from .airsupport import AirSupport, AwacsInfo, TankerInfo +from .missiondata import MissionData, AwacsInfo, TankerInfo from .frontlineconflictdescription import FrontLineConflictDescription if TYPE_CHECKING: @@ -43,14 +43,14 @@ class AirSupportGenerator: game: Game, radio_registry: RadioRegistry, tacan_registry: TacanRegistry, - air_support: AirSupport, + mission_data: MissionData, ) -> None: self.mission = mission self.conflict = conflict self.game = game self.radio_registry = radio_registry self.tacan_registry = tacan_registry - self.air_support = air_support + self.mission_data = mission_data @classmethod def support_tasks(cls) -> List[Type[MainTask]]: @@ -148,13 +148,13 @@ class AirSupportGenerator: tanker_group.points[0].tasks.append(SetInvisibleCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.tankers.append( + self.mission_data.tankers.append( TankerInfo( - str(tanker_group.name), - callsign, - tanker_unit_type.name, - freq, - tacan, + group_name=str(tanker_group.name), + callsign=callsign, + variant=tanker_unit_type.name, + freq=freq, + tacan=tacan, start_time=None, end_time=None, blue=True, @@ -197,7 +197,7 @@ class AirSupportGenerator: awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.awacs.append( + self.mission_data.awacs.append( AwacsInfo( group_name=str(awacs_flight.name), callsign=callsign_for_support_unit(awacs_flight), diff --git a/game/missiongenerator/flotgenerator.py b/game/missiongenerator/flotgenerator.py index 080950bf..07753dd1 100644 --- a/game/missiongenerator/flotgenerator.py +++ b/game/missiongenerator/flotgenerator.py @@ -43,7 +43,7 @@ from game.radio.radios import RadioRegistry from game.theater.controlpoint import ControlPoint from game.unitmap import UnitMap from game.utils import Heading -from .airsupport import AirSupport, JtacInfo +from .missiondata import MissionData, JtacInfo from .frontlineconflictdescription import FrontLineConflictDescription from .lasercoderegistry import LaserCodeRegistry @@ -80,7 +80,7 @@ class FlotGenerator: enemy_stance: CombatStance, unit_map: UnitMap, radio_registry: RadioRegistry, - air_support: AirSupport, + mission_data: MissionData, laser_code_registry: LaserCodeRegistry, ) -> None: self.mission = mission @@ -92,7 +92,7 @@ class FlotGenerator: self.game = game self.unit_map = unit_map self.radio_registry = radio_registry - self.air_support = air_support + self.mission_data = mission_data self.laser_code_registry = laser_code_registry def generate(self) -> None: @@ -166,7 +166,7 @@ class FlotGenerator: ) jtac.points[0].tasks.append( FAC( - callsign=len(self.air_support.jtacs) + 1, + callsign=len(self.mission_data.jtacs) + 1, frequency=int(freq.mhz), modulation=freq.modulation, ) @@ -181,13 +181,13 @@ class FlotGenerator: ) # Note: Will need to change if we ever add ground based JTAC. callsign = callsign_for_support_unit(jtac) - self.air_support.jtacs.append( + self.mission_data.jtacs.append( JtacInfo( - jtac.name, - jtac.name, - callsign, - frontline, - str(code), + group_name=jtac.name, + unit_name=jtac.units[0].name, + callsign=callsign, + region=frontline, + code=str(code), blue=True, freq=freq, ) diff --git a/game/missiongenerator/logisticsgenerator.py b/game/missiongenerator/logisticsgenerator.py new file mode 100644 index 00000000..3ee7c12f --- /dev/null +++ b/game/missiongenerator/logisticsgenerator.py @@ -0,0 +1,104 @@ +from typing import Any, Optional +from dcs import Mission +from dcs.unitgroup import FlyingGroup +from dcs.statics import Fortification +from game.ato import Flight +from game.ato.flightplans.airassault import AirAssaultFlightPlan +from game.ato.flightwaypointtype import FlightWaypointType +from game.missiongenerator.missiondata import CargoInfo, LogisticsInfo +from game.settings.settings import Settings +from game.transfers import TransferOrder + + +ZONE_RADIUS = 300 +CRATE_ZONE_RADIUS = 50 + + +class LogisticsGenerator: + def __init__( + self, + flight: Flight, + group: FlyingGroup[Any], + mission: Mission, + settings: Settings, + transfer: Optional[TransferOrder] = None, + ) -> None: + self.flight = flight + self.group = group + self.transfer = transfer + self.mission = mission + self.settings = settings + + def generate_logistics(self) -> LogisticsInfo: + # Add Logisitcs info for the flight + logistics_info = LogisticsInfo( + pilot_names=[u.name for u in self.group.units], + transport=self.flight.squadron.aircraft, + blue=self.flight.blue, + preload=self.flight.state.in_flight, + ) + + if isinstance(self.flight.flight_plan, AirAssaultFlightPlan): + # 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" + self.mission.triggers.add_triggerzone( + self.flight.flight_plan.layout.target.position, + self.flight.flight_plan.engagement_distance.meters, + False, + target_zone, + ) + logistics_info.target_zone = target_zone + + pickup_point = None + for waypoint in self.flight.points: + if ( + waypoint.waypoint_type + not in [ + FlightWaypointType.PICKUP, + FlightWaypointType.DROP_OFF, + ] + or waypoint.only_for_player + and not self.flight.client_count + ): + continue + # Create Pickup and DropOff zone + zone_name = f"{self.group.name}{waypoint.waypoint_type.name}" + self.mission.triggers.add_triggerzone( + waypoint.position, ZONE_RADIUS, False, zone_name + ) + if waypoint.waypoint_type == FlightWaypointType.PICKUP: + pickup_point = waypoint.position + logistics_info.pickup_zone = zone_name + else: + logistics_info.drop_off_zone = zone_name + + if self.transfer and self.flight.client_count > 0 and pickup_point is not None: + # Add spawnable crates for client airlifts + crate_location = pickup_point.random_point_within( + ZONE_RADIUS - CRATE_ZONE_RADIUS, CRATE_ZONE_RADIUS + ) + crate_zone = f"{self.group.name}crate_spawn" + self.mission.triggers.add_triggerzone( + crate_location, CRATE_ZONE_RADIUS, False, crate_zone + ) + logistics_info.cargo = [ + CargoInfo(cargo_unit_type.dcs_id, crate_zone, amount) + for cargo_unit_type, amount in self.transfer.units.items() + ] + + if pickup_point is not None and self.settings.plugin_option( + "ctld.logisticunit" + ): + # Spawn logisticsunit at pickup zones + country = self.mission.country(self.flight.country) + logistic_unit = self.mission.static_group( + country, + f"{self.group.name}logistic", + Fortification.FARP_Ammo_Dump_Coating, + pickup_point, + ) + logistics_info.logistic_unit = logistic_unit.units[0].name + + return logistics_info diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index 82c3287f..e29350dd 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -12,13 +12,13 @@ from dcs.translation import String from dcs.triggers import TriggerStart from game.ato import FlightType +from game.dcs.aircrafttype import AircraftType from game.plugins import LuaPluginManager from game.theater import TheaterGroundObject from game.theater.iadsnetwork.iadsrole import IadsRole from game.utils import escape_string_for_lua -from .aircraft.flightdata import FlightData -from .airsupport import AirSupport +from .missiondata import MissionData if TYPE_CHECKING: from game import Game @@ -29,13 +29,11 @@ class LuaGenerator: self, game: Game, mission: Mission, - air_support: AirSupport, - flights: list[FlightData], + mission_data: MissionData, ) -> None: self.game = game self.mission = mission - self.air_support = air_support - self.flights = flights + self.mission_data = mission_data self.plugin_scripts: list[str] = [] def generate(self) -> None: @@ -49,10 +47,20 @@ class LuaGenerator: install_path.set_value(os.path.abspath(".")) lua_data.add_item("Airbases") - lua_data.add_item("Carriers") + carriers_object = lua_data.add_item("Carriers") + + for carrier in self.mission_data.carriers: + carrier_item = carriers_object.add_item() + carrier_item.add_key_value("dcsGroupName", carrier.group_name) + carrier_item.add_key_value("unit_name", carrier.unit_name) + carrier_item.add_key_value("callsign", carrier.callsign) + carrier_item.add_key_value("radio", str(carrier.freq.mhz)) + carrier_item.add_key_value( + "tacan", str(carrier.tacan.number) + carrier.tacan.band.name + ) tankers_object = lua_data.add_item("Tankers") - for tanker in self.air_support.tankers: + for tanker in self.mission_data.tankers: tanker_item = tankers_object.add_item() tanker_item.add_key_value("dcsGroupName", tanker.group_name) tanker_item.add_key_value("callsign", tanker.callsign) @@ -63,14 +71,14 @@ class LuaGenerator: ) awacs_object = lua_data.add_item("AWACs") - for awacs in self.air_support.awacs: + for awacs in self.mission_data.awacs: awacs_item = awacs_object.add_item() awacs_item.add_key_value("dcsGroupName", awacs.group_name) awacs_item.add_key_value("callsign", awacs.callsign) awacs_item.add_key_value("radio", str(awacs.freq.mhz)) jtacs_object = lua_data.add_item("JTACs") - for jtac in self.air_support.jtacs: + for jtac in self.mission_data.jtacs: jtac_item = jtacs_object.add_item() jtac_item.add_key_value("dcsGroupName", jtac.group_name) jtac_item.add_key_value("callsign", jtac.callsign) @@ -81,16 +89,55 @@ class LuaGenerator: jtac_item.add_key_value("modulation", jtac.freq.modulation.name) logistics_object = lua_data.add_item("Logistics") - for logistic_info in self.air_support.logistics.values(): - logistics_item = logistics_object.add_item() + logistics_flights = logistics_object.add_item("flights") + crates_object = logistics_object.add_item("crates") + spawnable_crates: dict[str, str] = {} + transports: list[AircraftType] = [] + for logistic_info in self.mission_data.logistics: + if logistic_info.transport not in transports: + transports.append(logistic_info.transport) + coalition_color = "blue" if logistic_info.blue else "red" + logistics_item = logistics_flights.add_item() logistics_item.add_data_array("pilot_names", logistic_info.pilot_names) logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone) logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone) logistics_item.add_key_value("target_zone", logistic_info.target_zone) logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1)) + logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit) + logistics_item.add_key_value( + "aircraft_type", logistic_info.transport.dcs_id + ) + logistics_item.add_key_value( + "preload", "true" if logistic_info.preload else "false" + ) + for cargo in logistic_info.cargo: + if cargo.unit_type not in spawnable_crates: + spawnable_crates[cargo.unit_type] = str(200 + len(spawnable_crates)) + crate_weight = spawnable_crates[cargo.unit_type] + for i in range(cargo.amount): + cargo_item = crates_object.add_item() + cargo_item.add_key_value("weight", crate_weight) + cargo_item.add_key_value("coalition", coalition_color) + cargo_item.add_key_value("zone", cargo.spawn_zone) + transport_object = logistics_object.add_item("transports") + for transport in transports: + transport_item = transport_object.add_item() + transport_item.add_key_value("aircraft_type", transport.dcs_id) + transport_item.add_key_value("cabin_size", str(transport.cabin_size)) + transport_item.add_key_value( + "troops", "true" if transport.cabin_size > 0 else "false" + ) + transport_item.add_key_value( + "crates", "true" if transport.can_carry_crates else "false" + ) + spawnable_crates_object = logistics_object.add_item("spawnable_crates") + for unit, weight in spawnable_crates.items(): + crate_item = spawnable_crates_object.add_item() + crate_item.add_key_value("unit", unit) + crate_item.add_key_value("weight", weight) target_points = lua_data.add_item("TargetPoints") - for flight in self.flights: + for flight in self.mission_data.flights: if flight.friendly and flight.flight_type in [ FlightType.ANTISHIP, FlightType.DEAD, @@ -225,6 +272,9 @@ class LuaItem(ABC): def set_value(self, value: str) -> None: self.value = LuaValue(None, value) + def set_data_array(self, values: list[str]) -> None: + self.value = LuaValue(None, values) + def add_data_array(self, key: str, values: list[str]) -> None: self._add_value(LuaValue(key, values)) diff --git a/game/missiongenerator/missiondata.py b/game/missiongenerator/missiondata.py new file mode 100644 index 00000000..935f962b --- /dev/null +++ b/game/missiongenerator/missiondata.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Optional, TYPE_CHECKING +from game.dcs.aircrafttype import AircraftType +from game.missiongenerator.aircraft.flightdata import FlightData + +from game.runways import RunwayData + +if TYPE_CHECKING: + from game.radio.radios import RadioFrequency + from game.radio.tacan import TacanChannel + + +@dataclass +class GroupInfo: + group_name: str + callsign: str + freq: RadioFrequency + blue: bool + + +@dataclass +class UnitInfo(GroupInfo): + unit_name: str + + +@dataclass +class AwacsInfo(GroupInfo): + """AWACS information for the kneeboard.""" + + depature_location: Optional[str] + start_time: Optional[timedelta] + end_time: Optional[timedelta] + + +@dataclass +class TankerInfo(GroupInfo): + """Tanker information for the kneeboard.""" + + variant: str + tacan: TacanChannel + start_time: Optional[timedelta] + end_time: Optional[timedelta] + + +@dataclass +class CarrierInfo(UnitInfo): + """Carrier information.""" + + tacan: TacanChannel + + +@dataclass +class JtacInfo(UnitInfo): + """JTAC information.""" + + region: str + code: str + + +@dataclass +class CargoInfo: + """Cargo information.""" + + unit_type: str = field(default_factory=str) + spawn_zone: str = field(default_factory=str) + amount: int = field(default=1) + + +@dataclass +class LogisticsInfo: + """Logistics information.""" + + pilot_names: list[str] + transport: AircraftType + blue: bool + + logistic_unit: str = field(default_factory=str) + pickup_zone: str = field(default_factory=str) + drop_off_zone: str = field(default_factory=str) + target_zone: str = field(default_factory=str) + cargo: list[CargoInfo] = field(default_factory=list) + preload: bool = field(default=False) + + +@dataclass +class MissionData: + awacs: list[AwacsInfo] = field(default_factory=list) + runways: list[RunwayData] = field(default_factory=list) + carriers: list[CarrierInfo] = field(default_factory=list) + flights: list[FlightData] = field(default_factory=list) + tankers: list[TankerInfo] = field(default_factory=list) + jtacs: list[JtacInfo] = field(default_factory=list) + logistics: list[LogisticsInfo] = field(default_factory=list) diff --git a/game/missiongenerator/missiongenerator.py b/game/missiongenerator/missiongenerator.py index c7dd828c..3280c594 100644 --- a/game/missiongenerator/missiongenerator.py +++ b/game/missiongenerator/missiongenerator.py @@ -22,7 +22,7 @@ from game.theater import Airfield, FrontLine from game.theater.bullseye import Bullseye from game.unitmap import UnitMap from .aircraft.flightdata import FlightData -from .airsupport import AirSupport +from .missiondata import MissionData from .airsupportgenerator import AirSupportGenerator from .beacons import load_beacons_for_terrain from .briefinggenerator import BriefingGenerator, MissionInfoGenerator @@ -61,7 +61,7 @@ class MissionGenerator: self.mission = Mission(game.theater.terrain) self.unit_map = UnitMap() - self.air_support = AirSupport() + self.mission_data = MissionData() self.laser_code_registry = LaserCodeRegistry() self.radio_registry = RadioRegistry() @@ -92,6 +92,7 @@ class MissionGenerator: self.radio_registry, self.tacan_registry, self.unit_map, + self.mission_data, ) tgo_generator.generate() @@ -103,17 +104,17 @@ class MissionGenerator: # Generate ground conflicts first so the JTACs get the first laser code (1688) # rather than the first player flight with a TGP. self.generate_ground_conflicts() - air_support, flights = self.generate_air_units(tgo_generator) + self.generate_air_units(tgo_generator) TriggerGenerator(self.mission, self.game).generate() ForcedOptionsGenerator(self.mission, self.game).generate() VisualsGenerator(self.mission, self.game).generate() - LuaGenerator(self.game, self.mission, air_support, flights).generate() + LuaGenerator(self.game, self.mission, self.mission_data).generate() DrawingsGenerator(self.mission, self.game).generate() self.setup_combined_arms() - self.notify_info_generators(tgo_generator, air_support, flights) + self.notify_info_generators() # TODO: Shouldn't this be first? namegen.reset_numbers() @@ -217,14 +218,12 @@ class MissionGenerator: enemy_cp.stances[player_cp.id], self.unit_map, self.radio_registry, - self.air_support, + self.mission_data, self.laser_code_registry, ) ground_conflict_gen.generate() - def generate_air_units( - self, tgo_generator: TgoGenerator - ) -> tuple[AirSupport, list[FlightData]]: + def generate_air_units(self, tgo_generator: TgoGenerator) -> None: """Generate the air units for the Operation""" # Air Support (Tanker & Awacs) @@ -234,7 +233,7 @@ class MissionGenerator: self.game, self.radio_registry, self.tacan_registry, - self.air_support, + self.mission_data, ) air_support_generator.generate() @@ -248,7 +247,7 @@ class MissionGenerator: self.tacan_registry, self.laser_code_registry, self.unit_map, - air_support=air_support_generator.air_support, + mission_data=air_support_generator.mission_data, helipads=tgo_generator.helipads, ) @@ -273,10 +272,10 @@ class MissionGenerator: if not flight.client_units: continue flight.aircraft_type.assign_channels_for_flight( - flight, air_support_generator.air_support + flight, air_support_generator.mission_data ) - return air_support_generator.air_support, aircraft_generator.flights + self.mission_data.flights = aircraft_generator.flights def generate_destroyed_units(self) -> None: """Add destroyed units to the Mission""" @@ -325,33 +324,30 @@ class MissionGenerator: def notify_info_generators( self, - tgo_generator: TgoGenerator, - air_support: AirSupport, - flights: list[FlightData], ) -> None: """Generates subscribed MissionInfoGenerator objects.""" - + mission_data = self.mission_data gens: list[MissionInfoGenerator] = [ KneeboardGenerator(self.mission, self.game), BriefingGenerator(self.mission, self.game), ] for gen in gens: - for dynamic_runway in tgo_generator.runways.values(): + for dynamic_runway in mission_data.runways: gen.add_dynamic_runway(dynamic_runway) - for tanker in air_support.tankers: + for tanker in mission_data.tankers: if tanker.blue: gen.add_tanker(tanker) - for aewc in air_support.awacs: + for aewc in mission_data.awacs: if aewc.blue: gen.add_awacs(aewc) - for jtac in air_support.jtacs: + for jtac in mission_data.jtacs: if jtac.blue: gen.add_jtac(jtac) - for flight in flights: + for flight in mission_data.flights: gen.add_flight(flight) gen.generate() diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index 3ad6000f..b647cd14 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -41,6 +41,7 @@ from dcs.unit import Unit, InvisibleFARP from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unittype import ShipType, VehicleType from dcs.vehicles import vehicle_map +from game.missiongenerator.missiondata import CarrierInfo, MissionData from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage @@ -351,6 +352,7 @@ class GenericCarrierGenerator(GroundObjectGenerator): icls_alloc: Iterator[int], runways: Dict[str, RunwayData], unit_map: UnitMap, + mission_data: MissionData, ) -> None: super().__init__(ground_object, country, game, mission, unit_map) self.ground_object = ground_object @@ -359,6 +361,7 @@ class GenericCarrierGenerator(GroundObjectGenerator): self.tacan_registry = tacan_registry self.icls_alloc = icls_alloc self.runways = runways + self.mission_data = mission_data def generate(self) -> None: @@ -400,6 +403,16 @@ class GenericCarrierGenerator(GroundObjectGenerator): self.add_runway_data( brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls ) + self.mission_data.carriers.append( + CarrierInfo( + group_name=ship_group.name, + unit_name=ship_group.units[0].name, + callsign=tacan_callsign, + freq=atc, + tacan=tacan, + blue=self.control_point.captured, + ) + ) @property def carrier_type(self) -> Optional[Type[ShipType]]: @@ -604,6 +617,7 @@ class TgoGenerator: radio_registry: RadioRegistry, tacan_registry: TacanRegistry, unit_map: UnitMap, + mission_data: MissionData, ) -> None: self.m = mission self.game = game @@ -613,6 +627,7 @@ class TgoGenerator: self.icls_alloc = iter(range(1, 21)) self.runways: Dict[str, RunwayData] = {} self.helipads: dict[ControlPoint, StaticGroup] = {} + self.mission_data = mission_data def generate(self) -> None: for cp in self.game.theater.controlpoints: @@ -640,6 +655,7 @@ class TgoGenerator: self.icls_alloc, self.runways, self.unit_map, + self.mission_data, ) elif isinstance(ground_object, LhaGroundObject): generator = LhaGenerator( @@ -653,6 +669,7 @@ class TgoGenerator: self.icls_alloc, self.runways, self.unit_map, + self.mission_data, ) elif isinstance(ground_object, MissileSiteGroundObject): generator = MissileSiteGenerator( @@ -663,3 +680,4 @@ class TgoGenerator: ground_object, country, self.game, self.m, self.unit_map ) generator.generate() + self.mission_data.runways = list(self.runways.values()) diff --git a/game/radio/channels.py b/game/radio/channels.py index 9bc3ef93..eb51d4fa 100644 --- a/game/radio/channels.py +++ b/game/radio/channels.py @@ -5,14 +5,14 @@ from typing import Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from game.missiongenerator.aircraft.flightdata import FlightData - from game.missiongenerator.airsupport import AirSupport + from game.missiongenerator.missiondata import MissionData class RadioChannelAllocator: """Base class for radio channel allocators.""" def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: """Assigns mission frequencies to preset channels for the flight.""" raise NotImplementedError @@ -44,7 +44,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator): intra_flight_radio_index: Optional[int] def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: if self.intra_flight_radio_index is not None: flight.assign_channel( @@ -70,10 +70,10 @@ class CommonRadioChannelAllocator(RadioChannelAllocator): flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc) # TODO: If there ever are multiple AWACS, limit to mission relevant. - for awacs in air_support.awacs: + for awacs in mission_data.awacs: flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) - for jtac in air_support.jtacs: + for jtac in mission_data.jtacs: flight.assign_channel(radio_id, next(channel_alloc), jtac.freq) if flight.arrival != flight.departure and flight.arrival.atc is not None: @@ -81,7 +81,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator): try: # TODO: Skip incompatible tankers. - for tanker in air_support.tankers: + for tanker in mission_data.tankers: flight.assign_channel(radio_id, next(channel_alloc), tanker.freq) if flight.divert is not None and flight.divert.atc is not None: @@ -108,7 +108,7 @@ class NoOpChannelAllocator(RadioChannelAllocator): """Channel allocator for aircraft that don't support preset channels.""" def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: pass @@ -122,7 +122,7 @@ class FarmerRadioChannelAllocator(RadioChannelAllocator): """Preset channel allocator for the MiG-19P.""" def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: # The Farmer only has 6 preset channels. It also only has a VHF radio, # and currently our ATC data and AWACS are only in the UHF band. @@ -141,7 +141,7 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator): """Preset channel allocator for the AJS37.""" def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: # The Viggen's preset channels are handled differently from other # aircraft. Since 2.7.9 the group channels will not be generated automatically @@ -161,10 +161,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator): radio_id, next(channel_alloc), flight.intra_flight_channel ) - for awacs in air_support.awacs: + for awacs in mission_data.awacs: flight.assign_channel(radio_id, next(channel_alloc), awacs.freq) - for jtac in air_support.jtacs: + for jtac in mission_data.jtacs: flight.assign_channel(radio_id, next(channel_alloc), jtac.freq) if flight.departure.atc is not None: @@ -184,7 +184,7 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator): """Preset channel allocator for the SCR522 WW2 radios. (4 channels)""" def assign_channels_for_flight( - self, flight: FlightData, air_support: AirSupport + self, flight: FlightData, mission_data: MissionData ) -> None: radio_id = 1 flight.assign_channel(radio_id, 1, flight.intra_flight_channel) diff --git a/game/server/flights/routes.py b/game/server/flights/routes.py index 886e3fa0..6ce69315 100644 --- a/game/server/flights/routes.py +++ b/game/server/flights/routes.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends from shapely.geometry import LineString, Point as ShapelyPoint from game import Game +from game.ato.flightplans.airassault import AirAssaultFlightPlan from game.ato.flightplans.cas import CasFlightPlan from game.ato.flightplans.patrolling import PatrollingFlightPlan from game.server import GameContext @@ -39,19 +40,23 @@ def commit_boundary( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> LeafletPoly: flight = game.db.flights.get(flight_id) - if not isinstance(flight.flight_plan, PatrollingFlightPlan): - return [] - start = flight.flight_plan.layout.patrol_start - end = flight.flight_plan.layout.patrol_end - if isinstance(flight.flight_plan, CasFlightPlan): + if isinstance(flight.flight_plan, CasFlightPlan) or isinstance( + flight.flight_plan, AirAssaultFlightPlan + ): + # Special Commit boundary for CAS and AirAssault center = flight.flight_plan.layout.target.position commit_center = ShapelyPoint(center.x, center.y) - else: + elif isinstance(flight.flight_plan, PatrollingFlightPlan): + # Commit boundary for standard patrolling flight plan + start = flight.flight_plan.layout.patrol_start + end = flight.flight_plan.layout.patrol_end commit_center = LineString( [ ShapelyPoint(start.x, start.y), ShapelyPoint(end.x, end.y), ] ) + else: + return [] bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters) return ShapelyUtil.poly_to_leaflet(bubble, game.theater) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 00d7569a..d8a8bfa1 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -1078,6 +1078,7 @@ class Airfield(ControlPoint): yield from [ FlightType.OCA_AIRCRAFT, FlightType.OCA_RUNWAY, + FlightType.AIR_ASSAULT, ] yield from super().mission_types(for_player) @@ -1394,6 +1395,7 @@ class Fob(ControlPoint): if not self.is_friendly(for_player): yield FlightType.STRIKE + yield FlightType.AIR_ASSAULT yield from super().mission_types(for_player) diff --git a/game/transfers.py b/game/transfers.py index 788565ba..09524a4e 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -98,6 +98,8 @@ class TransferOrder: transport: Optional[Transport] = field(default=None) + request_airflift: bool = field(default=False) + def __str__(self) -> str: """Returns the text that should be displayed for the transfer.""" count = self.size @@ -317,6 +319,7 @@ class AirliftPlanner: ): self.create_airlift_flight(squadron) if self.package.flights: + self.package.set_tot_asap() self.game.ato_for(self.for_player).add_package(self.package) def create_airlift_flight(self, squadron: Squadron) -> int: @@ -585,15 +588,18 @@ class PendingTransfers: network = self.network_for(transfer.position) path = network.shortest_path_between(transfer.position, transfer.destination) next_stop = path[0] - if network.link_type(transfer.position, next_stop) == TransitConnection.Road: - self.convoys.add(transfer, next_stop) - elif ( - network.link_type(transfer.position, next_stop) - == TransitConnection.Shipping - ): - self.cargo_ships.add(transfer, next_stop) - else: - AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift() + if not transfer.request_airflift: + if ( + network.link_type(transfer.position, next_stop) + == TransitConnection.Road + ): + return self.convoys.add(transfer, next_stop) + elif ( + network.link_type(transfer.position, next_stop) + == TransitConnection.Shipping + ): + return self.cargo_ships.add(transfer, next_stop) + AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift() def new_transfer(self, transfer: TransferOrder) -> None: transfer.origin.base.commit_losses(transfer.units) @@ -774,3 +780,13 @@ class PendingTransfers: self.game.coalition_for(self.player).add_procurement_request( AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap) ) + + def transfer_for_flight(self, flight: Flight) -> Optional[TransferOrder]: + for transfer in self.pending_transfers: + if transfer.transport is None or not isinstance( + transfer.transport, Airlift + ): + continue + if transfer.transport.flight == flight: + return transfer + return None diff --git a/qt_ui/widgets/combos/QFlightTypeComboBox.py b/qt_ui/widgets/combos/QFlightTypeComboBox.py index df47f56d..bb214ad3 100644 --- a/qt_ui/widgets/combos/QFlightTypeComboBox.py +++ b/qt_ui/widgets/combos/QFlightTypeComboBox.py @@ -1,6 +1,8 @@ """Combo box for selecting a flight's task type.""" from PySide2.QtWidgets import QComboBox +from game.ato.flighttype import FlightType +from game.settings.settings import Settings from game.theater import ConflictTheater, MissionTarget @@ -8,9 +10,16 @@ from game.theater import ConflictTheater, MissionTarget class QFlightTypeComboBox(QComboBox): """Combo box for selecting a flight task type.""" - def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None: + def __init__( + self, theater: ConflictTheater, target: MissionTarget, settings: Settings + ) -> None: super().__init__() self.theater = theater self.target = target for mission_type in self.target.mission_types(for_player=True): + if mission_type == FlightType.AIR_ASSAULT and not settings.plugin_option( + "ctld" + ): + # Only add Air Assault if ctld plugin is enabled + continue self.addItem(str(mission_type), userData=mission_type) diff --git a/qt_ui/windows/basemenu/NewUnitTransferDialog.py b/qt_ui/windows/basemenu/NewUnitTransferDialog.py index 21a70159..30439162 100644 --- a/qt_ui/windows/basemenu/NewUnitTransferDialog.py +++ b/qt_ui/windows/basemenu/NewUnitTransferDialog.py @@ -86,7 +86,11 @@ class TransferOptionsPanel(QVBoxLayout): super().__init__() self.source_combo_box = TransferDestinationComboBox(game, origin) + self.transport_type = QComboBox() + self.transport_type.addItem("Auto", "auto") + self.transport_type.addItem("Airlift", "airlift") self.addLayout(QLabeledWidget("Destination:", self.source_combo_box)) + self.addLayout(QLabeledWidget("Requested transport type:", self.transport_type)) @property def changed(self): @@ -96,6 +100,10 @@ class TransferOptionsPanel(QVBoxLayout): def current(self) -> ControlPoint: return self.source_combo_box.currentData() + @property + def request_airlift(self) -> bool: + return self.transport_type.currentData() == "airlift" + class TransferControls(QGroupBox): def __init__( @@ -293,6 +301,7 @@ class NewUnitTransferDialog(QDialog): origin=self.origin, destination=destination, units=transfers, + request_airflift=self.dest_panel.request_airlift, ) self.game_model.transfer_model.new_transfer(transfer) self.close() diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index fd1cdcc4..5c9f5964 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -50,7 +50,9 @@ class QFlightCreator(QDialog): layout = QVBoxLayout() - self.task_selector = QFlightTypeComboBox(self.game.theater, package.target) + self.task_selector = QFlightTypeComboBox( + self.game.theater, package.target, self.game.settings + ) self.task_selector.setCurrentIndex(0) self.task_selector.currentIndexChanged.connect(self.on_task_changed) layout.addLayout(QLabeledWidget("Task:", self.task_selector)) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 67b253ef..55811bf9 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -62,6 +62,12 @@ class QFlightWaypointTab(QFrame): self.recreate_buttons.clear() for task in self.package.target.mission_types(for_player=True): + if task == FlightType.AIR_ASSAULT and not self.game.settings.plugin_option( + "ctld" + ): + # Only add Air Assault if ctld plugin is enabled + continue + def make_closure(arg): def closure(): return self.confirm_recreate(arg) diff --git a/resources/customized_payloads/UH-1H.lua b/resources/customized_payloads/UH-1H.lua index e0647245..c38b3b50 100644 --- a/resources/customized_payloads/UH-1H.lua +++ b/resources/customized_payloads/UH-1H.lua @@ -136,6 +136,26 @@ local unitPayloads = { [4] = 16, }, }, + [6] = { + ["displayName"] = "Liberation Air Assault", + ["name"] = "Liberation Air Assault", + ["pylons"] = { + [1] = { + ["CLSID"] = "M60_SIDE_R", + ["num"] = 4, + }, + [2] = { + ["CLSID"] = "M60_SIDE_L", + ["num"] = 3, + }, + }, + ["tasks"] = { + [1] = 32, + [2] = 31, + [3] = 35, + [4] = 16, + }, + }, }, ["unitType"] = "UH-1H", } diff --git a/resources/plugins/ctld/ctld-config.lua b/resources/plugins/ctld/ctld-config.lua index 359a0478..9a057b7e 100644 --- a/resources/plugins/ctld/ctld-config.lua +++ b/resources/plugins/ctld/ctld-config.lua @@ -5,6 +5,22 @@ -- see https://github.com/dcs-liberation/dcs_liberation ------------------------------------------------------------------------------------------------------------------------------------------------------------- +function spawn_crates() + --- CrateSpawn script which needs to be run after CTLD was initialized (3s delay) + env.info("DCSLiberation|CTLD plugin - Spawn crates") + for _, crate in pairs(dcsLiberation.Logistics.crates) do + ctld.spawnCrateAtZone(crate.coalition, tonumber(crate.weight), crate.zone) + end +end + +function preload_troops(preload_data) + --- Troop loading script which needs to be run after CTLD was initialized (5s delay) + env.info(string.format("DCSLiberation|CTLD plugin - Preloading Troops into %s", preload_data["unit"])) + ctld.preLoadTransport(preload_data["unit"], preload_data["amount"], true) +end + +function toboolean(str) return str == "true" end + -- CTLD plugin - configuration if dcsLiberation then local ctld_pickup_smoke = "none" @@ -19,25 +35,94 @@ if dcsLiberation then if dcsLiberation.plugins then if dcsLiberation.plugins.ctld then env.info("DCSLiberation|CTLD plugin - Setting Up") - + --- Debug Settings ctld.Debug = dcsLiberation.plugins.ctld.debug ctld.Trace = dcsLiberation.plugins.ctld.debug - ctld.transportPilotNames = {} - ctld.pickupZones = {} - ctld.dropOffZones = {} - ctld.wpZones = {} - for _, item in pairs(dcsLiberation.Logistics) do + -- Sling loadings settings + ctld.enableCrates = true + ctld.slingLoad = dcsLiberation.plugins.ctld.slingload + ctld.staticBugFix = not dcsLiberation.plugins.ctld.slingload + + --- Special unitLoad Settings as proposed in #2174 + ctld.maximumDistanceLogistic = 300 + ctld.unitLoadLimits = {} + ctld.unitActions = {} + for _, transport in pairs(dcsLiberation.Logistics.transports) do + ctld.unitLoadLimits[transport.aircraft_type] = tonumber(transport.cabin_size) + ctld.unitActions[transport.aircraft_type] = { crates = toboolean(transport.crates), troops = toboolean(transport.troops) } + end + + if dcsLiberation.plugins.ctld.smoke then + ctld_pickup_smoke = "blue" + ctld_dropoff_smoke = "green" + end + + -- Definition of spawnable things + local ctld_troops = ctld.loadableGroups + ctld.loadableGroups = { + { name = "Liberation Troops (2)", inf = 2 }, + { name = "Liberation Troops (4)", inf = 4 }, + { name = "Liberation Troops (6)", inf = 4, mg = 1, at = 1 }, + { name = "Liberation Troops (10)", inf = 5, mg = 2, at = 2, aa = 1 }, + { name = "Liberation Troops (12)", inf = 6, mg = 2, at = 2, aa = 2 }, + { name = "Liberation Troops (24)", inf = 12, mg = 4, at = 4, aa = 3, jtac = 1 }, + } + if dcsLiberation.plugins.ctld.tailorctld then + --- remove all default CTLD spawning settings + --- so that we can tailor them for the tasked missions + ctld.enableSmokeDrop = false + ctld.enabledRadioBeaconDrop = false + ctld.spawnableCrates = {} + ctld.vehiclesForTransportRED = {} + ctld.vehiclesForTransportBLUE = {} + ctld.transportPilotNames = {} + ctld.logisticUnits = {} + ctld.pickupZones = {} + ctld.dropOffZones = {} + ctld.wpZones = {} + else + --- append the default CTLD troops + for _, troop in pairs(ctld_troops) do + table.insert(ctld.loadableGroups, troop) + end + end + + --- add all carriers as pickup zone + if dcsLiberation.Carriers then + for _, carrier in pairs(dcsLiberation.Carriers) do + table.insert(ctld.pickupZones, { carrier.unit_name, ctld_pickup_smoke, -1, "yes", 0 }) + end + end + + --- generate mission specific spawnable crates + local spawnable_crates = {} + for _, crate in pairs(dcsLiberation.Logistics.spawnable_crates) do + table.insert(spawnable_crates, { weight = tonumber(crate.weight), desc = crate.unit, unit = crate.unit }) + end + ctld.spawnableCrates["Liberation Crates"] = spawnable_crates + + --- Parse the LogisticsInfo for the mission + for _, item in pairs(dcsLiberation.Logistics.flights) do for _, pilot in pairs(item.pilot_names) do table.insert(ctld.transportPilotNames, pilot) + if toboolean(item.preload) then + local amount = ctld.unitLoadLimits[item.aircraft_type] + timer.scheduleFunction(preload_troops, { unit = pilot, amount = amount }, timer.getTime() + 5) + end end - if dcsLiberation.plugins.ctld.smoke then - ctld_pickup_smoke = "blue" - ctld_dropoff_smoke = "green" + if item.pickup_zone then + table.insert(ctld.pickupZones, { item.pickup_zone, ctld_pickup_smoke, -1, "yes", tonumber(item.side) }) + end + if item.drop_off_zone then + table.insert(ctld.dropOffZones, { item.drop_off_zone, ctld_dropoff_smoke, tonumber(item.side) }) + end + if item.target_zone then + table.insert(ctld.wpZones, { item.target_zone, "none", "yes", tonumber(item.side) }) + end + if dcsLiberation.plugins.ctld.logisticunit and item.logistic_unit then + table.insert(ctld.logisticUnits, item.logistic_unit) end - table.insert(ctld.pickupZones, { item.pickup_zone, ctld_pickup_smoke, -1, "yes", tonumber(item.side) }) - table.insert(ctld.dropOffZones, { item.drop_off_zone, ctld_dropoff_smoke, tonumber(item.side) }) - table.insert(ctld.wpZones, { item.target_zone, "none", "yes", tonumber(item.side) }) end autolase = dcsLiberation.plugins.ctld.autolase @@ -52,15 +137,18 @@ if dcsLiberation then -- JTAC Autolase configuration code for _, jtac in pairs(dcsLiberation.JTACs) do - env.info(string.format("DCSLiberation|JTACAutolase - setting up %s", jtac.dcsUnit)) + env.info(string.format("DCSLiberation|JTACAutolase - setting up %s", jtac.dcsGroupName)) if fc3LaserCode then -- If fc3LaserCode is enabled in the plugin configuration, force the JTAC -- laser code to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. jtac.laserCode = 1113 end - ctld.JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle', nil, { freq = jtac.radio, mod = jtac.modulation, name = jtac.dcsGroupName }) + ctld.JTACAutoLase(jtac.dcsGroupName, jtac.laserCode, smoke, 'vehicle', nil, { freq = jtac.radio, mod = jtac.modulation, name = jtac.dcsGroupName }) end end + if dcsLiberation.plugins.ctld.airliftcrates then + timer.scheduleFunction(spawn_crates, nil, timer.getTime() + 3) + end end end end diff --git a/resources/plugins/ctld/plugin.json b/resources/plugins/ctld/plugin.json index ce8d8453..d50c8e85 100644 --- a/resources/plugins/ctld/plugin.json +++ b/resources/plugins/ctld/plugin.json @@ -2,11 +2,31 @@ "nameInUI": "CTLD", "defaultValue": true, "specificOptions": [ + { + "nameInUI": "Tailor CTLD for the Liberation specific missions", + "mnemonic": "tailorctld", + "defaultValue": true + }, + { + "nameInUI": "Create logistic unit in each pickup zone", + "mnemonic": "logisticunit", + "defaultValue": true + }, { "nameInUI": "CTLD Use smoke in zones", "mnemonic": "smoke", "defaultValue": true }, + { + "nameInUI": "Automatically spawn crates for airlift", + "mnemonic": "airliftcrates", + "defaultValue": false + }, + { + "nameInUI": "Use real sling loading", + "mnemonic": "slingload", + "defaultValue": false + }, { "nameInUI": "JTAC Autolase", "mnemonic": "autolase", @@ -18,13 +38,13 @@ "defaultValue": true }, { - "nameInUI": "Use FC3 laser code (1113)", + "nameInUI": "JTAC Use FC3 laser code (1113)", "mnemonic": "fc3LaserCode", "defaultValue": false }, { "nameInUI": "CTLD Debug", - "mnemonic": "ctld-debug", + "mnemonic": "debug", "defaultValue": false } ], diff --git a/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml b/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml index eb9524a6..c15ea350 100644 --- a/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml +++ b/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml @@ -10,3 +10,4 @@ mission_types: - CAS - BAI - Transport + - Air Assault diff --git a/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml b/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml index e080e529..aecd10fc 100644 --- a/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml +++ b/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml @@ -10,3 +10,4 @@ mission_types: - CAS - BAI - Transport + - Air Assault diff --git a/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml b/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml index 5d1ee4a6..0c4297dc 100644 --- a/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml +++ b/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml @@ -10,3 +10,4 @@ mission_types: - Transport - CAS - BAI + - Air Assault \ No newline at end of file diff --git a/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml b/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml index e9588acb..1a2a19cd 100644 --- a/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml +++ b/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml @@ -10,3 +10,4 @@ mission_types: - Transport - CAS - BAI + - Air Assault diff --git a/resources/squadrons/SH-60B/HSM-40.yaml b/resources/squadrons/SH-60B/HSM-40.yaml index 21707857..bb7e54f9 100644 --- a/resources/squadrons/SH-60B/HSM-40.yaml +++ b/resources/squadrons/SH-60B/HSM-40.yaml @@ -9,3 +9,4 @@ livery: standard mission_types: - Transport - Anti-ship + - Air Assault diff --git a/resources/squadrons/UH-1/HMLA-169-UH1H.yaml b/resources/squadrons/UH-1/HMLA-169-UH1H.yaml index 4075dcfe..77b748c4 100644 --- a/resources/squadrons/UH-1/HMLA-169-UH1H.yaml +++ b/resources/squadrons/UH-1/HMLA-169-UH1H.yaml @@ -10,3 +10,4 @@ mission_types: - CAS - OCA/Aircraft - Transport + - Air Assault diff --git a/resources/squadrons/UH-1/HMLA-269-UH1H.yaml b/resources/squadrons/UH-1/HMLA-269-UH1H.yaml index d02b98fb..155c6344 100644 --- a/resources/squadrons/UH-1/HMLA-269-UH1H.yaml +++ b/resources/squadrons/UH-1/HMLA-269-UH1H.yaml @@ -10,3 +10,4 @@ mission_types: - CAS - OCA/Aircraft - Transport + - Air Assault diff --git a/resources/squadrons/UH-60/US Army 101st Combat Aviation Brigade.yaml b/resources/squadrons/UH-60/US Army 101st Combat Aviation Brigade.yaml index b209274a..4ab8f192 100644 --- a/resources/squadrons/UH-60/US Army 101st Combat Aviation Brigade.yaml +++ b/resources/squadrons/UH-60/US Army 101st Combat Aviation Brigade.yaml @@ -10,3 +10,4 @@ mission_types: - CAS - OCA/Aircraft - Transport + - Air Assault diff --git a/resources/units/aircraft/AH-1W.yaml b/resources/units/aircraft/AH-1W.yaml index 24c4c3a8..0499ae68 100644 --- a/resources/units/aircraft/AH-1W.yaml +++ b/resources/units/aircraft/AH-1W.yaml @@ -1,5 +1,8 @@ +class: Helicopter always_keeps_gun: true carrier_capable: true +cabin_size: 0 # Can not transport troops +can_carry_crates: false # Can not carry crates description: The AH-1 Cobra was developed in the mid-1960s as an interim gunship for the U.S. Army for use during the Vietnam War. The Cobra shared the proven transmission, rotor system, and the T53 turboshaft engine of the UH-1 'Huey'. By June 1967, the diff --git a/resources/units/aircraft/AH-64A.yaml b/resources/units/aircraft/AH-64A.yaml index aeee4b0a..049aa61e 100644 --- a/resources/units/aircraft/AH-64A.yaml +++ b/resources/units/aircraft/AH-64A.yaml @@ -1,4 +1,7 @@ +class: Helicopter always_keeps_gun: true +cabin_size: 0 # Can not transport troops +can_carry_crates: false # Can not carry crates description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for a crew of two. It features a nose-mounted sensor suite for target acquisition and night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried diff --git a/resources/units/aircraft/AH-64D.yaml b/resources/units/aircraft/AH-64D.yaml index 82fe1f00..fddbef4e 100644 --- a/resources/units/aircraft/AH-64D.yaml +++ b/resources/units/aircraft/AH-64D.yaml @@ -1,5 +1,8 @@ +class: Helicopter always_keeps_gun: true lha_capable: true +cabin_size: 0 # Can not transport troops +can_carry_crates: false # Can not carry crates description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for a crew of two. It features a nose-mounted sensor suite for target acquisition and night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried diff --git a/resources/units/aircraft/AH-64D_BLK_II.yaml b/resources/units/aircraft/AH-64D_BLK_II.yaml index 514250bb..f16f4a62 100644 --- a/resources/units/aircraft/AH-64D_BLK_II.yaml +++ b/resources/units/aircraft/AH-64D_BLK_II.yaml @@ -1,5 +1,8 @@ +class: Helicopter always_keeps_gun: true lha_capable: true +cabin_size: 0 # Can not transport troops +can_carry_crates: false # Can not carry crates description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for a crew of two. It features a nose-mounted sensor suite for target acquisition and night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried diff --git a/resources/units/aircraft/CH-47D.yaml b/resources/units/aircraft/CH-47D.yaml index 69c44b1b..20b1bf82 100644 --- a/resources/units/aircraft/CH-47D.yaml +++ b/resources/units/aircraft/CH-47D.yaml @@ -1,4 +1,7 @@ +class: Helicopter +cabin_size: 24 # It should have 33 but we do not want so much for CTLD to be possible +can_carry_crates: true description: The CH-47D is a transport helicopter. -price: 4 +price: 6 variants: CH-47D: null diff --git a/resources/units/aircraft/CH-53E.yaml b/resources/units/aircraft/CH-53E.yaml index 8456be99..b28f03a1 100644 --- a/resources/units/aircraft/CH-53E.yaml +++ b/resources/units/aircraft/CH-53E.yaml @@ -1,4 +1,7 @@ +class: Helicopter +cabin_size: 24 # It should have 37 but we do not want so much for CTLD to be possible +can_carry_crates: true description: The CH-53 is a military transport helicopter. -price: 4 +price: 6 variants: CH-53E: null diff --git a/resources/units/aircraft/Hercules.yaml b/resources/units/aircraft/Hercules.yaml index af82aaa6..6d213225 100644 --- a/resources/units/aircraft/Hercules.yaml +++ b/resources/units/aircraft/Hercules.yaml @@ -9,5 +9,6 @@ origin: USA price: 18 role: Transport max_range: 1000 +cabin_size: 24 # It should have more but we do not want so much for CTLD to be possible variants: C-130J-30 Super Hercules: {} diff --git a/resources/units/aircraft/Ka-50.yaml b/resources/units/aircraft/Ka-50.yaml index 8391235d..c65366e7 100644 --- a/resources/units/aircraft/Ka-50.yaml +++ b/resources/units/aircraft/Ka-50.yaml @@ -1,3 +1,4 @@ +class: Helicopter always_keeps_gun: true carrier_capable: true description: @@ -8,6 +9,8 @@ description: that it has an ejection seat." introduced: 1995 lha_capable: true +cabin_size: 0 # Can not transport troops +can_carry_crates: true manufacturer: Kamov origin: USSR/Russia price: 20 diff --git a/resources/units/aircraft/Mi-24P.yaml b/resources/units/aircraft/Mi-24P.yaml index 10463359..aa1d0fb1 100644 --- a/resources/units/aircraft/Mi-24P.yaml +++ b/resources/units/aircraft/Mi-24P.yaml @@ -1,3 +1,4 @@ +class: Helicopter always_keeps_gun: true description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\ \ reporting name: Hind) is a large helicopter gunship, attack helicopter and low-capacity\ @@ -14,6 +15,8 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\ \ where equipped with Stinger Missiles from the CIA." lha_capable: true +cabin_size: 6 +can_carry_crates: true introduced: 1981 manufacturer: Mil origin: USSR/Russia diff --git a/resources/units/aircraft/Mi-24V.yaml b/resources/units/aircraft/Mi-24V.yaml index 7d6feb85..844037b2 100644 --- a/resources/units/aircraft/Mi-24V.yaml +++ b/resources/units/aircraft/Mi-24V.yaml @@ -1,3 +1,4 @@ +class: Helicopter always_keeps_gun: true description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\ \ reporting name: Hind) is a large helicopter gunship, attack helicopter and low-capacity\ @@ -14,6 +15,8 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\ \ where equiped with Stinger Misseles from the CIA." lha_capable: true +cabin_size: 6 +can_carry_crates: true introduced: 1976 manufacturer: Mil origin: USSR/Russia diff --git a/resources/units/aircraft/Mi-26.yaml b/resources/units/aircraft/Mi-26.yaml index 894aaa06..3d315194 100644 --- a/resources/units/aircraft/Mi-26.yaml +++ b/resources/units/aircraft/Mi-26.yaml @@ -1,3 +1,6 @@ -price: 4 +class: Helicopter +cabin_size: 24 # It should have 60+ but we do not want so much for CTLD to be possible +can_carry_crates: true +price: 6 variants: Mi-26: null diff --git a/resources/units/aircraft/Mi-28N.yaml b/resources/units/aircraft/Mi-28N.yaml index 78c7777a..2600df34 100644 --- a/resources/units/aircraft/Mi-28N.yaml +++ b/resources/units/aircraft/Mi-28N.yaml @@ -1,3 +1,6 @@ +class: Helicopter +cabin_size: 0 +can_carry_crates: false always_keeps_gun: true description: The Mil Mi-28 (NATO reporting name 'Havoc') is a Russian all-weather, day-night, military tandem, two-seat anti-armor attack helicopter. It is an attack diff --git a/resources/units/aircraft/Mi-8MT.yaml b/resources/units/aircraft/Mi-8MT.yaml index 0fe0ad20..dfd32982 100644 --- a/resources/units/aircraft/Mi-8MT.yaml +++ b/resources/units/aircraft/Mi-8MT.yaml @@ -1,9 +1,12 @@ +class: Helicopter carrier_capable: true description: The Mil Mi-8MTV2 is an upgraded version of one of the most widely produced helicopters in history and a combat transport and fire support veteran of countless operations around the world. introduced: 1981 lha_capable: true +cabin_size: 12 +can_carry_crates: true manufacturer: Mil origin: USSR/Russia price: 5 diff --git a/resources/units/aircraft/OH-58D.yaml b/resources/units/aircraft/OH-58D.yaml index d12d25b4..7e7825d4 100644 --- a/resources/units/aircraft/OH-58D.yaml +++ b/resources/units/aircraft/OH-58D.yaml @@ -1,3 +1,6 @@ +class: Helicopter +cabin_size: 0 # Can not transport troops +can_carry_crates: false # Can not carry crates carrier_capable: true description: The Bell OH-58 Kiowa is a family of single-engine, single-rotor, military helicopters used for observation, utility, and direct fire support. Bell Helicopter diff --git a/resources/units/aircraft/SA342L.yaml b/resources/units/aircraft/SA342L.yaml index 4c1384f4..83e40938 100644 --- a/resources/units/aircraft/SA342L.yaml +++ b/resources/units/aircraft/SA342L.yaml @@ -1,3 +1,4 @@ +class: Helicopter carrier_capable: true description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ \ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\ @@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter \ which features the famous Fenestron tail rotor." introduced: 1977 lha_capable: true +cabin_size: 2 +can_carry_crates: false manufacturer: "A\xE9rospatiale" origin: France price: 5 diff --git a/resources/units/aircraft/SA342M.yaml b/resources/units/aircraft/SA342M.yaml index 02ac8cad..b4511711 100644 --- a/resources/units/aircraft/SA342M.yaml +++ b/resources/units/aircraft/SA342M.yaml @@ -1,3 +1,4 @@ +class: Helicopter carrier_capable: true description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ \ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\ @@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter \ which features the famous Fenestron tail rotor." introduced: 1977 lha_capable: true +cabin_size: 2 +can_carry_crates: false manufacturer: "A\xE9rospatiale" origin: France price: 8 diff --git a/resources/units/aircraft/SA342Minigun.yaml b/resources/units/aircraft/SA342Minigun.yaml index 99da8f7e..15f7289f 100644 --- a/resources/units/aircraft/SA342Minigun.yaml +++ b/resources/units/aircraft/SA342Minigun.yaml @@ -1,4 +1,7 @@ +class: Helicopter price: 4 +cabin_size: 2 +can_carry_crates: false variants: SA342Minigun: null kneeboard_units: "metric" \ No newline at end of file diff --git a/resources/units/aircraft/SA342Mistral.yaml b/resources/units/aircraft/SA342Mistral.yaml index abbc0ebb..64ed19f9 100644 --- a/resources/units/aircraft/SA342Mistral.yaml +++ b/resources/units/aircraft/SA342Mistral.yaml @@ -1,3 +1,4 @@ +class: Helicopter carrier_capable: true description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ \ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\ @@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter \ which features the famous Fenestron tail rotor." introduced: 1977 lha_capable: true +cabin_size: 2 +can_carry_crates: false manufacturer: "A\xE9rospatiale" origin: France price: 8 diff --git a/resources/units/aircraft/SH-60B.yaml b/resources/units/aircraft/SH-60B.yaml index cbe61c7d..09644f37 100644 --- a/resources/units/aircraft/SH-60B.yaml +++ b/resources/units/aircraft/SH-60B.yaml @@ -1,3 +1,6 @@ +class: Helicopter +cabin_size: 6 +can_carry_crates: true carrier_capable: true description: The Sikorsky SH-60/MH-60 Seahawk (or Sea Hawk) is a twin turboshaft engine, multi-mission United States Navy helicopter based on the United States Army UH-60 diff --git a/resources/units/aircraft/UH-1H.yaml b/resources/units/aircraft/UH-1H.yaml index 8adf5933..dcd3e5b3 100644 --- a/resources/units/aircraft/UH-1H.yaml +++ b/resources/units/aircraft/UH-1H.yaml @@ -1,3 +1,4 @@ +class: Helicopter carrier_capable: true description: The UH-1 Iroquois, better known as the Huey, is one of the most iconic @@ -5,6 +6,8 @@ description: serve in both military and civilian roles around the globe today. introduced: 1967 lha_capable: true +cabin_size: 6 +can_carry_crates: true manufacturer: Bell origin: USA price: 4 diff --git a/resources/units/aircraft/UH-60A.yaml b/resources/units/aircraft/UH-60A.yaml index b99814f2..8c992940 100644 --- a/resources/units/aircraft/UH-60A.yaml +++ b/resources/units/aircraft/UH-60A.yaml @@ -1,3 +1,6 @@ +class: Helicopter price: 4 +cabin_size: 10 +can_carry_crates: true variants: UH-60A: null diff --git a/resources/units/aircraft/UH-60L.yaml b/resources/units/aircraft/UH-60L.yaml index 756bc7b2..a5b77607 100644 --- a/resources/units/aircraft/UH-60L.yaml +++ b/resources/units/aircraft/UH-60L.yaml @@ -1,3 +1,4 @@ +class: Helicopter description: The Sikorsky UH-60 Black Hawk is a four-blade, twin-engine, medium-lift utility helicopter manufactured by Sikorsky Aircraft. The UH-60A entered service with the U.S. Army in 1979, to replace the Bell UH-1 Iroquois as the Army's tactical transport helicopter. @@ -5,6 +6,8 @@ description: introduced: 1989 carrier_capable: true lha_capable: true +cabin_size: 10 +can_carry_crates: true manufacturer: Sikorsky origin: USA price: 4