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]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress.
* **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle.
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
* **[Modding]** Updated UH-60L mod version support to 1.3.1
* **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly.
* **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
* **[UI]** Added separate images for the different carrier types.
* **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
* **[Plugins]** Added support for the CTLD script by ciribob and updated the JTAC Autolase
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
## Fixes

View File

@ -483,6 +483,20 @@ TRANSPORT_CAPABLE = [
Mi_26,
]
AIR_ASSAULT_CAPABLE = [
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_26,
Mi_24P,
Mi_24V,
Hercules,
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
@ -538,6 +552,8 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
elif task == FlightType.AIR_ASSAULT:
return AIR_ASSAULT_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []

View File

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

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 datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
from game.utils import feet
from .ibuilder import IBuilder
@ -30,9 +31,32 @@ class Builder(IBuilder):
builder = WaypointBuilder(self.flight, self.coalition)
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:
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(
self.flight.departure.position,
cargo.origin.position,
@ -40,6 +64,16 @@ class Builder(IBuilder):
altitude_is_agl,
)
if self.flight.client_count > 0:
# Normal Landing Waypoint
arrival = builder.land(self.flight.arrival)
else:
# The AI Needs another Stopover point to actually fly back to the original
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
# AI will end its mission there instead of flying back.
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
arrival = builder.stopover(self.flight.arrival, "LANDING")
return AirliftLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup,
@ -50,14 +84,15 @@ class Builder(IBuilder):
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(cargo.next_stop),
drop_off=drop_off,
stopover=stopover,
nav_to_home=builder.nav_path(
cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=builder.land(self.flight.arrival),
arrival=arrival,
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)
@ -69,15 +104,18 @@ class AirliftLayout(StandardLayout):
pickup: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
if self.stopover is not None:
yield self.stopover
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -139,6 +139,14 @@ class ObjectiveFinder:
"""Iterates over all active front lines in the theater."""
yield from self.game.theater.conflicts()
def air_assault_targets(self) -> Iterator[ControlPoint]:
"""Iterates over all capturable controlpoints for all active front lines"""
if not self.game.settings.plugin_option("ctld"):
# Air Assault should only be tasked with CTLD enabled
return
for front_line in self.front_lines():
yield front_line.control_point_hostile_to(self.is_player)
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.

View File

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

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

View File

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

View File

@ -47,7 +47,7 @@ from game.utils import (
if TYPE_CHECKING:
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.airsupport import AirSupport
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@ -182,6 +182,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
# Logisitcs info
# cabin_size defines how many troops can be loaded. 0 means the aircraft can not
# transport any troops. Default for helos is 10, non helos will have 0.
cabin_size: int
# If the aircraft can carry crates can_carry_crates should be set to true which
# will be set to true for helos by default
can_carry_crates: bool
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@ -281,10 +289,10 @@ class AircraftType(UnitType[Type[FlyingType]]):
return freq
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
self, flight: FlightData, mission_data: MissionData
) -> None:
if self.channel_allocator is not None:
self.channel_allocator.assign_channels_for_flight(flight, air_support)
self.channel_allocator.assign_channels_for_flight(flight, mission_data)
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
@ -387,6 +395,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
if units_data == "metric":
units = MetricUnits()
class_name = data.get("class")
unit_class = UnitClass.PLANE if class_name is None else UnitClass(class_name)
prop_overrides = data.get("default_overrides")
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path)
@ -419,5 +430,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
channel_namer=radio_config.channel_namer,
kneeboard_units=units,
utc_kneeboard=data.get("utc_kneeboard", False),
unit_class=UnitClass.PLANE,
unit_class=unit_class,
cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0),
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
)

View File

@ -62,7 +62,10 @@ class AircraftBehavior:
self.configure_runway_attack(group, flight)
elif self.task == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, flight)
elif self.task == FlightType.TRANSPORT:
elif self.task in [
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
]:
self.configure_transport(group, flight)
elif self.task == FlightType.FERRY:
self.configure_ferry(group, flight)

