Add AirAssault and Airlift mission types with CTLD support

- Add the new airassault mission type and special flightplans for it
- Add the mission type to airbase and FOB
- Add Layout for the UH-1H
- Add mission type to capable squadrons
- Allow the auto planner to task air assault missions when preconditions are met
- Improve Airlift mission type and improve the flightplan (Stopover and Helo landing)
- Allow Slingload and spawnable crates for airlift
- Rework airsupport to a general missiondata class
- Added Carrier Information to mission data
- Allow to define CTLD specific capabilities in the unit yaml
- Allow inflight preload and fixed wing support for air assault
This commit is contained in:
RndName 2022-04-12 12:42:17 +02:00
parent de148dbb61
commit aa77cfe4b9
73 changed files with 996 additions and 212 deletions

View File

@ -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]** 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]** 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]** 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 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. * **[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 options to the loadout editor for setting properties such as HMD choice.
* **[UI]** Added separate images for the different carrier types. * **[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). * **[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]** 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 ## Fixes

View File

@ -483,6 +483,20 @@ TRANSPORT_CAPABLE = [
Mi_26, 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] DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [ AEWC_CAPABLE = [
@ -538,6 +552,8 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
return REFUELING_CAPABALE return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT: elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE return TRANSPORT_CAPABLE
elif task == FlightType.AIR_ASSAULT:
return AIR_ASSAULT_CAPABLE
else: else:
logging.error(f"Unplannable flight type: {task}") logging.error(f"Unplannable flight type: {task}")
return [] return []

View File

@ -139,6 +139,10 @@ class Flight(SidcDescribable):
def unit_type(self) -> AircraftType: def unit_type(self) -> AircraftType:
return self.squadron.aircraft return self.squadron.aircraft
@property
def is_helo(self) -> bool:
return self.unit_type.dcs_unit_type.helicopter
@property @property
def from_cp(self) -> ControlPoint: def from_cp(self) -> ControlPoint:
return self.departure return self.departure

View File

@ -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

View File

@ -4,6 +4,7 @@ from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.utils import feet from game.utils import feet
from .ibuilder import IBuilder from .ibuilder import IBuilder
@ -30,9 +31,32 @@ class Builder(IBuilder):
builder = WaypointBuilder(self.flight, self.coalition) builder = WaypointBuilder(self.flight, self.coalition)
pickup = None pickup = None
nav_to_pickup = [] 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: if cargo.origin != self.flight.departure:
pickup = builder.pickup(cargo.origin) pickup = builder.stopover(cargo.origin, "PICKUP")
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
nav_to_pickup = builder.nav_path( nav_to_pickup = builder.nav_path(
self.flight.departure.position, self.flight.departure.position,
cargo.origin.position, cargo.origin.position,
@ -40,6 +64,16 @@ class Builder(IBuilder):
altitude_is_agl, 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( return AirliftLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup, nav_to_pickup=nav_to_pickup,
@ -50,14 +84,15 @@ class Builder(IBuilder):
altitude, altitude,
altitude_is_agl, altitude_is_agl,
), ),
drop_off=builder.drop_off(cargo.next_stop), drop_off=drop_off,
stopover=stopover,
nav_to_home=builder.nav_path( nav_to_home=builder.nav_path(
cargo.origin.position, cargo.origin.position,
self.flight.arrival.position, self.flight.arrival.position,
altitude, altitude,
altitude_is_agl, altitude_is_agl,
), ),
arrival=builder.land(self.flight.arrival), arrival=arrival,
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
@ -69,15 +104,18 @@ class AirliftLayout(StandardLayout):
pickup: FlightWaypoint | None pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint] nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint] nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.nav_to_pickup yield from self.nav_to_pickup
if self.pickup: if self.pickup is not None:
yield self.pickup yield self.pickup
yield from self.nav_to_drop_off yield from self.nav_to_drop_off
yield self.drop_off yield self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home yield from self.nav_to_home
yield self.arrival yield self.arrival
if self.divert is not None: if self.divert is not None:

View File

@ -8,6 +8,7 @@ from game.data.doctrine import Doctrine
from game.flightplan import IpZoneGeometry, JoinZoneGeometry from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from .aewc import AewcFlightPlan from .aewc import AewcFlightPlan
from .airassault import AirAssaultFlightPlan
from .airlift import AirliftFlightPlan from .airlift import AirliftFlightPlan
from .antiship import AntiShipFlightPlan from .antiship import AntiShipFlightPlan
from .bai import BaiFlightPlan from .bai import BaiFlightPlan
@ -108,6 +109,7 @@ class FlightPlanBuilder:
FlightType.AEWC: AewcFlightPlan, FlightType.AEWC: AewcFlightPlan,
FlightType.TRANSPORT: AirliftFlightPlan, FlightType.TRANSPORT: AirliftFlightPlan,
FlightType.FERRY: FerryFlightPlan, FlightType.FERRY: FerryFlightPlan,
FlightType.AIR_ASSAULT: AirAssaultFlightPlan,
} }
return plan_dict.get(task) return plan_dict.get(task)

View File

@ -54,7 +54,7 @@ class WaypointBuilder:
@property @property
def is_helo(self) -> bool: 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: def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier. """Create takeoff waypoint for the given arrival airfield or carrier.
@ -303,6 +303,9 @@ class WaypointBuilder:
def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint: def oca_strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"ATTACK {target.name}", target, flyover=True) 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 @staticmethod
def _target_area( def _target_area(
name: str, name: str,
@ -491,36 +494,57 @@ class WaypointBuilder:
) )
@staticmethod @staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint: def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint:
"""Creates a cargo pickup waypoint. """Creates a stopover waypoint.
Args: Args:
control_point: Pick up location. control_point: Pick up location.
""" """
return FlightWaypoint( return FlightWaypoint(
"PICKUP", name,
FlightWaypointType.PICKUP, FlightWaypointType.STOPOVER,
control_point.position, stopover.position,
meters(0), meters(0),
"RADIO", "RADIO",
description=f"Pick up cargo from {control_point}", description=f"Stopover at {stopover}",
pretty_name="Pick up location", pretty_name="Stopover location",
control_point=stopover,
) )
@staticmethod @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. """Creates a cargo drop-off waypoint.
Args: Args:
control_point: Drop-off location. control_point: Drop-off location.
""" """
control_point = drop_off if isinstance(drop_off, ControlPoint) else None
return FlightWaypoint( return FlightWaypoint(
"DROP OFF", "DROP OFF",
FlightWaypointType.PICKUP, FlightWaypointType.DROP_OFF,
control_point.position, drop_off.position,
meters(0), meters(0),
"RADIO", "RADIO",
description=f"Drop off cargo at {control_point}", description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop off location", pretty_name="Drop off location",
control_point=control_point, control_point=control_point,
) )

View File

