mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +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:
@@ -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,15 +31,48 @@ class Builder(IBuilder):
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
pickup = None
|
||||
nav_to_pickup = []
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.pickup(cargo.origin)
|
||||
nav_to_pickup = builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
stopover = None
|
||||
if self.flight.is_helo:
|
||||
# Create a pickupzone where the cargo will be spawned
|
||||
pickup_zone = MissionTarget(
|
||||
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
|
||||
)
|
||||
pickup = builder.pickup(pickup_zone)
|
||||
# If The cargo is at the departure controlpoint, the pickup waypoint should
|
||||
# only be created for client flights
|
||||
pickup.only_for_player = cargo.origin == self.flight.departure
|
||||
|
||||
# Create a dropoff zone where the cargo should be dropped
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
cargo.next_stop.position.random_point_within(1000, 200),
|
||||
)
|
||||
drop_off = builder.drop_off(drop_off_zone)
|
||||
|
||||
# Add an additional stopover point so that the flight can refuel
|
||||
stopover = builder.stopover(cargo.next_stop)
|
||||
else:
|
||||
# Fixed Wing will get stopover points for pickup and dropoff
|
||||
if cargo.origin != self.flight.departure:
|
||||
pickup = builder.stopover(cargo.origin, "PICKUP")
|
||||
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
|
||||
|
||||
nav_to_pickup = builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
cargo.origin.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
)
|
||||
|
||||
if self.flight.client_count > 0:
|
||||
# Normal Landing Waypoint
|
||||
arrival = builder.land(self.flight.arrival)
|
||||
else:
|
||||
# The AI Needs another Stopover point to actually fly back to the original
|
||||
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
|
||||
# AI will end its mission there instead of flying back.
|
||||
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
|
||||
arrival = builder.stopover(self.flight.arrival, "LANDING")
|
||||
|
||||
return AirliftLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -50,14 +84,15 @@ class Builder(IBuilder):
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
drop_off=builder.drop_off(cargo.next_stop),
|
||||
drop_off=drop_off,
|
||||
stopover=stopover,
|
||||
nav_to_home=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
altitude_is_agl,
|
||||
),
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
arrival=arrival,
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
@@ -69,15 +104,18 @@ class AirliftLayout(StandardLayout):
|
||||
pickup: FlightWaypoint | None
|
||||
nav_to_drop_off: list[FlightWaypoint]
|
||||
drop_off: FlightWaypoint
|
||||
stopover: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
if self.pickup:
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
yield from self.nav_to_drop_off
|
||||
yield self.drop_off
|
||||
if self.stopover is not None:
|
||||
yield self.stopover
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
|
||||
@@ -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,15 +588,18 @@ class PendingTransfers:
|
||||
network = self.network_for(transfer.position)
|
||||
path = network.shortest_path_between(transfer.position, transfer.destination)
|
||||
next_stop = path[0]
|
||||
if network.link_type(transfer.position, next_stop) == TransitConnection.Road:
|
||||
self.convoys.add(transfer, next_stop)
|
||||
elif (
|
||||
network.link_type(transfer.position, next_stop)
|
||||
== TransitConnection.Shipping
|
||||
):
|
||||
self.cargo_ships.add(transfer, next_stop)
|
||||
else:
|
||||
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
|
||||
if not transfer.request_airflift:
|
||||
if (
|
||||
network.link_type(transfer.position, next_stop)
|
||||
== TransitConnection.Road
|
||||
):
|
||||
return self.convoys.add(transfer, next_stop)
|
||||
elif (
|
||||
network.link_type(transfer.position, next_stop)
|
||||
== TransitConnection.Shipping
|
||||
):
|
||||
return self.cargo_ships.add(transfer, next_stop)
|
||||
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
|
||||
|
||||
def new_transfer(self, transfer: TransferOrder) -> None:
|
||||
transfer.origin.base.commit_losses(transfer.units)
|
||||
@@ -774,3 +780,13 @@ class PendingTransfers:
|
||||
self.game.coalition_for(self.player).add_procurement_request(
|
||||
AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap)
|
||||
)
|
||||
|
||||
def transfer_for_flight(self, flight: Flight) -> Optional[TransferOrder]:
|
||||
for transfer in self.pending_transfers:
|
||||
if transfer.transport is None or not isinstance(
|
||||
transfer.transport, Airlift
|
||||
):
|
||||
continue
|
||||
if transfer.transport.flight == flight:
|
||||
return transfer
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user