"""Runway information and selection.""" from __future__ import annotations from dataclasses import dataclass from typing import Iterator, Optional, TYPE_CHECKING from dcs.terrain.terrain import Airport, RunwayApproach from game.atcdata import AtcData from game.dcs.beacons import BeaconType, Beacons from game.radio.radios import RadioFrequency from game.radio.tacan import TacanChannel from game.utils import Heading from game.weather.conditions import Conditions if TYPE_CHECKING: from game.theater import ConflictTheater @dataclass(frozen=True) class RunwayData: airfield_name: str runway_heading: Heading runway_name: str atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None tacan_callsign: Optional[str] = None ils: Optional[RadioFrequency] = None icls: Optional[int] = None @classmethod def for_pydcs_runway_runway( cls, theater: ConflictTheater, airport: Airport, runway: RunwayApproach, ) -> RunwayData: """Creates RunwayData for the given runway of an airfield. Args: theater: The theater the airport is in. airport: The airfield the runway belongs to. runway: The pydcs runway. """ atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None tacan_callsign: Optional[str] = None ils: Optional[RadioFrequency] = None atc_radio = AtcData.from_pydcs(airport) if atc_radio is not None: atc = atc_radio.uhf for beacon_data in airport.beacons: beacon = Beacons.with_id(beacon_data.id, theater) if beacon.is_tacan: tacan = beacon.tacan_channel tacan_callsign = beacon.callsign for beacon_data in runway.beacons: beacon = Beacons.with_id(beacon_data.id, theater) if beacon.beacon_type is BeaconType.BEACON_TYPE_ILS_GLIDESLOPE: ils = beacon.frequency return cls( airfield_name=airport.name, runway_heading=Heading(runway.heading), runway_name=runway.name, atc=atc, tacan=tacan, tacan_callsign=tacan_callsign, ils=ils, ) @classmethod def for_pydcs_airport( cls, theater: ConflictTheater, airport: Airport ) -> Iterator[RunwayData]: for runway in airport.runways: yield cls.for_pydcs_runway_runway( theater, airport, runway.main, ) yield cls.for_pydcs_runway_runway( theater, airport, runway.opposite, ) class RunwayAssigner: def __init__(self, conditions: Conditions): self.conditions = conditions def angle_off_headwind(self, runway: RunwayData) -> Heading: wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction) ideal_heading = wind.opposite return runway.runway_heading.angle_between(ideal_heading) def get_preferred_runway( self, theater: ConflictTheater, airport: Airport ) -> RunwayData: """Returns the preferred runway for the given airport. Right now we're only selecting runways based on whether or not they have ILS, but we could also choose based on wind conditions, or which direction flight plans should follow. """ runways = list(RunwayData.for_pydcs_airport(theater, airport)) # Find the runway with the best headwind first. best_runways = [runways[0]] best_angle_off_headwind = self.angle_off_headwind(best_runways[0]) for runway in runways[1:]: angle_off_headwind = self.angle_off_headwind(runway) if angle_off_headwind == best_angle_off_headwind: best_runways.append(runway) elif angle_off_headwind < best_angle_off_headwind: best_runways = [runway] best_angle_off_headwind = angle_off_headwind for runway in best_runways: # But if there are multiple runways with the same heading, # prefer # and ILS capable runway. if runway.ils is not None: return runway # Otherwise the only difference between the two is the distance from # parking, which we don't know, so just pick the first one. return best_runways[0]