@ -56,6 +56,7 @@ class FlightType(Enum):
SEAD_ESCORT = "SEAD Escort" SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling" REFUELING = "Refueling"
FERRY = "Ferry" FERRY = "Ferry"
AIR_ASSAULT = "Air Assault"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@ -89,6 +90,7 @@ class FlightType(Enum):
FlightType.OCA_RUNWAY, FlightType.OCA_RUNWAY,
FlightType.OCA_AIRCRAFT, FlightType.OCA_AIRCRAFT,
FlightType.SEAD_ESCORT, FlightType.SEAD_ESCORT,
FlightType.AIR_ASSAULT,
} }
@property @property
@ -112,4 +114,5 @@ class FlightType(Enum):
FlightType.SWEEP: AirEntity.FIGHTER, FlightType.SWEEP: AirEntity.FIGHTER,
FlightType.TARCAP: AirEntity.FIGHTER, FlightType.TARCAP: AirEntity.FIGHTER,
FlightType.TRANSPORT: AirEntity.UTILITY, FlightType.TRANSPORT: AirEntity.UTILITY,
FlightType.AIR_ASSAULT: AirEntity.ROTARY_WING,
}.get(self, AirEntity.UNSPECIFIED) }.get(self, AirEntity.UNSPECIFIED)

View File

@ -47,3 +47,4 @@ class FlightWaypointType(IntEnum):
DROP_OFF = 27 DROP_OFF = 27
BULLSEYE = 28 BULLSEYE = 28
REFUEL = 29 # Should look for nearby tanker to refuel from. REFUEL = 29 # Should look for nearby tanker to refuel from.
STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type

View File

@ -161,6 +161,7 @@ class Package:
FlightType.BAI, FlightType.BAI,
FlightType.DEAD, FlightType.DEAD,
FlightType.TRANSPORT, FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
FlightType.SEAD, FlightType.SEAD,
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,

View File

