mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
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:
parent
de148dbb61
commit
aa77cfe4b9
@ -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
|
||||
|
||||
|
||||
@ -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 []
|
||||
|
||||
@ -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
|
||||
|
||||
128
game/ato/flightplans/airassault.py
Normal file
128
game/ato/flightplans/airassault.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -161,6 +161,7 @@ class Package:
|
||||
FlightType.BAI,
|
||||
FlightType.DEAD,
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.AIR_ASSAULT,
|
||||
FlightType.SEAD,
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
34
game/commander/tasks/primitive/airassault.py
Normal file
34
game/commander/tasks/primitive/airassault.py
Normal 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)
|
||||
@ -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()],
|
||||
|
||||
@ -18,6 +18,7 @@ class UnitClass(Enum):
|
||||
EARLY_WARNING_RADAR = "EarlyWarningRadar"
|
||||
FORTIFICATION = "Fortification"
|
||||
FRIGATE = "Frigate"
|
||||
HELICOPTER = "Helicopter"
|
||||
HELICOPTER_CARRIER = "HelicopterCarrier"
|
||||
IFV = "IFV"
|
||||
INFANTRY = "Infantry"
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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",
|
||||
|
||||
15
game/missiongenerator/aircraft/waypoints/stopover.py
Normal file
15
game/missiongenerator/aircraft/waypoints/stopover.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
104
game/missiongenerator/logisticsgenerator.py
Normal file
104
game/missiongenerator/logisticsgenerator.py
Normal 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
|
||||
@ -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))
|
||||
|
||||
|
||||
96
game/missiongenerator/missiondata.py
Normal file
96
game/missiongenerator/missiondata.py
Normal 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)
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
],
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- CAS
|
||||
- BAI
|
||||
- Transport
|
||||
- Air Assault
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- CAS
|
||||
- BAI
|
||||
- Transport
|
||||
- Air Assault
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- Transport
|
||||
- CAS
|
||||
- BAI
|
||||
- Air Assault
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- Transport
|
||||
- CAS
|
||||
- BAI
|
||||
- Air Assault
|
||||
|
||||
@ -9,3 +9,4 @@ livery: standard
|
||||
mission_types:
|
||||
- Transport
|
||||
- Anti-ship
|
||||
- Air Assault
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- CAS
|
||||
- OCA/Aircraft
|
||||
- Transport
|
||||
- Air Assault
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- CAS
|
||||
- OCA/Aircraft
|
||||
- Transport
|
||||
- Air Assault
|
||||
|
||||
@ -10,3 +10,4 @@ mission_types:
|
||||
- CAS
|
||||
- OCA/Aircraft
|
||||
- Transport
|
||||
- Air Assault
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
class: Helicopter
|
||||
price: 4
|
||||
cabin_size: 2
|
||||
can_carry_crates: false
|
||||
variants:
|
||||
SA342Minigun: null
|
||||
kneeboard_units: "metric"
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
class: Helicopter
|
||||
price: 4
|
||||
cabin_size: 10
|
||||
can_carry_crates: true
|
||||
variants:
|
||||
UH-60A: null
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user