mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
These "flights" are only created so that we can spawn the aircraft on the ramp for OCA strikes. They shouldn't have pilots assigned.
309 lines
11 KiB
Python
309 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from enum import Enum
|
|
from typing import List, Optional, TYPE_CHECKING, Type, Union
|
|
|
|
from dcs.mapping import Point
|
|
from dcs.point import MovingPoint, PointAction
|
|
from dcs.unit import Unit
|
|
from dcs.unittype import FlyingType
|
|
|
|
from game import db
|
|
from game.squadrons import Pilot, Squadron
|
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
|
from game.utils import Distance, meters
|
|
from gen.flights.loadouts import Loadout
|
|
|
|
if TYPE_CHECKING:
|
|
from game.transfers import TransferOrder
|
|
from gen.ato import Package
|
|
from gen.flights.flightplan import FlightPlan
|
|
|
|
|
|
class FlightType(Enum):
|
|
"""Enumeration of mission types.
|
|
|
|
The value of each enumeration is the name that will be shown in the UI.
|
|
|
|
These values are persisted to the save game as well since they are a part of
|
|
each flight and thus a part of the ATO, so changing these values will break
|
|
save compat.
|
|
|
|
When adding new mission types to this list, you will also need to update:
|
|
|
|
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
|
|
plan type if necessary, though most are a subclass of StrikeFlightPlan.
|
|
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
|
|
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
|
|
and mission abort parameters (winchester, bingo, etc).
|
|
* Implementations of MissionTarget.mission_types: A mission type can only be planned
|
|
against compatible targets. The mission_types method of each target class defines
|
|
which missions may target it.
|
|
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
|
|
returns the list of compatible aircraft in order of preference.
|
|
|
|
You may also need to update:
|
|
|
|
* flight.py: Add a new waypoint type if necessary. Most mission types will need
|
|
these, as aircraft.py uses the ingress point type to specialize AI tasks, and non-
|
|
strike-like missions will need more specialized control.
|
|
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
|
|
plan the new mission type.
|
|
"""
|
|
|
|
TARCAP = "TARCAP"
|
|
BARCAP = "BARCAP"
|
|
CAS = "CAS"
|
|
INTERCEPTION = "Intercept"
|
|
STRIKE = "Strike"
|
|
ANTISHIP = "Anti-ship"
|
|
SEAD = "SEAD"
|
|
DEAD = "DEAD"
|
|
ESCORT = "Escort"
|
|
BAI = "BAI"
|
|
SWEEP = "Fighter sweep"
|
|
OCA_RUNWAY = "OCA/Runway"
|
|
OCA_AIRCRAFT = "OCA/Aircraft"
|
|
AEWC = "AEW&C"
|
|
TRANSPORT = "Transport"
|
|
SEAD_ESCORT = "SEAD Escort"
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
@classmethod
|
|
def from_name(cls, name: str) -> FlightType:
|
|
for entry in cls:
|
|
if name == entry.value:
|
|
return entry
|
|
raise KeyError(f"No FlightType with name {name}")
|
|
|
|
|
|
class FlightWaypointType(Enum):
|
|
"""Enumeration of waypoint types.
|
|
|
|
The value of the enum has no meaning but should remain stable to prevent breaking
|
|
save game compatibility.
|
|
|
|
When adding a new waypoint type, you will also need to update:
|
|
|
|
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
|
|
type unless the new waypoint type will be a parameter to an existing builder
|
|
method (such as how escort ingress waypoints work).
|
|
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
|
|
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
|
|
"""
|
|
|
|
TAKEOFF = 0 # Take off point
|
|
ASCEND_POINT = 1 # Ascension point after take off
|
|
PATROL = 2 # Patrol point
|
|
PATROL_TRACK = 3 # Patrol race track
|
|
NAV = 4 # Nav point
|
|
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
|
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
|
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
|
CAS = 8 # Should do CAS there
|
|
EGRESS = 9 # Should stop attack
|
|
DESCENT_POINT = 10 # Should start descending to pattern alt
|
|
LANDING_POINT = 11 # Should land there
|
|
TARGET_POINT = 12 # A target building or static object, position
|
|
TARGET_GROUP_LOC = 13 # A target group approximate location
|
|
TARGET_SHIP = 14 # Unused.
|
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
|
JOIN = 16
|
|
SPLIT = 17
|
|
LOITER = 18
|
|
INGRESS_ESCORT = 19
|
|
INGRESS_DEAD = 20
|
|
INGRESS_SWEEP = 21
|
|
INGRESS_BAI = 22
|
|
DIVERT = 23
|
|
INGRESS_OCA_RUNWAY = 24
|
|
INGRESS_OCA_AIRCRAFT = 25
|
|
PICKUP = 26
|
|
DROP_OFF = 27
|
|
BULLSEYE = 28
|
|
|
|
|
|
class FlightWaypoint:
|
|
def __init__(
|
|
self,
|
|
waypoint_type: FlightWaypointType,
|
|
x: float,
|
|
y: float,
|
|
alt: Distance = meters(0),
|
|
) -> None:
|
|
"""Creates a flight waypoint.
|
|
|
|
Args:
|
|
waypoint_type: The waypoint type.
|
|
x: X cooidinate of the waypoint.
|
|
y: Y coordinate of the waypoint.
|
|
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
|
changed to MSL by setting alt_type to "RADIO".
|
|
"""
|
|
self.waypoint_type = waypoint_type
|
|
self.x = x
|
|
self.y = y
|
|
self.alt = alt
|
|
self.alt_type = "BARO"
|
|
self.name = ""
|
|
# TODO: Merge with pretty_name.
|
|
# Only used in the waypoint list in the flight edit page. No sense
|
|
# having three names. A short and long form is enough.
|
|
self.description = ""
|
|
self.targets: List[Union[MissionTarget, Unit]] = []
|
|
self.obj_name = ""
|
|
self.pretty_name = ""
|
|
self.only_for_player = False
|
|
self.flyover = False
|
|
|
|
# These are set very late by the air conflict generator (part of mission
|
|
# generation). We do it late so that we don't need to propagate changes
|
|
# to waypoint times whenever the player alters the package TOT or the
|
|
# flight's offset in the UI.
|
|
self.tot: Optional[timedelta] = None
|
|
self.departure_time: Optional[timedelta] = None
|
|
|
|
@property
|
|
def position(self) -> Point:
|
|
return Point(self.x, self.y)
|
|
|
|
@classmethod
|
|
def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint":
|
|
waypoint = FlightWaypoint(
|
|
FlightWaypointType.NAV,
|
|
point.position.x,
|
|
point.position.y,
|
|
meters(point.alt),
|
|
)
|
|
waypoint.alt_type = point.alt_type
|
|
# Other actions exist... but none of them *should* be the first
|
|
# waypoint for a flight.
|
|
waypoint.waypoint_type = {
|
|
PointAction.TurningPoint: FlightWaypointType.NAV,
|
|
PointAction.FlyOverPoint: FlightWaypointType.NAV,
|
|
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
|
|
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
|
|
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
|
|
}[point.action]
|
|
if waypoint.waypoint_type == FlightWaypointType.NAV:
|
|
waypoint.name = "NAV"
|
|
waypoint.pretty_name = "Nav"
|
|
waypoint.description = "Nav"
|
|
else:
|
|
waypoint.name = "TAKEOFF"
|
|
waypoint.pretty_name = "Takeoff"
|
|
waypoint.description = "Takeoff"
|
|
waypoint.description = f"Takeoff from {from_cp.name}"
|
|
return waypoint
|
|
|
|
|
|
class Flight:
|
|
def __init__(
|
|
self,
|
|
package: Package,
|
|
country: str,
|
|
squadron: Squadron,
|
|
count: int,
|
|
flight_type: FlightType,
|
|
start_type: str,
|
|
departure: ControlPoint,
|
|
arrival: ControlPoint,
|
|
divert: Optional[ControlPoint],
|
|
custom_name: Optional[str] = None,
|
|
cargo: Optional[TransferOrder] = None,
|
|
no_pilots: bool = False,
|
|
) -> None:
|
|
self.package = package
|
|
self.country = country
|
|
self.squadron = squadron
|
|
self.pilots: List[Optional[Pilot]]
|
|
if no_pilots:
|
|
self.pilots = [None for _ in range(count)]
|
|
else:
|
|
self.pilots = [squadron.claim_available_pilot() for _ in range(count)]
|
|
self.departure = departure
|
|
self.arrival = arrival
|
|
self.divert = divert
|
|
self.flight_type = flight_type
|
|
# TODO: Replace with FlightPlan.
|
|
self.targets: List[MissionTarget] = []
|
|
self.loadout = Loadout.default_for(self)
|
|
self.start_type = start_type
|
|
self.use_custom_loadout = False
|
|
self.custom_name = custom_name
|
|
|
|
# Only used by transport missions.
|
|
self.cargo = cargo
|
|
|
|
# Will be replaced with a more appropriate FlightPlan by
|
|
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
|
# empty flight plan.
|
|
from gen.flights.flightplan import CustomFlightPlan
|
|
|
|
self.flight_plan: FlightPlan = CustomFlightPlan(
|
|
package=package, flight=self, custom_waypoints=[]
|
|
)
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
return len(self.pilots)
|
|
|
|
@property
|
|
def client_count(self) -> int:
|
|
return len([p for p in self.pilots if p is not None and p.player])
|
|
|
|
@property
|
|
def unit_type(self) -> Type[FlyingType]:
|
|
return self.squadron.aircraft
|
|
|
|
@property
|
|
def from_cp(self) -> ControlPoint:
|
|
return self.departure
|
|
|
|
@property
|
|
def points(self) -> List[FlightWaypoint]:
|
|
return self.flight_plan.waypoints[1:]
|
|
|
|
def resize(self, new_size: int) -> None:
|
|
if self.count > new_size:
|
|
self.squadron.return_pilots(
|
|
p for p in self.pilots[new_size:] if p is not None
|
|
)
|
|
self.pilots = self.pilots[:new_size]
|
|
return
|
|
self.pilots.extend(
|
|
[
|
|
self.squadron.claim_available_pilot()
|
|
for _ in range(new_size - self.count)
|
|
]
|
|
)
|
|
|
|
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
|
if pilot is not None:
|
|
self.squadron.claim_pilot(pilot)
|
|
if (current_pilot := self.pilots[index]) is not None:
|
|
self.squadron.return_pilot(current_pilot)
|
|
self.pilots[index] = pilot
|
|
|
|
@property
|
|
def missing_pilots(self) -> int:
|
|
return len([p for p in self.pilots if p is None])
|
|
|
|
def clear_roster(self) -> None:
|
|
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
|
|
|
def __repr__(self):
|
|
name = db.unit_type_name(self.unit_type)
|
|
if self.custom_name:
|
|
return f"{self.custom_name} {self.count} x {name}"
|
|
return f"[{self.flight_type}] {self.count} x {name}"
|
|
|
|
def __str__(self):
|
|
name = db.unit_get_expanded_info(self.country, self.unit_type, "name")
|
|
if self.custom_name:
|
|
return f"{self.custom_name} {self.count} x {name}"
|
|
return f"[{self.flight_type}] {self.count} x {name}"
|