@ -139,6 +139,14 @@ class ObjectiveFinder:
"""Iterates over all active front lines in the theater.""" """Iterates over all active front lines in the theater."""
yield from self.game.theater.conflicts() 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]: def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs. """Iterates over friendly CPs that are vulnerable to enemy CPs.

View File

@ -7,6 +7,7 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import ( from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity, ReduceEnemyFrontLineCapacity,
) )
from game.commander.tasks.primitive.airassault import PlanAirAssault
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method from game.htn import CompoundTask, Method
@ -18,6 +19,7 @@ class CaptureBase(CompoundTask[TheaterState]):
front_line: FrontLine front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: 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 [BreakthroughAttack(self.front_line, state.context.coalition.player)]
yield [DestroyEnemyGroundUnits(self.front_line)] yield [DestroyEnemyGroundUnits(self.front_line)]
if self.worth_destroying_ammo_depots(state): if self.worth_destroying_ammo_depots(state):

View File

@ -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)

View File

@ -45,6 +45,7 @@ class TheaterState(WorldState["TheaterState"]):
context: PersistentContext context: PersistentContext
barcaps_needed: dict[ControlPoint, int] barcaps_needed: dict[ControlPoint, int]
active_front_lines: list[FrontLine] active_front_lines: list[FrontLine]
air_assault_targets: list[ControlPoint]
front_line_stances: dict[FrontLine, Optional[CombatStance]] front_line_stances: dict[FrontLine, Optional[CombatStance]]
vulnerable_front_lines: list[FrontLine] vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget] aewc_targets: list[MissionTarget]
@ -109,6 +110,7 @@ class TheaterState(WorldState["TheaterState"]):
context=self.context, context=self.context,
barcaps_needed=dict(self.barcaps_needed), barcaps_needed=dict(self.barcaps_needed),
active_front_lines=list(self.active_front_lines), active_front_lines=list(self.active_front_lines),
air_assault_targets=list(self.air_assault_targets),
front_line_stances=dict(self.front_line_stances), front_line_stances=dict(self.front_line_stances),
vulnerable_front_lines=list(self.vulnerable_front_lines), vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets), aewc_targets=list(self.aewc_targets),
@ -158,6 +160,7 @@ class TheaterState(WorldState["TheaterState"]):
cp: barcap_rounds for cp in finder.vulnerable_control_points() cp: barcap_rounds for cp in finder.vulnerable_control_points()
}, },
active_front_lines=list(finder.front_lines()), 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()}, front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()), vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()], aewc_targets=[finder.farthest_friendly_control_point()],

View File

@ -18,6 +18,7 @@ class UnitClass(Enum):
EARLY_WARNING_RADAR = "EarlyWarningRadar" EARLY_WARNING_RADAR = "EarlyWarningRadar"
FORTIFICATION = "Fortification" FORTIFICATION = "Fortification"
FRIGATE = "Frigate" FRIGATE = "Frigate"
HELICOPTER = "Helicopter"
HELICOPTER_CARRIER = "HelicopterCarrier" HELICOPTER_CARRIER = "HelicopterCarrier"
IFV = "IFV" IFV = "IFV"
INFANTRY = "Infantry" INFANTRY = "Infantry"

View File

@ -47,7 +47,7 @@ from game.utils import (
if TYPE_CHECKING: if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData 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 from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@ -182,6 +182,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator] channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer] 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 @property
def flyable(self) -> bool: def flyable(self) -> bool:
return self.dcs_unit_type.flyable return self.dcs_unit_type.flyable
@ -281,10 +289,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
return freq return freq
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
if self.channel_allocator is not 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: def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id) return self.channel_namer.channel_name(radio_id, channel_id)
@ -387,6 +395,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
if units_data == "metric": if units_data == "metric":
units = MetricUnits() 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") prop_overrides = data.get("default_overrides")
if prop_overrides is not None: if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path) cls._set_props_overrides(prop_overrides, aircraft, data_path)
@ -419,5 +430,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer, channel_namer=radio_config.channel_namer,
kneeboard_units=units, kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False), 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),
) )

View File

@ -62,7 +62,10 @@ class AircraftBehavior:
self.configure_runway_attack(group, flight) self.configure_runway_attack(group, flight)
elif self.task == FlightType.OCA_AIRCRAFT: elif self.task == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, flight) 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) self.configure_transport(group, flight)
elif self.task == FlightType.FERRY: elif self.task == FlightType.FERRY:
self.configure_ferry(group, flight) self.configure_ferry(group, flight)

View File

@ -17,7 +17,7 @@ from game.ato.flighttype import FlightType
from game.ato.package import Package from game.ato.package import Package
from game.ato.starttype import StartType from game.ato.starttype import StartType
from game.factions.faction import Faction 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.missiongenerator.lasercoderegistry import LaserCodeRegistry
from game.radio.radios import RadioRegistry from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanRegistry from game.radio.tacan import TacanRegistry
@ -49,7 +49,7 @@ class AircraftGenerator:
tacan_registry: TacanRegistry, tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry, laser_code_registry: LaserCodeRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport, mission_data: MissionData,
helipads: dict[ControlPoint, StaticGroup], helipads: dict[ControlPoint, StaticGroup],
) -> None: ) -> None:
self.mission = mission self.mission = mission
@ -61,7 +61,7 @@ class AircraftGenerator:
self.laser_code_registry = laser_code_registry self.laser_code_registry = laser_code_registry
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support self.mission_data = mission_data
self.helipads = helipads self.helipads = helipads
@cached_property @cached_property
@ -174,7 +174,7 @@ class AircraftGenerator:
self.radio_registry, self.radio_registry,
self.tacan_registy, self.tacan_registy,
self.laser_code_registry, self.laser_code_registry,
self.air_support, self.mission_data,
dynamic_runways, dynamic_runways,
self.use_client, self.use_client,
).configure() ).configure()

View File

@ -12,8 +12,9 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType from game.ato import Flight, FlightType
from game.callsigns import callsign_for_support_unit from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum 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.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.runways import RunwayData from game.runways import RunwayData
@ -40,7 +41,7 @@ class FlightGroupConfigurator:
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, tacan_registry: TacanRegistry,
laser_code_registry: LaserCodeRegistry, laser_code_registry: LaserCodeRegistry,
air_support: AirSupport, mission_data: MissionData,
dynamic_runways: dict[str, RunwayData], dynamic_runways: dict[str, RunwayData],
use_client: bool, use_client: bool,
) -> None: ) -> None:
@ -52,7 +53,7 @@ class FlightGroupConfigurator:
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.laser_code_registry = laser_code_registry self.laser_code_registry = laser_code_registry
self.air_support = air_support self.mission_data = mission_data
self.dynamic_runways = dynamic_runways self.dynamic_runways = dynamic_runways
self.use_client = use_client self.use_client = use_client
@ -74,6 +75,20 @@ class FlightGroupConfigurator:
self.game.theater, self.game.conditions, self.dynamic_runways 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( mission_start_time, waypoints = WaypointGenerator(
self.flight, self.flight,
self.group, self.group,
@ -81,7 +96,7 @@ class FlightGroupConfigurator:
self.game.conditions.start_time, self.game.conditions.start_time,
self.time, self.time,
self.game.settings, self.game.settings,
self.air_support, self.mission_data,
).create_waypoints() ).create_waypoints()
return FlightData( return FlightData(
@ -130,7 +145,7 @@ class FlightGroupConfigurator:
def register_air_support(self, channel: RadioFrequency) -> None: def register_air_support(self, channel: RadioFrequency) -> None:
callsign = callsign_for_support_unit(self.group) callsign = callsign_for_support_unit(self.group)
if isinstance(self.flight.flight_plan, AewcFlightPlan): if isinstance(self.flight.flight_plan, AewcFlightPlan):
self.air_support.awacs.append( self.mission_data.awacs.append(
AwacsInfo( AwacsInfo(
group_name=str(self.group.name), group_name=str(self.group.name),
callsign=callsign, callsign=callsign,
@ -143,7 +158,7 @@ class FlightGroupConfigurator:
) )
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan): elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir) tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.air_support.tankers.append( self.mission_data.tankers.append(
TankerInfo( TankerInfo(
group_name=str(self.group.name), group_name=str(self.group.name),
callsign=callsign, callsign=callsign,

View File

@ -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 from .pydcswaypointbuilder import PydcsWaypointBuilder
@ -6,9 +11,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class CargoStopBuilder(PydcsWaypointBuilder): class CargoStopBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
waypoint.type = "LandingReFuAr" # Create a landing task, currently only for Helos!
waypoint.action = PointAction.LandingReFuAr if self.flight.is_helo:
waypoint.landing_refuel_rearm_time = 2 # Minutes. # Calculate a landing point with a small buffer to prevent AI from landing
if (control_point := self.waypoint.control_point) is not None: # directly at the static ammo depot and exploding
waypoint.airdrome_id = control_point.airdrome_id_for_landing 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 return waypoint

View File

@ -10,7 +10,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit from game.theater import MissionTarget, TheaterUnit
TARGET_WAYPOINTS = ( TARGET_WAYPOINTS = (
@ -28,7 +28,7 @@ class PydcsWaypointBuilder:
flight: Flight, flight: Flight,
mission: Mission, mission: Mission,
elapsed_mission_time: timedelta, elapsed_mission_time: timedelta,
air_support: AirSupport, mission_data: MissionData,
) -> None: ) -> None:
self.waypoint = waypoint self.waypoint = waypoint
self.group = group self.group = group
@ -36,7 +36,7 @@ class PydcsWaypointBuilder:
self.flight = flight self.flight = flight
self.mission = mission self.mission = mission
self.elapsed_mission_time = elapsed_mission_time self.elapsed_mission_time = elapsed_mission_time
self.air_support = air_support self.mission_data = mission_data
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(

View File

@ -64,7 +64,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
waypoint.add_task(Tanker()) waypoint.add_task(Tanker())
if self.flight.unit_type.dcs_unit_type.tacan: 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 = tanker_info.tacan
tacan_callsign = { tacan_callsign = {
"Texaco": "TEX", "Texaco": "TEX",

View File

@ -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

View File

@ -16,7 +16,8 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightstate import InFlight, WaitingForStart from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType 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.settings import Settings
from game.utils import pairwise from game.utils import pairwise
from .baiingress import BaiIngressBuilder from .baiingress import BaiIngressBuilder
@ -48,7 +49,7 @@ class WaypointGenerator:
turn_start_time: datetime, turn_start_time: datetime,
time: datetime, time: datetime,
settings: Settings, settings: Settings,
air_support: AirSupport, mission_data: MissionData,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.group = group self.group = group
@ -56,7 +57,7 @@ class WaypointGenerator:
self.elapsed_mission_time = time - turn_start_time self.elapsed_mission_time = time - turn_start_time
self.time = time self.time = time
self.settings = settings self.settings = settings
self.air_support = air_support self.mission_data = mission_data
def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]: def create_waypoints(self) -> tuple[timedelta, list[FlightWaypoint]]:
for waypoint in self.flight.points: for waypoint in self.flight.points:
@ -134,6 +135,7 @@ class WaypointGenerator:
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.PICKUP: CargoStopBuilder, FlightWaypointType.PICKUP: CargoStopBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder, FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.STOPOVER: StopoverBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder( return builder(
@ -142,7 +144,7 @@ class WaypointGenerator:
self.flight, self.flight,
self.mission, self.mission,
self.elapsed_mission_time, self.elapsed_mission_time,
self.air_support, self.mission_data,
) )
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None: def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:

View File

@ -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)

View File

@ -21,7 +21,7 @@ from game.radio.radios import RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.utils import Heading from game.utils import Heading
from game.ato.ai_flight_planner_db import AEWC_CAPABLE 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 from .frontlineconflictdescription import FrontLineConflictDescription
if TYPE_CHECKING: if TYPE_CHECKING:
@ -43,14 +43,14 @@ class AirSupportGenerator:
game: Game, game: Game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, tacan_registry: TacanRegistry,
air_support: AirSupport, mission_data: MissionData,
) -> None: ) -> None:
self.mission = mission self.mission = mission
self.conflict = conflict self.conflict = conflict
self.game = game self.game = game
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.air_support = air_support self.mission_data = mission_data
@classmethod @classmethod
def support_tasks(cls) -> List[Type[MainTask]]: 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(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append( self.mission_data.tankers.append(
TankerInfo( TankerInfo(
str(tanker_group.name), group_name=str(tanker_group.name),
callsign, callsign=callsign,
tanker_unit_type.name, variant=tanker_unit_type.name,
freq, freq=freq,
tacan, tacan=tacan,
start_time=None, start_time=None,
end_time=None, end_time=None,
blue=True, blue=True,
@ -197,7 +197,7 @@ class AirSupportGenerator:
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append( self.mission_data.awacs.append(
AwacsInfo( AwacsInfo(
group_name=str(awacs_flight.name), group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight), callsign=callsign_for_support_unit(awacs_flight),

View File

@ -43,7 +43,7 @@ from game.radio.radios import RadioRegistry
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading from game.utils import Heading
from .airsupport import AirSupport, JtacInfo from .missiondata import MissionData, JtacInfo
from .frontlineconflictdescription import FrontLineConflictDescription from .frontlineconflictdescription import FrontLineConflictDescription
from .lasercoderegistry import LaserCodeRegistry from .lasercoderegistry import LaserCodeRegistry
@ -80,7 +80,7 @@ class FlotGenerator:
enemy_stance: CombatStance, enemy_stance: CombatStance,
unit_map: UnitMap, unit_map: UnitMap,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
air_support: AirSupport, mission_data: MissionData,
laser_code_registry: LaserCodeRegistry, laser_code_registry: LaserCodeRegistry,
) -> None: ) -> None:
self.mission = mission self.mission = mission
@ -92,7 +92,7 @@ class FlotGenerator:
self.game = game self.game = game
self.unit_map = unit_map self.unit_map = unit_map
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.air_support = air_support self.mission_data = mission_data
self.laser_code_registry = laser_code_registry self.laser_code_registry = laser_code_registry
def generate(self) -> None: def generate(self) -> None:
@ -166,7 +166,7 @@ class FlotGenerator:
) )
jtac.points[0].tasks.append( jtac.points[0].tasks.append(
FAC( FAC(
callsign=len(self.air_support.jtacs) + 1, callsign=len(self.mission_data.jtacs) + 1,
frequency=int(freq.mhz), frequency=int(freq.mhz),
modulation=freq.modulation, modulation=freq.modulation,
) )
@ -181,13 +181,13 @@ class FlotGenerator:
) )
# Note: Will need to change if we ever add ground based JTAC. # Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac) callsign = callsign_for_support_unit(jtac)
self.air_support.jtacs.append( self.mission_data.jtacs.append(
JtacInfo( JtacInfo(
jtac.name, group_name=jtac.name,
jtac.name, unit_name=jtac.units[0].name,
callsign, callsign=callsign,
frontline, region=frontline,
str(code), code=str(code),
blue=True, blue=True,
freq=freq, freq=freq,
) )

View File

@ -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

View File

@ -12,13 +12,13 @@ from dcs.translation import String
from dcs.triggers import TriggerStart from dcs.triggers import TriggerStart
from game.ato import FlightType from game.ato import FlightType
from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua from game.utils import escape_string_for_lua
from .aircraft.flightdata import FlightData from .missiondata import MissionData
from .airsupport import AirSupport
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -29,13 +29,11 @@ class LuaGenerator:
self, self,
game: Game, game: Game,
mission: Mission, mission: Mission,
air_support: AirSupport, mission_data: MissionData,
flights: list[FlightData],
) -> None: ) -> None:
self.game = game self.game = game
self.mission = mission self.mission = mission
self.air_support = air_support self.mission_data = mission_data
self.flights = flights
self.plugin_scripts: list[str] = [] self.plugin_scripts: list[str] = []
def generate(self) -> None: def generate(self) -> None:
@ -49,10 +47,20 @@ class LuaGenerator:
install_path.set_value(os.path.abspath(".")) install_path.set_value(os.path.abspath("."))
lua_data.add_item("Airbases") 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") 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 = tankers_object.add_item()
tanker_item.add_key_value("dcsGroupName", tanker.group_name) tanker_item.add_key_value("dcsGroupName", tanker.group_name)
tanker_item.add_key_value("callsign", tanker.callsign) tanker_item.add_key_value("callsign", tanker.callsign)
@ -63,14 +71,14 @@ class LuaGenerator:
) )
awacs_object = lua_data.add_item("AWACs") 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 = awacs_object.add_item()
awacs_item.add_key_value("dcsGroupName", awacs.group_name) awacs_item.add_key_value("dcsGroupName", awacs.group_name)
awacs_item.add_key_value("callsign", awacs.callsign) awacs_item.add_key_value("callsign", awacs.callsign)
awacs_item.add_key_value("radio", str(awacs.freq.mhz)) awacs_item.add_key_value("radio", str(awacs.freq.mhz))
jtacs_object = lua_data.add_item("JTACs") 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 = jtacs_object.add_item()
jtac_item.add_key_value("dcsGroupName", jtac.group_name) jtac_item.add_key_value("dcsGroupName", jtac.group_name)
jtac_item.add_key_value("callsign", jtac.callsign) jtac_item.add_key_value("callsign", jtac.callsign)
@ -81,16 +89,55 @@ class LuaGenerator:
jtac_item.add_key_value("modulation", jtac.freq.modulation.name) jtac_item.add_key_value("modulation", jtac.freq.modulation.name)
logistics_object = lua_data.add_item("Logistics") logistics_object = lua_data.add_item("Logistics")
for logistic_info in self.air_support.logistics.values(): logistics_flights = logistics_object.add_item("flights")
logistics_item = logistics_object.add_item() 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_data_array("pilot_names", logistic_info.pilot_names)
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone) 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("drop_off_zone", logistic_info.drop_off_zone)
logistics_item.add_key_value("target_zone", logistic_info.target_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("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") 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 [ if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP, FlightType.ANTISHIP,
FlightType.DEAD, FlightType.DEAD,
@ -225,6 +272,9 @@ class LuaItem(ABC):
def set_value(self, value: str) -> None: def set_value(self, value: str) -> None:
self.value = LuaValue(None, value) 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: def add_data_array(self, key: str, values: list[str]) -> None:
self._add_value(LuaValue(key, values)) self._add_value(LuaValue(key, values))

View File

@ -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)

View File

@ -22,7 +22,7 @@ from game.theater import Airfield, FrontLine
from game.theater.bullseye import Bullseye from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap from game.unitmap import UnitMap
from .aircraft.flightdata import FlightData from .aircraft.flightdata import FlightData
from .airsupport import AirSupport from .missiondata import MissionData
from .airsupportgenerator import AirSupportGenerator from .airsupportgenerator import AirSupportGenerator
from .beacons import load_beacons_for_terrain from .beacons import load_beacons_for_terrain
from .briefinggenerator import BriefingGenerator, MissionInfoGenerator from .briefinggenerator import BriefingGenerator, MissionInfoGenerator
@ -61,7 +61,7 @@ class MissionGenerator:
self.mission = Mission(game.theater.terrain) self.mission = Mission(game.theater.terrain)
self.unit_map = UnitMap() self.unit_map = UnitMap()
self.air_support = AirSupport() self.mission_data = MissionData()
self.laser_code_registry = LaserCodeRegistry() self.laser_code_registry = LaserCodeRegistry()
self.radio_registry = RadioRegistry() self.radio_registry = RadioRegistry()
@ -92,6 +92,7 @@ class MissionGenerator:
self.radio_registry, self.radio_registry,
self.tacan_registry, self.tacan_registry,
self.unit_map, self.unit_map,
self.mission_data,
) )
tgo_generator.generate() tgo_generator.generate()
@ -103,17 +104,17 @@ class MissionGenerator:
# Generate ground conflicts first so the JTACs get the first laser code (1688) # Generate ground conflicts first so the JTACs get the first laser code (1688)
# rather than the first player flight with a TGP. # rather than the first player flight with a TGP.
self.generate_ground_conflicts() 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() TriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate() ForcedOptionsGenerator(self.mission, self.game).generate()
VisualsGenerator(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() DrawingsGenerator(self.mission, self.game).generate()
self.setup_combined_arms() self.setup_combined_arms()
self.notify_info_generators(tgo_generator, air_support, flights) self.notify_info_generators()
# TODO: Shouldn't this be first? # TODO: Shouldn't this be first?
namegen.reset_numbers() namegen.reset_numbers()
@ -217,14 +218,12 @@ class MissionGenerator:
enemy_cp.stances[player_cp.id], enemy_cp.stances[player_cp.id],
self.unit_map, self.unit_map,
self.radio_registry, self.radio_registry,
self.air_support, self.mission_data,
self.laser_code_registry, self.laser_code_registry,
) )
ground_conflict_gen.generate() ground_conflict_gen.generate()
def generate_air_units( def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
self, tgo_generator: TgoGenerator
) -> tuple[AirSupport, list[FlightData]]:
"""Generate the air units for the Operation""" """Generate the air units for the Operation"""
# Air Support (Tanker & Awacs) # Air Support (Tanker & Awacs)
@ -234,7 +233,7 @@ class MissionGenerator:
self.game, self.game,
self.radio_registry, self.radio_registry,
self.tacan_registry, self.tacan_registry,
self.air_support, self.mission_data,
) )
air_support_generator.generate() air_support_generator.generate()
@ -248,7 +247,7 @@ class MissionGenerator:
self.tacan_registry, self.tacan_registry,
self.laser_code_registry, self.laser_code_registry,
self.unit_map, self.unit_map,
air_support=air_support_generator.air_support, mission_data=air_support_generator.mission_data,
helipads=tgo_generator.helipads, helipads=tgo_generator.helipads,
) )
@ -273,10 +272,10 @@ class MissionGenerator:
if not flight.client_units: if not flight.client_units:
continue continue
flight.aircraft_type.assign_channels_for_flight( 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: def generate_destroyed_units(self) -> None:
"""Add destroyed units to the Mission""" """Add destroyed units to the Mission"""
@ -325,33 +324,30 @@ class MissionGenerator:
def notify_info_generators( def notify_info_generators(
self, self,
tgo_generator: TgoGenerator,
air_support: AirSupport,
flights: list[FlightData],
) -> None: ) -> None:
"""Generates subscribed MissionInfoGenerator objects.""" """Generates subscribed MissionInfoGenerator objects."""
mission_data = self.mission_data
gens: list[MissionInfoGenerator] = [ gens: list[MissionInfoGenerator] = [
KneeboardGenerator(self.mission, self.game), KneeboardGenerator(self.mission, self.game),
BriefingGenerator(self.mission, self.game), BriefingGenerator(self.mission, self.game),
] ]
for gen in gens: 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) gen.add_dynamic_runway(dynamic_runway)
for tanker in air_support.tankers: for tanker in mission_data.tankers:
if tanker.blue: if tanker.blue:
gen.add_tanker(tanker) gen.add_tanker(tanker)
for aewc in air_support.awacs: for aewc in mission_data.awacs:
if aewc.blue: if aewc.blue:
gen.add_awacs(aewc) gen.add_awacs(aewc)
for jtac in air_support.jtacs: for jtac in mission_data.jtacs:
if jtac.blue: if jtac.blue:
gen.add_jtac(jtac) gen.add_jtac(jtac)
for flight in flights: for flight in mission_data.flights:
gen.add_flight(flight) gen.add_flight(flight)
gen.generate() gen.generate()

View File

@ -41,6 +41,7 @@ from dcs.unit import Unit, InvisibleFARP
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.radio.radios import RadioFrequency, RadioRegistry from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
@ -351,6 +352,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
icls_alloc: Iterator[int], icls_alloc: Iterator[int],
runways: Dict[str, RunwayData], runways: Dict[str, RunwayData],
unit_map: UnitMap, unit_map: UnitMap,
mission_data: MissionData,
) -> None: ) -> None:
super().__init__(ground_object, country, game, mission, unit_map) super().__init__(ground_object, country, game, mission, unit_map)
self.ground_object = ground_object self.ground_object = ground_object
@ -359,6 +361,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.tacan_registry = tacan_registry self.tacan_registry = tacan_registry
self.icls_alloc = icls_alloc self.icls_alloc = icls_alloc
self.runways = runways self.runways = runways
self.mission_data = mission_data
def generate(self) -> None: def generate(self) -> None:
@ -400,6 +403,16 @@ class GenericCarrierGenerator(GroundObjectGenerator):
self.add_runway_data( self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls 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 @property
def carrier_type(self) -> Optional[Type[ShipType]]: def carrier_type(self) -> Optional[Type[ShipType]]:
@ -604,6 +617,7 @@ class TgoGenerator:
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, tacan_registry: TacanRegistry,
unit_map: UnitMap, unit_map: UnitMap,
mission_data: MissionData,
) -> None: ) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
@ -613,6 +627,7 @@ class TgoGenerator:
self.icls_alloc = iter(range(1, 21)) self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {} self.runways: Dict[str, RunwayData] = {}
self.helipads: dict[ControlPoint, StaticGroup] = {} self.helipads: dict[ControlPoint, StaticGroup] = {}
self.mission_data = mission_data
def generate(self) -> None: def generate(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
@ -640,6 +655,7 @@ class TgoGenerator:
self.icls_alloc, self.icls_alloc,
self.runways, self.runways,
self.unit_map, self.unit_map,
self.mission_data,
) )
elif isinstance(ground_object, LhaGroundObject): elif isinstance(ground_object, LhaGroundObject):
generator = LhaGenerator( generator = LhaGenerator(
@ -653,6 +669,7 @@ class TgoGenerator:
self.icls_alloc, self.icls_alloc,
self.runways, self.runways,
self.unit_map, self.unit_map,
self.mission_data,
) )
elif isinstance(ground_object, MissileSiteGroundObject): elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator( generator = MissileSiteGenerator(
@ -663,3 +680,4 @@ class TgoGenerator:
ground_object, country, self.game, self.m, self.unit_map ground_object, country, self.game, self.m, self.unit_map
) )
generator.generate() generator.generate()
self.mission_data.runways = list(self.runways.values())

View File

@ -5,14 +5,14 @@ from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.airsupport import AirSupport from game.missiongenerator.missiondata import MissionData
class RadioChannelAllocator: class RadioChannelAllocator:
"""Base class for radio channel allocators.""" """Base class for radio channel allocators."""
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
"""Assigns mission frequencies to preset channels for the flight.""" """Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError raise NotImplementedError
@ -44,7 +44,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
intra_flight_radio_index: Optional[int] intra_flight_radio_index: Optional[int]
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
if self.intra_flight_radio_index is not None: if self.intra_flight_radio_index is not None:
flight.assign_channel( flight.assign_channel(
@ -70,10 +70,10 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc) flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant. # 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) 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) flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None: if flight.arrival != flight.departure and flight.arrival.atc is not None:
@ -81,7 +81,7 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
try: try:
# TODO: Skip incompatible tankers. # 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) flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None: 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.""" """Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
pass pass
@ -122,7 +122,7 @@ class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P.""" """Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio, # 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. # 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.""" """Preset channel allocator for the AJS37."""
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
# The Viggen's preset channels are handled differently from other # The Viggen's preset channels are handled differently from other
# aircraft. Since 2.7.9 the group channels will not be generated automatically # 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 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) 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) flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
if flight.departure.atc is not None: if flight.departure.atc is not None:
@ -184,7 +184,7 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)""" """Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight( def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport self, flight: FlightData, mission_data: MissionData
) -> None: ) -> None:
radio_id = 1 radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel) flight.assign_channel(radio_id, 1, flight.intra_flight_channel)