View File

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

View File

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

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
@ -6,9 +11,16 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class CargoStopBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
# Create a landing task, currently only for Helos!
if self.flight.is_helo:
# Calculate a landing point with a small buffer to prevent AI from landing
# directly at the static ammo depot and exploding
landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos
waypoint.add_task(Land(landing_point, duration=30))
else:
# Fixed wing will drop the cargo at the waypoint so we set a lower altitude
waypoint.alt = int(feet(10000).meters)
waypoint.alt_type = "BARO"
waypoint.action = PointAction.FlyOverPoint
return waypoint

View File

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

View File

@ -64,7 +64,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
waypoint.add_task(Tanker())
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.air_support.tankers[-1]
tanker_info = self.mission_data.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {
"Texaco": "TEX",

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

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

View File

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

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 game.ato import FlightType
from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua
from .aircraft.flightdata import FlightData
from .airsupport import AirSupport
from .missiondata import MissionData
if TYPE_CHECKING:
from game import Game
@ -29,13 +29,11 @@ class LuaGenerator:
self,
game: Game,
mission: Mission,
air_support: AirSupport,
flights: list[FlightData],
mission_data: MissionData,
) -> None:
self.game = game
self.mission = mission
self.air_support = air_support
self.flights = flights
self.mission_data = mission_data
self.plugin_scripts: list[str] = []
def generate(self) -> None:
@ -49,10 +47,20 @@ class LuaGenerator:
install_path.set_value(os.path.abspath("."))
lua_data.add_item("Airbases")
lua_data.add_item("Carriers")
carriers_object = lua_data.add_item("Carriers")
for carrier in self.mission_data.carriers:
carrier_item = carriers_object.add_item()
carrier_item.add_key_value("dcsGroupName", carrier.group_name)
carrier_item.add_key_value("unit_name", carrier.unit_name)
carrier_item.add_key_value("callsign", carrier.callsign)
carrier_item.add_key_value("radio", str(carrier.freq.mhz))
carrier_item.add_key_value(
"tacan", str(carrier.tacan.number) + carrier.tacan.band.name
)
tankers_object = lua_data.add_item("Tankers")
for tanker in self.air_support.tankers:
for tanker in self.mission_data.tankers:
tanker_item = tankers_object.add_item()
tanker_item.add_key_value("dcsGroupName", tanker.group_name)
tanker_item.add_key_value("callsign", tanker.callsign)
@ -63,14 +71,14 @@ class LuaGenerator:
)
awacs_object = lua_data.add_item("AWACs")
for awacs in self.air_support.awacs:
for awacs in self.mission_data.awacs:
awacs_item = awacs_object.add_item()
awacs_item.add_key_value("dcsGroupName", awacs.group_name)
awacs_item.add_key_value("callsign", awacs.callsign)
awacs_item.add_key_value("radio", str(awacs.freq.mhz))
jtacs_object = lua_data.add_item("JTACs")
for jtac in self.air_support.jtacs:
for jtac in self.mission_data.jtacs:
jtac_item = jtacs_object.add_item()
jtac_item.add_key_value("dcsGroupName", jtac.group_name)
jtac_item.add_key_value("callsign", jtac.callsign)
@ -81,16 +89,55 @@ class LuaGenerator:
jtac_item.add_key_value("modulation", jtac.freq.modulation.name)
logistics_object = lua_data.add_item("Logistics")
for logistic_info in self.air_support.logistics.values():
logistics_item = logistics_object.add_item()
logistics_flights = logistics_object.add_item("flights")
crates_object = logistics_object.add_item("crates")
spawnable_crates: dict[str, str] = {}
transports: list[AircraftType] = []
for logistic_info in self.mission_data.logistics:
if logistic_info.transport not in transports:
transports.append(logistic_info.transport)
coalition_color = "blue" if logistic_info.blue else "red"
logistics_item = logistics_flights.add_item()
logistics_item.add_data_array("pilot_names", logistic_info.pilot_names)
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone)
logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone)
logistics_item.add_key_value("target_zone", logistic_info.target_zone)
logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1))
logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit)
logistics_item.add_key_value(
"aircraft_type", logistic_info.transport.dcs_id
)
logistics_item.add_key_value(
"preload", "true" if logistic_info.preload else "false"
)
for cargo in logistic_info.cargo:
if cargo.unit_type not in spawnable_crates:
spawnable_crates[cargo.unit_type] = str(200 + len(spawnable_crates))
crate_weight = spawnable_crates[cargo.unit_type]
for i in range(cargo.amount):
cargo_item = crates_object.add_item()
cargo_item.add_key_value("weight", crate_weight)
cargo_item.add_key_value("coalition", coalition_color)
cargo_item.add_key_value("zone", cargo.spawn_zone)
transport_object = logistics_object.add_item("transports")
for transport in transports:
transport_item = transport_object.add_item()
transport_item.add_key_value("aircraft_type", transport.dcs_id)
transport_item.add_key_value("cabin_size", str(transport.cabin_size))
transport_item.add_key_value(
"troops", "true" if transport.cabin_size > 0 else "false"
)
transport_item.add_key_value(
"crates", "true" if transport.can_carry_crates else "false"
)
spawnable_crates_object = logistics_object.add_item("spawnable_crates")
for unit, weight in spawnable_crates.items():
crate_item = spawnable_crates_object.add_item()
crate_item.add_key_value("unit", unit)
crate_item.add_key_value("weight", weight)
target_points = lua_data.add_item("TargetPoints")
for flight in self.flights:
for flight in self.mission_data.flights:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
@ -225,6 +272,9 @@ class LuaItem(ABC):
def set_value(self, value: str) -> None:
self.value = LuaValue(None, value)
def set_data_array(self, values: list[str]) -> None:
self.value = LuaValue(None, values)
def add_data_array(self, key: str, values: list[str]) -> None:
self._add_value(LuaValue(key, values))

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
"""Combo box for selecting a flight's task type."""
from PySide2.QtWidgets import QComboBox
from game.ato.flighttype import FlightType
from game.settings.settings import Settings
from game.theater import ConflictTheater, MissionTarget
@ -8,9 +10,16 @@ from game.theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type."""
def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None:
def __init__(
self, theater: ConflictTheater, target: MissionTarget, settings: Settings
) -> None:
super().__init__()
self.theater = theater
self.target = target
for mission_type in self.target.mission_types(for_player=True):
if mission_type == FlightType.AIR_ASSAULT and not settings.plugin_option(
"ctld"
):
# Only add Air Assault if ctld plugin is enabled
continue
self.addItem(str(mission_type), userData=mission_type)

View File

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

View File

@ -50,7 +50,9 @@ class QFlightCreator(QDialog):
layout = QVBoxLayout()
self.task_selector = QFlightTypeComboBox(self.game.theater, package.target)
self.task_selector = QFlightTypeComboBox(
self.game.theater, package.target, self.game.settings
)
self.task_selector.setCurrentIndex(0)
self.task_selector.currentIndexChanged.connect(self.on_task_changed)
layout.addLayout(QLabeledWidget("Task:", self.task_selector))

View File

@ -62,6 +62,12 @@ class QFlightWaypointTab(QFrame):
self.recreate_buttons.clear()
for task in self.package.target.mission_types(for_player=True):
if task == FlightType.AIR_ASSAULT and not self.game.settings.plugin_option(
"ctld"
):
# Only add Air Assault if ctld plugin is enabled
continue
def make_closure(arg):
def closure():
return self.confirm_recreate(arg)

View File

@ -136,6 +136,26 @@ local unitPayloads = {
[4] = 16,
},
},
[6] = {
["displayName"] = "Liberation Air Assault",
["name"] = "Liberation Air Assault",
["pylons"] = {
[1] = {
["CLSID"] = "M60_SIDE_R",
["num"] = 4,
},
[2] = {
["CLSID"] = "M60_SIDE_L",
["num"] = 3,
},
},
["tasks"] = {
[1] = 32,
[2] = 31,
[3] = 35,
[4] = 16,
},
},
},
["unitType"] = "UH-1H",
}

View File

@ -5,6 +5,22 @@
-- see https://github.com/dcs-liberation/dcs_liberation
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function spawn_crates()
--- CrateSpawn script which needs to be run after CTLD was initialized (3s delay)
env.info("DCSLiberation|CTLD plugin - Spawn crates")
for _, crate in pairs(dcsLiberation.Logistics.crates) do
ctld.spawnCrateAtZone(crate.coalition, tonumber(crate.weight), crate.zone)
end
end
function preload_troops(preload_data)
--- Troop loading script which needs to be run after CTLD was initialized (5s delay)
env.info(string.format("DCSLiberation|CTLD plugin - Preloading Troops into %s", preload_data["unit"]))
ctld.preLoadTransport(preload_data["unit"], preload_data["amount"], true)
end
function toboolean(str) return str == "true" end
-- CTLD plugin - configuration
if dcsLiberation then
local ctld_pickup_smoke = "none"
@ -19,26 +35,95 @@ if dcsLiberation then
if dcsLiberation.plugins then
if dcsLiberation.plugins.ctld then
env.info("DCSLiberation|CTLD plugin - Setting Up")
--- Debug Settings
ctld.Debug = dcsLiberation.plugins.ctld.debug
ctld.Trace = dcsLiberation.plugins.ctld.debug
ctld.transportPilotNames = {}
ctld.pickupZones = {}
ctld.dropOffZones = {}
ctld.wpZones = {}
for _, item in pairs(dcsLiberation.Logistics) do
for _, pilot in pairs(item.pilot_names) do
table.insert(ctld.transportPilotNames, pilot)
-- Sling loadings settings
ctld.enableCrates = true
ctld.slingLoad = dcsLiberation.plugins.ctld.slingload
ctld.staticBugFix = not dcsLiberation.plugins.ctld.slingload
--- Special unitLoad Settings as proposed in #2174
ctld.maximumDistanceLogistic = 300
ctld.unitLoadLimits = {}
ctld.unitActions = {}
for _, transport in pairs(dcsLiberation.Logistics.transports) do
ctld.unitLoadLimits[transport.aircraft_type] = tonumber(transport.cabin_size)
ctld.unitActions[transport.aircraft_type] = { crates = toboolean(transport.crates), troops = toboolean(transport.troops) }
end
if dcsLiberation.plugins.ctld.smoke then
ctld_pickup_smoke = "blue"
ctld_dropoff_smoke = "green"
end
-- Definition of spawnable things
local ctld_troops = ctld.loadableGroups
ctld.loadableGroups = {
{ name = "Liberation Troops (2)", inf = 2 },
{ name = "Liberation Troops (4)", inf = 4 },
{ name = "Liberation Troops (6)", inf = 4, mg = 1, at = 1 },
{ name = "Liberation Troops (10)", inf = 5, mg = 2, at = 2, aa = 1 },
{ name = "Liberation Troops (12)", inf = 6, mg = 2, at = 2, aa = 2 },
{ name = "Liberation Troops (24)", inf = 12, mg = 4, at = 4, aa = 3, jtac = 1 },
}
if dcsLiberation.plugins.ctld.tailorctld then
--- remove all default CTLD spawning settings
--- so that we can tailor them for the tasked missions
ctld.enableSmokeDrop = false
ctld.enabledRadioBeaconDrop = false
ctld.spawnableCrates = {}
ctld.vehiclesForTransportRED = {}
ctld.vehiclesForTransportBLUE = {}
ctld.transportPilotNames = {}
ctld.logisticUnits = {}
ctld.pickupZones = {}
ctld.dropOffZones = {}
ctld.wpZones = {}
else
--- append the default CTLD troops
for _, troop in pairs(ctld_troops) do
table.insert(ctld.loadableGroups, troop)
end
end
--- add all carriers as pickup zone
if dcsLiberation.Carriers then
for _, carrier in pairs(dcsLiberation.Carriers) do
table.insert(ctld.pickupZones, { carrier.unit_name, ctld_pickup_smoke, -1, "yes", 0 })
end
end
--- generate mission specific spawnable crates
local spawnable_crates = {}
for _, crate in pairs(dcsLiberation.Logistics.spawnable_crates) do
table.insert(spawnable_crates, { weight = tonumber(crate.weight), desc = crate.unit, unit = crate.unit })
end
ctld.spawnableCrates["Liberation Crates"] = spawnable_crates
--- Parse the LogisticsInfo for the mission
for _, item in pairs(dcsLiberation.Logistics.flights) do
for _, pilot in pairs(item.pilot_names) do
table.insert(ctld.transportPilotNames, pilot)
if toboolean(item.preload) then
local amount = ctld.unitLoadLimits[item.aircraft_type]
timer.scheduleFunction(preload_troops, { unit = pilot, amount = amount }, timer.getTime() + 5)
end
end
if item.pickup_zone then
table.insert(ctld.pickupZones, { item.pickup_zone, ctld_pickup_smoke, -1, "yes", tonumber(item.side) })
end
if item.drop_off_zone then
table.insert(ctld.dropOffZones, { item.drop_off_zone, ctld_dropoff_smoke, tonumber(item.side) })
end
if item.target_zone then
table.insert(ctld.wpZones, { item.target_zone, "none", "yes", tonumber(item.side) })
end
if dcsLiberation.plugins.ctld.logisticunit and item.logistic_unit then
table.insert(ctld.logisticUnits, item.logistic_unit)
end
end
autolase = dcsLiberation.plugins.ctld.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
for _, jtac in pairs(dcsLiberation.JTACs) do
env.info(string.format("DCSLiberation|JTACAutolase - setting up %s", jtac.dcsUnit))
env.info(string.format("DCSLiberation|JTACAutolase - setting up %s", jtac.dcsGroupName))
if fc3LaserCode then
-- If fc3LaserCode is enabled in the plugin configuration, force the JTAC
-- laser code to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
jtac.laserCode = 1113
end
ctld.JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle', nil, { freq = jtac.radio, mod = jtac.modulation, name = jtac.dcsGroupName })
end
ctld.JTACAutoLase(jtac.dcsGroupName, jtac.laserCode, smoke, 'vehicle', nil, { freq = jtac.radio, mod = jtac.modulation, name = jtac.dcsGroupName })
end
end
if dcsLiberation.plugins.ctld.airliftcrates then
timer.scheduleFunction(spawn_crates, nil, timer.getTime() + 3)
end
end
end

View File

@ -2,11 +2,31 @@
"nameInUI": "CTLD",
"defaultValue": true,
"specificOptions": [
{
"nameInUI": "Tailor CTLD for the Liberation specific missions",
"mnemonic": "tailorctld",
"defaultValue": true
},
{
"nameInUI": "Create logistic unit in each pickup zone",
"mnemonic": "logisticunit",
"defaultValue": true
},
{
"nameInUI": "CTLD Use smoke in zones",
"mnemonic": "smoke",
"defaultValue": true
},
{
"nameInUI": "Automatically spawn crates for airlift",
"mnemonic": "airliftcrates",
"defaultValue": false
},
{
"nameInUI": "Use real sling loading",
"mnemonic": "slingload",
"defaultValue": false
},
{
"nameInUI": "JTAC Autolase",
"mnemonic": "autolase",
@ -18,13 +38,13 @@
"defaultValue": true
},
{
"nameInUI": "Use FC3 laser code (1113)",
"nameInUI": "JTAC Use FC3 laser code (1113)",
"mnemonic": "fc3LaserCode",
"defaultValue": false
},
{
"nameInUI": "CTLD Debug",
"mnemonic": "ctld-debug",
"mnemonic": "debug",
"defaultValue": false
}
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true
carrier_capable: true
cabin_size: 0 # Can not transport troops
can_carry_crates: false # Can not carry crates
description: The AH-1 Cobra was developed in the mid-1960s as an interim gunship for
the U.S. Army for use during the Vietnam War. The Cobra shared the proven transmission,
rotor system, and the T53 turboshaft engine of the UH-1 'Huey'. By June 1967, the

View File

@ -1,4 +1,7 @@
class: Helicopter
always_keeps_gun: true
cabin_size: 0 # Can not transport troops
can_carry_crates: false # Can not carry crates
description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for
a crew of two. It features a nose-mounted sensor suite for target acquisition and
night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true
lha_capable: true
cabin_size: 0 # Can not transport troops
can_carry_crates: false # Can not carry crates
description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for
a crew of two. It features a nose-mounted sensor suite for target acquisition and
night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

View File

@ -1,5 +1,8 @@
class: Helicopter
always_keeps_gun: true
lha_capable: true
cabin_size: 0 # Can not transport troops
can_carry_crates: false # Can not carry crates
description: The legendary 'Apache' is an US twin-turboshaft attack helicopter for
a crew of two. It features a nose-mounted sensor suite for target acquisition and
night vision systems. It is armed with a 30 mm (1.18 in) M230 chain gun carried

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.
price: 4
price: 6
variants:
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.
price: 4
price: 6
variants:
CH-53E: null

View File

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

View File

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

View File

@ -1,3 +1,4 @@
class: Helicopter
always_keeps_gun: true
description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\
\ reporting name: Hind) is a large helicopter gunship, attack helicopter and low-capacity\
@ -14,6 +15,8 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24;
\ cockpits. It served to a great success in the Afghanistan war, until the Taliban\
\ where equipped with Stinger Missiles from the CIA."
lha_capable: true
cabin_size: 6
can_carry_crates: true
introduced: 1981
manufacturer: Mil
origin: USSR/Russia

View File

@ -1,3 +1,4 @@
class: Helicopter
always_keeps_gun: true
description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; NATO\
\ reporting name: Hind) is a large helicopter gunship, attack helicopter and low-capacity\
@ -14,6 +15,8 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24;
\ cockpits. It served to a great success in the Afghanistan war, until the Taliban\
\ where equiped with Stinger Misseles from the CIA."
lha_capable: true
cabin_size: 6
can_carry_crates: true
introduced: 1976
manufacturer: Mil
origin: USSR/Russia

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

View File

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

View File

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

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

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\
\ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\
@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter
\ which features the famous Fenestron tail rotor."
introduced: 1977
lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale"
origin: France
price: 5

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\
\ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\
@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter
\ which features the famous Fenestron tail rotor."
introduced: 1977
lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale"
origin: France
price: 8

View File

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

View File

@ -1,3 +1,4 @@
class: Helicopter
carrier_capable: true
description: "The SA342 Gazelle is a light scout/attack and transport helicopter.\
\ It was introduced in 1968 as a result of cooperation between A\xE9rospatiale and\
@ -9,6 +10,8 @@ description: "The SA342 Gazelle is a light scout/attack and transport helicopter
\ which features the famous Fenestron tail rotor."
introduced: 1977
lha_capable: true
cabin_size: 2
can_carry_crates: false
manufacturer: "A\xE9rospatiale"
origin: France
price: 8

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
class: Helicopter
description:
The Sikorsky UH-60 Black Hawk is a four-blade, twin-engine, medium-lift utility helicopter manufactured by Sikorsky Aircraft.
The UH-60A entered service with the U.S. Army in 1979, to replace the Bell UH-1 Iroquois as the Army's tactical transport helicopter.
@ -5,6 +6,8 @@ description:
introduced: 1989
carrier_capable: true
lha_capable: true
cabin_size: 10
can_carry_crates: true
manufacturer: Sikorsky
origin: USA
price: 4