View File

@ -4,6 +4,7 @@ from fastapi import APIRouter, Depends
from shapely.geometry import LineString, Point as ShapelyPoint from shapely.geometry import LineString, Point as ShapelyPoint
from game import Game from game import Game
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightplans.cas import CasFlightPlan from game.ato.flightplans.cas import CasFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.server import GameContext from game.server import GameContext
@ -39,19 +40,23 @@ def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.require) flight_id: UUID, game: Game = Depends(GameContext.require)
) -> LeafletPoly: ) -> LeafletPoly:
flight = game.db.flights.get(flight_id) flight = game.db.flights.get(flight_id)
if not isinstance(flight.flight_plan, PatrollingFlightPlan): if isinstance(flight.flight_plan, CasFlightPlan) or isinstance(
return [] flight.flight_plan, AirAssaultFlightPlan
start = flight.flight_plan.layout.patrol_start ):
end = flight.flight_plan.layout.patrol_end # Special Commit boundary for CAS and AirAssault
if isinstance(flight.flight_plan, CasFlightPlan):
center = flight.flight_plan.layout.target.position center = flight.flight_plan.layout.target.position
commit_center = ShapelyPoint(center.x, center.y) 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( commit_center = LineString(
[ [
ShapelyPoint(start.x, start.y), ShapelyPoint(start.x, start.y),
ShapelyPoint(end.x, end.y), ShapelyPoint(end.x, end.y),
] ]
) )
else:
return []
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters) bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
return ShapelyUtil.poly_to_leaflet(bubble, game.theater) return ShapelyUtil.poly_to_leaflet(bubble, game.theater)

View File

@ -1078,6 +1078,7 @@ class Airfield(ControlPoint):
yield from [ yield from [
FlightType.OCA_AIRCRAFT, FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY, FlightType.OCA_RUNWAY,
FlightType.AIR_ASSAULT,
] ]
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@ -1394,6 +1395,7 @@ class Fob(ControlPoint):
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
yield FlightType.STRIKE yield FlightType.STRIKE
yield FlightType.AIR_ASSAULT
yield from super().mission_types(for_player) yield from super().mission_types(for_player)

View File

@ -98,6 +98,8 @@ class TransferOrder:
transport: Optional[Transport] = field(default=None) transport: Optional[Transport] = field(default=None)
request_airflift: bool = field(default=False)
def __str__(self) -> str: def __str__(self) -> str:
"""Returns the text that should be displayed for the transfer.""" """Returns the text that should be displayed for the transfer."""
count = self.size count = self.size
@ -317,6 +319,7 @@ class AirliftPlanner:
): ):
self.create_airlift_flight(squadron) self.create_airlift_flight(squadron)
if self.package.flights: if self.package.flights:
self.package.set_tot_asap()
self.game.ato_for(self.for_player).add_package(self.package) self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(self, squadron: Squadron) -> int: def create_airlift_flight(self, squadron: Squadron) -> int:
@ -585,14 +588,17 @@ class PendingTransfers:
network = self.network_for(transfer.position) network = self.network_for(transfer.position)
path = network.shortest_path_between(transfer.position, transfer.destination) path = network.shortest_path_between(transfer.position, transfer.destination)
next_stop = path[0] next_stop = path[0]
if network.link_type(transfer.position, next_stop) == TransitConnection.Road: if not transfer.request_airflift:
self.convoys.add(transfer, next_stop) if (
network.link_type(transfer.position, next_stop)
== TransitConnection.Road
):
return self.convoys.add(transfer, next_stop)
elif ( elif (
network.link_type(transfer.position, next_stop) network.link_type(transfer.position, next_stop)
== TransitConnection.Shipping == TransitConnection.Shipping
): ):
self.cargo_ships.add(transfer, next_stop) return self.cargo_ships.add(transfer, next_stop)
else:
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift() AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
def new_transfer(self, transfer: TransferOrder) -> None: def new_transfer(self, transfer: TransferOrder) -> None:
@ -774,3 +780,13 @@ class PendingTransfers:
self.game.coalition_for(self.player).add_procurement_request( self.game.coalition_for(self.player).add_procurement_request(
AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap) 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

View File

@ -1,6 +1,8 @@
"""Combo box for selecting a flight's task type.""" """Combo box for selecting a flight's task type."""
from PySide2.QtWidgets import QComboBox from PySide2.QtWidgets import QComboBox
from game.ato.flighttype import FlightType
from game.settings.settings import Settings
from game.theater import ConflictTheater, MissionTarget from game.theater import ConflictTheater, MissionTarget
@ -8,9 +10,16 @@ from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox): class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type.""" """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__() super().__init__()
self.theater = theater self.theater = theater
self.target = target self.target = target
for mission_type in self.target.mission_types(for_player=True): 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) self.addItem(str(mission_type), userData=mission_type)

View File

@ -86,7 +86,11 @@ class TransferOptionsPanel(QVBoxLayout):
super().__init__() super().__init__()
self.source_combo_box = TransferDestinationComboBox(game, origin) 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("Destination:", self.source_combo_box))
self.addLayout(QLabeledWidget("Requested transport type:", self.transport_type))
@property @property
def changed(self): def changed(self):
@ -96,6 +100,10 @@ class TransferOptionsPanel(QVBoxLayout):
def current(self) -> ControlPoint: def current(self) -> ControlPoint:
return self.source_combo_box.currentData() return self.source_combo_box.currentData()
@property
def request_airlift(self) -> bool:
return self.transport_type.currentData() == "airlift"
class TransferControls(QGroupBox): class TransferControls(QGroupBox):
def __init__( def __init__(
@ -293,6 +301,7 @@ class NewUnitTransferDialog(QDialog):
origin=self.origin, origin=self.origin,
destination=destination, destination=destination,
units=transfers, units=transfers,
request_airflift=self.dest_panel.request_airlift,
) )
self.game_model.transfer_model.new_transfer(transfer) self.game_model.transfer_model.new_transfer(transfer)
self.close() self.close()

View File

@ -50,7 +50,9 @@ class QFlightCreator(QDialog):
layout = QVBoxLayout() 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.setCurrentIndex(0)
self.task_selector.currentIndexChanged.connect(self.on_task_changed) self.task_selector.currentIndexChanged.connect(self.on_task_changed)
layout.addLayout(QLabeledWidget("Task:", self.task_selector)) layout.addLayout(QLabeledWidget("Task:", self.task_selector))

View File

@ -62,6 +62,12 @@ class QFlightWaypointTab(QFrame):
self.recreate_buttons.clear() self.recreate_buttons.clear()
for task in self.package.target.mission_types(for_player=True): 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 make_closure(arg):
def closure(): def closure():
return self.confirm_recreate(arg) return self.confirm_recreate(arg)

View File

@ -136,6 +136,26 @@ local unitPayloads = {
[4] = 16, [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", ["unitType"] = "UH-1H",
} }

View File

@ -5,6 +5,22 @@
-- see https://github.com/dcs-liberation/dcs_liberation -- 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 -- CTLD plugin - configuration
if dcsLiberation then if dcsLiberation then
local ctld_pickup_smoke = "none" local ctld_pickup_smoke = "none"
@ -19,26 +35,95 @@ if dcsLiberation then
if dcsLiberation.plugins then if dcsLiberation.plugins then
if dcsLiberation.plugins.ctld then if dcsLiberation.plugins.ctld then
env.info("DCSLiberation|CTLD plugin - Setting Up") env.info("DCSLiberation|CTLD plugin - Setting Up")
--- Debug Settings
ctld.Debug = dcsLiberation.plugins.ctld.debug ctld.Debug = dcsLiberation.plugins.ctld.debug
ctld.Trace = 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
for _, pilot in pairs(item.pilot_names) do ctld.enableCrates = true
table.insert(ctld.transportPilotNames, pilot) 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 end
if dcsLiberation.plugins.ctld.smoke then if dcsLiberation.plugins.ctld.smoke then
ctld_pickup_smoke = "blue" ctld_pickup_smoke = "blue"
ctld_dropoff_smoke = "green" ctld_dropoff_smoke = "green"
end 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 item.pickup_zone then
table.insert(ctld.pickupZones, { item.pickup_zone, ctld_pickup_smoke, -1, "yes", tonumber(item.side) }) 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) }) 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) }) table.insert(ctld.wpZones, { item.target_zone, "none", "yes", tonumber(item.side) })
end end
if dcsLiberation.plugins.ctld.logisticunit and item.logistic_unit then
table.insert(ctld.logisticUnits, item.logistic_unit)
end
end
autolase = dcsLiberation.plugins.ctld.autolase autolase = dcsLiberation.plugins.ctld.autolase
env.info(string.format("DCSLiberation|CTLD plugin - JTAC AutoLase enabled = %s", tostring(autolase))) env.info(string.format("DCSLiberation|CTLD plugin - JTAC AutoLase enabled = %s", tostring(autolase)))
@ -52,14 +137,17 @@ if dcsLiberation then
-- JTAC Autolase configuration code -- JTAC Autolase configuration code
for _, jtac in pairs(dcsLiberation.JTACs) do 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 then
-- If fc3LaserCode is enabled in the plugin configuration, force the JTAC -- 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. -- laser code to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
jtac.laserCode = 1113 jtac.laserCode = 1113
end 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
end
if dcsLiberation.plugins.ctld.airliftcrates then
timer.scheduleFunction(spawn_crates, nil, timer.getTime() + 3)
end end
end end
end end

View File

@ -2,11 +2,31 @@
"nameInUI": "CTLD", "nameInUI": "CTLD",
"defaultValue": true, "defaultValue": true,
"specificOptions": [ "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", "nameInUI": "CTLD Use smoke in zones",
"mnemonic": "smoke", "mnemonic": "smoke",
"defaultValue": true "defaultValue": true
}, },
{
"nameInUI": "Automatically spawn crates for airlift",
"mnemonic": "airliftcrates",
"defaultValue": false
},
{
"nameInUI": "Use real sling loading",
"mnemonic": "slingload",
"defaultValue": false
},
{ {
"nameInUI": "JTAC Autolase", "nameInUI": "JTAC Autolase",
"mnemonic": "autolase", "mnemonic": "autolase",
@ -18,13 +38,13 @@
"defaultValue": true "defaultValue": true
}, },
{ {
"nameInUI": "Use FC3 laser code (1113)", "nameInUI": "JTAC Use FC3 laser code (1113)",
"mnemonic": "fc3LaserCode", "mnemonic": "fc3LaserCode",
"defaultValue": false "defaultValue": false
}, },
{ {
"nameInUI": "CTLD Debug", "nameInUI": "CTLD Debug",
"mnemonic": "ctld-debug", "mnemonic": "debug",
"defaultValue": false "defaultValue": false
} }
], ],

View File

@ -10,3 +10,4 @@ mission_types:
- CAS - CAS
- BAI - BAI
- Transport - Transport
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- CAS - CAS
- BAI - BAI
- Transport - Transport
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- Transport - Transport
- CAS - CAS
- BAI - BAI
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- Transport - Transport
- CAS - CAS
- BAI - BAI
- Air Assault

View File

@ -9,3 +9,4 @@ livery: standard
mission_types: mission_types:
- Transport - Transport
- Anti-ship - Anti-ship
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- CAS - CAS
- OCA/Aircraft - OCA/Aircraft
- Transport - Transport
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- CAS - CAS
- OCA/Aircraft - OCA/Aircraft
- Transport - Transport
- Air Assault

View File

@ -10,3 +10,4 @@ mission_types:
- CAS - CAS
- OCA/Aircraft - OCA/Aircraft
- Transport - Transport
- Air Assault

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
carrier_capable: 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 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, 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 rotor system, and the T53 turboshaft engine of the UH-1 'Huey'. By June 1967, the

View File

@ -1,4 +1,7 @@
class: Helicopter
always_keeps_gun: true 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 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 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 night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
lha_capable: 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 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 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 night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
lha_capable: 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 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 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 night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

View File

@ -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. description: The CH-47D is a transport helicopter.
price: 4 price: 6
variants: variants:
CH-47D: null CH-47D: null

View File

@ -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. description: The CH-53 is a military transport helicopter.
price: 4 price: 6
variants: variants:
CH-53E: null CH-53E: null

View File

@ -9,5 +9,6 @@ origin: USA
price: 18 price: 18
role: Transport role: Transport
max_range: 1000 max_range: 1000
cabin_size: 24 # It should have more but we do not want so much for CTLD to be possible
variants: variants:
C-130J-30 Super Hercules: {} C-130J-30 Super Hercules: {}

View File

@ -1,3 +1,4 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
carrier_capable: true carrier_capable: true
description: description:
@ -8,6 +9,8 @@ description:
that it has an ejection seat." that it has an ejection seat."
introduced: 1995 introduced: 1995
lha_capable: true lha_capable: true
cabin_size: 0 # Can not transport troops
can_carry_crates: true
manufacturer: Kamov manufacturer: Kamov
origin: USSR/Russia origin: USSR/Russia
price: 20 price: 20

View File

@ -1,3 +1,4 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\ 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\ \ 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\ \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\
\ where equipped with Stinger Missiles from the CIA." \ where equipped with Stinger Missiles from the CIA."
lha_capable: true lha_capable: true
cabin_size: 6
can_carry_crates: true
introduced: 1981 introduced: 1981
manufacturer: Mil manufacturer: Mil
origin: USSR/Russia origin: USSR/Russia

View File

@ -1,3 +1,4 @@
class: Helicopter
always_keeps_gun: true always_keeps_gun: true
description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\ 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\ \ 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\ \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\
\ where equiped with Stinger Misseles from the CIA." \ where equiped with Stinger Misseles from the CIA."
lha_capable: true lha_capable: true
cabin_size: 6
can_carry_crates: true
introduced: 1976 introduced: 1976
manufacturer: Mil manufacturer: Mil
origin: USSR/Russia origin: USSR/Russia

View File

@ -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: variants:
Mi-26: null Mi-26: null

View File

@ -1,3 +1,6 @@
class: Helicopter
cabin_size: 0
can_carry_crates: false
always_keeps_gun: true always_keeps_gun: true
description: The Mil Mi-28 (NATO reporting name 'Havoc') is a Russian all-weather, 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 day-night, military tandem, two-seat anti-armor attack helicopter. It is an attack

View File

@ -1,9 +1,12 @@
class: Helicopter
carrier_capable: true carrier_capable: true
description: The Mil Mi-8MTV2 is an upgraded version of one of the most widely produced 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 helicopters in history and a combat transport and fire support veteran of countless
operations around the world. operations around the world.
introduced: 1981 introduced: 1981
lha_capable: true lha_capable: true
cabin_size: 12
can_carry_crates: true
manufacturer: Mil manufacturer: Mil
origin: USSR/Russia origin: USSR/Russia
price: 5 price: 5

View File

@ -1,3 +1,6 @@
class: Helicopter
cabin_size: 0 # Can not transport troops
can_carry_crates: false # Can not carry crates
carrier_capable: true carrier_capable: true
description: The Bell OH-58 Kiowa is a family of single-engine, single-rotor, military 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 helicopters used for observation, utility, and direct fire support. Bell Helicopter

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ 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\ \ 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." \ which features the famous Fenestron tail rotor."
introduced: 1977 introduced: 1977
lha_capable: true lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale" manufacturer: "A\xE9rospatiale"
origin: France origin: France
price: 5 price: 5

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ 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\ \ 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." \ which features the famous Fenestron tail rotor."
introduced: 1977 introduced: 1977
lha_capable: true lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale" manufacturer: "A\xE9rospatiale"
origin: France origin: France
price: 8 price: 8

View File

@ -1,4 +1,7 @@
class: Helicopter
price: 4 price: 4
cabin_size: 2
can_carry_crates: false
variants: variants:
SA342Minigun: null SA342Minigun: null
kneeboard_units: "metric" kneeboard_units: "metric"

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\ 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\ \ 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." \ which features the famous Fenestron tail rotor."
introduced: 1977 introduced: 1977
lha_capable: true lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale" manufacturer: "A\xE9rospatiale"
origin: France origin: France
price: 8 price: 8

View File

@ -1,3 +1,6 @@
class: Helicopter
cabin_size: 6
can_carry_crates: true
carrier_capable: true carrier_capable: true
description: The Sikorsky SH-60/MH-60 Seahawk (or Sea Hawk) is a twin turboshaft engine, 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 multi-mission United States Navy helicopter based on the United States Army UH-60

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true carrier_capable: true
description: description:
The UH-1 Iroquois, better known as the Huey, is one of the most iconic 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. serve in both military and civilian roles around the globe today.
introduced: 1967 introduced: 1967
lha_capable: true lha_capable: true
cabin_size: 6
can_carry_crates: true
manufacturer: Bell manufacturer: Bell
origin: USA origin: USA
price: 4 price: 4

View File

@ -1,3 +1,6 @@
class: Helicopter
price: 4 price: 4
cabin_size: 10
can_carry_crates: true
variants: variants:
UH-60A: null UH-60A: null

View File

@ -1,3 +1,4 @@
class: Helicopter
description: description:
The Sikorsky UH-60 Black Hawk is a four-blade, twin-engine, medium-lift utility helicopter manufactured by Sikorsky Aircraft. 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. 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 introduced: 1989
carrier_capable: true carrier_capable: true
lha_capable: true lha_capable: true
cabin_size: 10
can_carry_crates: true
manufacturer: Sikorsky manufacturer: Sikorsky
origin: USA origin: USA
price: 4 price: 4