mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add types for distance and speed.
Not converting all at once so I can prove the concept. After that we'll want to cover all the cases where an int distance or speed is a part of the save game (I've done one of them here with `Flight.alt`) so further cleanups don't break save compat. https://github.com/Khopa/dcs_liberation/issues/558
This commit is contained in:
parent
44bc2d769b
commit
113947b9f0
@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from game.utils import nm_to_meter, feet_to_meter
|
from game.utils import Distance, feet, nm_to_meter, feet_to_meter
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -15,13 +15,13 @@ class Doctrine:
|
|||||||
strike_max_range: int
|
strike_max_range: int
|
||||||
sead_max_range: int
|
sead_max_range: int
|
||||||
|
|
||||||
rendezvous_altitude: int
|
rendezvous_altitude: Distance
|
||||||
hold_distance: int
|
hold_distance: int
|
||||||
push_distance: int
|
push_distance: int
|
||||||
join_distance: int
|
join_distance: int
|
||||||
split_distance: int
|
split_distance: int
|
||||||
ingress_egress_distance: int
|
ingress_egress_distance: int
|
||||||
ingress_altitude: int
|
ingress_altitude: Distance
|
||||||
egress_altitude: int
|
egress_altitude: int
|
||||||
|
|
||||||
min_patrol_altitude: int
|
min_patrol_altitude: int
|
||||||
@ -47,13 +47,13 @@ MODERN_DOCTRINE = Doctrine(
|
|||||||
antiship=True,
|
antiship=True,
|
||||||
strike_max_range=1500000,
|
strike_max_range=1500000,
|
||||||
sead_max_range=1500000,
|
sead_max_range=1500000,
|
||||||
rendezvous_altitude=feet_to_meter(25000),
|
rendezvous_altitude=feet(25000),
|
||||||
hold_distance=nm_to_meter(15),
|
hold_distance=nm_to_meter(15),
|
||||||
push_distance=nm_to_meter(20),
|
push_distance=nm_to_meter(20),
|
||||||
join_distance=nm_to_meter(20),
|
join_distance=nm_to_meter(20),
|
||||||
split_distance=nm_to_meter(20),
|
split_distance=nm_to_meter(20),
|
||||||
ingress_egress_distance=nm_to_meter(45),
|
ingress_egress_distance=nm_to_meter(45),
|
||||||
ingress_altitude=feet_to_meter(20000),
|
ingress_altitude=feet(20000),
|
||||||
egress_altitude=feet_to_meter(20000),
|
egress_altitude=feet_to_meter(20000),
|
||||||
min_patrol_altitude=feet_to_meter(15000),
|
min_patrol_altitude=feet_to_meter(15000),
|
||||||
max_patrol_altitude=feet_to_meter(33000),
|
max_patrol_altitude=feet_to_meter(33000),
|
||||||
@ -75,13 +75,13 @@ COLDWAR_DOCTRINE = Doctrine(
|
|||||||
antiship=True,
|
antiship=True,
|
||||||
strike_max_range=1500000,
|
strike_max_range=1500000,
|
||||||
sead_max_range=1500000,
|
sead_max_range=1500000,
|
||||||
rendezvous_altitude=feet_to_meter(22000),
|
rendezvous_altitude=feet(22000),
|
||||||
hold_distance=nm_to_meter(10),
|
hold_distance=nm_to_meter(10),
|
||||||
push_distance=nm_to_meter(10),
|
push_distance=nm_to_meter(10),
|
||||||
join_distance=nm_to_meter(10),
|
join_distance=nm_to_meter(10),
|
||||||
split_distance=nm_to_meter(10),
|
split_distance=nm_to_meter(10),
|
||||||
ingress_egress_distance=nm_to_meter(30),
|
ingress_egress_distance=nm_to_meter(30),
|
||||||
ingress_altitude=feet_to_meter(18000),
|
ingress_altitude=feet(18000),
|
||||||
egress_altitude=feet_to_meter(18000),
|
egress_altitude=feet_to_meter(18000),
|
||||||
min_patrol_altitude=feet_to_meter(10000),
|
min_patrol_altitude=feet_to_meter(10000),
|
||||||
max_patrol_altitude=feet_to_meter(24000),
|
max_patrol_altitude=feet_to_meter(24000),
|
||||||
@ -107,9 +107,9 @@ WWII_DOCTRINE = Doctrine(
|
|||||||
push_distance=nm_to_meter(5),
|
push_distance=nm_to_meter(5),
|
||||||
join_distance=nm_to_meter(5),
|
join_distance=nm_to_meter(5),
|
||||||
split_distance=nm_to_meter(5),
|
split_distance=nm_to_meter(5),
|
||||||
rendezvous_altitude=feet_to_meter(10000),
|
rendezvous_altitude=feet(10000),
|
||||||
ingress_egress_distance=nm_to_meter(7),
|
ingress_egress_distance=nm_to_meter(7),
|
||||||
ingress_altitude=feet_to_meter(8000),
|
ingress_altitude=feet(8000),
|
||||||
egress_altitude=feet_to_meter(8000),
|
egress_altitude=feet_to_meter(8000),
|
||||||
min_patrol_altitude=feet_to_meter(4000),
|
min_patrol_altitude=feet_to_meter(4000),
|
||||||
max_patrol_altitude=feet_to_meter(15000),
|
max_patrol_altitude=feet_to_meter(15000),
|
||||||
|
|||||||
239
game/utils.py
239
game/utils.py
@ -1,65 +1,18 @@
|
|||||||
def meter_to_feet(value_in_meter: float) -> int:
|
from __future__ import annotations
|
||||||
"""Converts meters to feets
|
|
||||||
|
|
||||||
:arg value_in_meter Value in meters
|
import math
|
||||||
"""
|
from dataclasses import dataclass
|
||||||
return int(3.28084 * value_in_meter)
|
from typing import Union
|
||||||
|
|
||||||
|
METERS_TO_FEET = 3.28084
|
||||||
|
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||||
|
NM_TO_METERS = 1852
|
||||||
|
METERS_TO_NM = 1 / NM_TO_METERS
|
||||||
|
|
||||||
def feet_to_meter(value_in_feet: float) -> int:
|
KNOTS_TO_KPH = 1.852
|
||||||
"""Converts feets to meters
|
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||||
|
MS_TO_KPH = 3.6
|
||||||
:arg value_in_feet Value in feets
|
KPH_TO_MS = 1 / MS_TO_KPH
|
||||||
"""
|
|
||||||
return int(value_in_feet / 3.28084)
|
|
||||||
|
|
||||||
|
|
||||||
def meter_to_nm(value_in_meter: float) -> int:
|
|
||||||
"""Converts meters to nautic miles
|
|
||||||
|
|
||||||
:arg value_in_meter Value in meters
|
|
||||||
"""
|
|
||||||
return int(value_in_meter / 1852)
|
|
||||||
|
|
||||||
|
|
||||||
def nm_to_meter(value_in_nm: float) -> int:
|
|
||||||
"""Converts nautic miles to meters
|
|
||||||
|
|
||||||
:arg value_in_nm Value in nautic miles
|
|
||||||
"""
|
|
||||||
return int(value_in_nm * 1852)
|
|
||||||
|
|
||||||
|
|
||||||
def knots_to_kph(value_in_knots: float) -> int:
|
|
||||||
"""Converts Knots to Kilometer Per Hour
|
|
||||||
|
|
||||||
:arg value_in_knots Knots
|
|
||||||
"""
|
|
||||||
return int(value_in_knots * 1.852)
|
|
||||||
|
|
||||||
|
|
||||||
def mps_to_knots(value_in_mps: float) -> int:
|
|
||||||
"""Converts Meters Per Second To Knots
|
|
||||||
|
|
||||||
:arg value_in_mps Meters Per Second
|
|
||||||
"""
|
|
||||||
return int(value_in_mps * 1.943)
|
|
||||||
|
|
||||||
|
|
||||||
def mps_to_kph(speed: float) -> int:
|
|
||||||
"""Converts meters per second to kilometers per hour.
|
|
||||||
|
|
||||||
:arg speed Speed in m/s.
|
|
||||||
"""
|
|
||||||
return int(speed * 3.6)
|
|
||||||
|
|
||||||
|
|
||||||
def kph_to_mps(speed: float) -> int:
|
|
||||||
"""Converts kilometers per hour to meters per second.
|
|
||||||
|
|
||||||
:arg speed Speed in KPH.
|
|
||||||
"""
|
|
||||||
return int(speed / 3.6)
|
|
||||||
|
|
||||||
|
|
||||||
def heading_sum(h, a) -> int:
|
def heading_sum(h, a) -> int:
|
||||||
@ -71,5 +24,171 @@ def heading_sum(h, a) -> int:
|
|||||||
else:
|
else:
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
|
||||||
def opposite_heading(h):
|
def opposite_heading(h):
|
||||||
return heading_sum(h, 180)
|
return heading_sum(h, 180)
|
||||||
|
|
||||||
|
|
||||||
|
def meter_to_feet(value: float) -> int:
|
||||||
|
return int(meters(value).feet)
|
||||||
|
|
||||||
|
|
||||||
|
def feet_to_meter(value: float) -> int:
|
||||||
|
return int(feet(value).meters)
|
||||||
|
|
||||||
|
|
||||||
|
def meter_to_nm(value: float) -> int:
|
||||||
|
return int(meters(value).nautical_miles)
|
||||||
|
|
||||||
|
|
||||||
|
def nm_to_meter(value: float) -> int:
|
||||||
|
return int(nautical_miles(value).meters)
|
||||||
|
|
||||||
|
|
||||||
|
def knots_to_kph(value: float) -> int:
|
||||||
|
return int(knots(value).kph)
|
||||||
|
|
||||||
|
|
||||||
|
def kph_to_mps(value: float) -> int:
|
||||||
|
return int(kph(value).meters_per_second)
|
||||||
|
|
||||||
|
|
||||||
|
def mps_to_kph(value: float) -> int:
|
||||||
|
return int(mps(value).kph)
|
||||||
|
|
||||||
|
|
||||||
|
def mps_to_knots(value: float) -> int:
|
||||||
|
return int(mps(value).knots)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, order=True)
|
||||||
|
class Distance:
|
||||||
|
distance_in_meters: float
|
||||||
|
|
||||||
|
@property
|
||||||
|
def feet(self) -> float:
|
||||||
|
return self.distance_in_meters * METERS_TO_FEET
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meters(self) -> float:
|
||||||
|
return self.distance_in_meters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nautical_miles(self) -> float:
|
||||||
|
return self.distance_in_meters * METERS_TO_NM
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_feet(cls, value: float) -> Distance:
|
||||||
|
return cls(value * FEET_TO_METERS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_meters(cls, value: float) -> Distance:
|
||||||
|
return cls(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_nautical_miles(cls, value: float) -> Distance:
|
||||||
|
return cls(value * NM_TO_METERS)
|
||||||
|
|
||||||
|
def __mul__(self, other: Union[float, int]) -> Distance:
|
||||||
|
return Distance(self.meters * other)
|
||||||
|
|
||||||
|
def __truediv__(self, other: Union[float, int]) -> Distance:
|
||||||
|
return Distance(self.meters / other)
|
||||||
|
|
||||||
|
def __floordiv__(self, other: Union[float, int]) -> Distance:
|
||||||
|
return Distance(self.meters // other)
|
||||||
|
|
||||||
|
|
||||||
|
def feet(value: float) -> Distance:
|
||||||
|
return Distance.from_feet(value)
|
||||||
|
|
||||||
|
|
||||||
|
def meters(value: float) -> Distance:
|
||||||
|
return Distance.from_meters(value)
|
||||||
|
|
||||||
|
|
||||||
|
def nautical_miles(value: float) -> Distance:
|
||||||
|
return Distance.from_nautical_miles(value)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, order=True)
|
||||||
|
class Speed:
|
||||||
|
speed_in_kph: float
|
||||||
|
|
||||||
|
@property
|
||||||
|
def knots(self) -> float:
|
||||||
|
return self.speed_in_kph * KPH_TO_KNOTS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kph(self) -> float:
|
||||||
|
return self.speed_in_kph
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meters_per_second(self) -> float:
|
||||||
|
return self.speed_in_kph * KPH_TO_MS
|
||||||
|
|
||||||
|
def mach(self, altitude: Distance = meters(0)) -> float:
|
||||||
|
c_sound = mach(1, altitude)
|
||||||
|
return self.speed_in_kph / c_sound.kph
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_knots(cls, value: float) -> Speed:
|
||||||
|
return cls(value * KNOTS_TO_KPH)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_kph(cls, value: float) -> Speed:
|
||||||
|
return cls(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_meters_per_second(cls, value: float) -> Speed:
|
||||||
|
return cls(value * MS_TO_KPH)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_mach(cls, value: float, altitude: Distance) -> Speed:
|
||||||
|
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||||
|
if altitude <= feet(36152):
|
||||||
|
temperature_f = 59 - 0.00356 * altitude.feet
|
||||||
|
else:
|
||||||
|
# There's another formula for altitudes over 82k feet, but we better
|
||||||
|
# not be planning waypoints that high...
|
||||||
|
temperature_f = -70
|
||||||
|
|
||||||
|
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||||
|
|
||||||
|
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||||
|
# Dependent on temperature, but varies very little (+/-0.001)
|
||||||
|
# between -40F and 180F.
|
||||||
|
heat_capacity_ratio = 1.4
|
||||||
|
|
||||||
|
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||||
|
gas_constant = 286 # m^2/s^2/K
|
||||||
|
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||||
|
return mps(c_sound) * value
|
||||||
|
|
||||||
|
def __mul__(self, other: Union[float, int]) -> Speed:
|
||||||
|
return Speed(self.kph * other)
|
||||||
|
|
||||||
|
def __truediv__(self, other: Union[float, int]) -> Speed:
|
||||||
|
return Speed(self.kph / other)
|
||||||
|
|
||||||
|
def __floordiv__(self, other: Union[float, int]) -> Speed:
|
||||||
|
return Speed(self.kph // other)
|
||||||
|
|
||||||
|
|
||||||
|
def knots(value: float) -> Speed:
|
||||||
|
return Speed.from_knots(value)
|
||||||
|
|
||||||
|
|
||||||
|
def kph(value: float) -> Speed:
|
||||||
|
return Speed.from_kph(value)
|
||||||
|
|
||||||
|
|
||||||
|
def mps(value: float) -> Speed:
|
||||||
|
return Speed.from_meters_per_second(value)
|
||||||
|
|
||||||
|
|
||||||
|
def mach(value: float, altitude: Distance) -> Speed:
|
||||||
|
return Speed.from_mach(value, altitude)
|
||||||
|
|
||||||
|
|
||||||
|
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||||
|
|||||||
@ -85,7 +85,7 @@ from game.theater.controlpoint import (
|
|||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import knots_to_kph, nm_to_meter
|
from game.utils import Distance, Speed, knots_to_kph, kph, meters, nm_to_meter
|
||||||
from gen.airsupportgen import AirSupport
|
from gen.airsupportgen import AirSupport
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
from gen.callsigns import create_group_callsign_from_unit
|
from gen.callsigns import create_group_callsign_from_unit
|
||||||
@ -110,12 +110,12 @@ from .naming import namegen
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
|
||||||
WARM_START_HELI_AIRSPEED = 120
|
WARM_START_HELI_AIRSPEED = kph(120)
|
||||||
WARM_START_HELI_ALT = 500
|
WARM_START_HELI_ALT = meters(500)
|
||||||
WARM_START_ALTITUDE = 3000
|
WARM_START_ALTITUDE = meters(3000)
|
||||||
WARM_START_AIRSPEED = 550
|
WARM_START_AIRSPEED = kph(550)
|
||||||
|
|
||||||
RTB_ALTITUDE = 800
|
RTB_ALTITUDE = meters(800)
|
||||||
RTB_DISTANCE = 5000
|
RTB_DISTANCE = 5000
|
||||||
HELI_ALT = 500
|
HELI_ALT = 500
|
||||||
|
|
||||||
@ -867,7 +867,9 @@ class AircraftConflictGenerator:
|
|||||||
start_type=self._start_type(start_type),
|
start_type=self._start_type(start_type),
|
||||||
group_size=count)
|
group_size=count)
|
||||||
|
|
||||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
def _add_radio_waypoint(self, group: FlyingGroup, position,
|
||||||
|
altitude: Distance,
|
||||||
|
airspeed: int = 600) -> MovingPoint:
|
||||||
point = group.add_waypoint(position, altitude, airspeed)
|
point = group.add_waypoint(position, altitude, airspeed)
|
||||||
point.alt_type = "RADIO"
|
point.alt_type = "RADIO"
|
||||||
return point
|
return point
|
||||||
@ -884,7 +886,8 @@ class AircraftConflictGenerator:
|
|||||||
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
||||||
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
||||||
|
|
||||||
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
|
destination_waypoint = self._add_radio_waypoint(group, position,
|
||||||
|
RTB_ALTITUDE)
|
||||||
if isinstance(at, Airport):
|
if isinstance(at, Airport):
|
||||||
group.land_at(at)
|
group.land_at(at)
|
||||||
return destination_waypoint
|
return destination_waypoint
|
||||||
@ -1380,7 +1383,8 @@ class PydcsWaypointBuilder:
|
|||||||
|
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = self.group.add_waypoint(
|
waypoint = self.group.add_waypoint(
|
||||||
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt,
|
Point(self.waypoint.x, self.waypoint.y),
|
||||||
|
self.waypoint.alt.meters,
|
||||||
name=self.mission.string(self.waypoint.name))
|
name=self.mission.string(self.waypoint.name))
|
||||||
|
|
||||||
if self.waypoint.flyover:
|
if self.waypoint.flyover:
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from dcs.unittype import FlyingType
|
|||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||||
|
from game.utils import Distance, meters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
@ -67,7 +68,7 @@ class FlightWaypointType(Enum):
|
|||||||
class FlightWaypoint:
|
class FlightWaypoint:
|
||||||
|
|
||||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||||
alt: int = 0) -> None:
|
alt: Distance = meters(0)) -> None:
|
||||||
"""Creates a flight waypoint.
|
"""Creates a flight waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from game.theater import (
|
|||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import EwrGroundObject
|
from game.theater.theatergroundobject import EwrGroundObject
|
||||||
from game.utils import nm_to_meter, meter_to_nm
|
from game.utils import Distance, meters, nm_to_meter, meter_to_nm
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
from .traveltime import GroundSpeed, TravelTime
|
from .traveltime import GroundSpeed, TravelTime
|
||||||
@ -537,7 +537,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
def target_area_waypoint(self) -> FlightWaypoint:
|
def target_area_waypoint(self) -> FlightWaypoint:
|
||||||
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
|
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
self.package.target.position.x,
|
self.package.target.position.x,
|
||||||
self.package.target.position.y, 0)
|
self.package.target.position.y,
|
||||||
|
meters(0))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def travel_time_to_target(self) -> timedelta:
|
def travel_time_to_target(self) -> timedelta:
|
||||||
@ -863,10 +864,10 @@ class FlightPlanBuilder:
|
|||||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
start, end = self.racetrack_for_objective(location)
|
start, end = self.racetrack_for_objective(location)
|
||||||
patrol_alt = random.randint(
|
patrol_alt = meters(random.randint(
|
||||||
self.doctrine.min_patrol_altitude,
|
self.doctrine.min_patrol_altitude,
|
||||||
self.doctrine.max_patrol_altitude
|
self.doctrine.max_patrol_altitude
|
||||||
)
|
))
|
||||||
|
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
start, end = builder.race_track(start, end, patrol_alt)
|
start, end = builder.race_track(start, end, patrol_alt)
|
||||||
@ -983,8 +984,8 @@ class FlightPlanBuilder:
|
|||||||
"""
|
"""
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
|
patrol_alt = meters(random.randint(self.doctrine.min_patrol_altitude,
|
||||||
self.doctrine.max_patrol_altitude)
|
self.doctrine.max_patrol_altitude))
|
||||||
|
|
||||||
# Create points
|
# Create points
|
||||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||||
|
|||||||
@ -8,7 +8,13 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game.utils import meter_to_nm
|
from game.utils import (
|
||||||
|
Distance,
|
||||||
|
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||||
|
Speed,
|
||||||
|
kph, mach, meter_to_nm,
|
||||||
|
meters,
|
||||||
|
)
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -18,7 +24,7 @@ if TYPE_CHECKING:
|
|||||||
class GroundSpeed:
|
class GroundSpeed:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
def for_flight(cls, flight: Flight, altitude: Distance) -> int:
|
||||||
if not issubclass(flight.unit_type, FlyingType):
|
if not issubclass(flight.unit_type, FlyingType):
|
||||||
raise TypeError("Flight has non-flying unit")
|
raise TypeError("Flight has non-flying unit")
|
||||||
|
|
||||||
@ -27,56 +33,20 @@ class GroundSpeed:
|
|||||||
# on fuel, but mission speed will be fast enough to keep the flight
|
# on fuel, but mission speed will be fast enough to keep the flight
|
||||||
# safer.
|
# safer.
|
||||||
|
|
||||||
c_sound_sea_level = 661.5
|
# DCS's max speed is in kph at 0 MSL.
|
||||||
|
max_speed = kph(flight.unit_type.max_speed)
|
||||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||||
max_speed = flight.unit_type.max_speed * 0.539957
|
|
||||||
if max_speed > c_sound_sea_level:
|
|
||||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||||
# account for heavily loaded jets.
|
# account for heavily loaded jets.
|
||||||
return int(cls.from_mach(0.8, altitude))
|
return int(mach(0.8, altitude).knots)
|
||||||
|
|
||||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||||
# as it can at sea level. This probably isn't great assumption, but
|
# as it can at sea level. This probably isn't great assumption, but
|
||||||
# might. be sufficient given the wiggle room. We can come up with
|
# might. be sufficient given the wiggle room. We can come up with
|
||||||
# another heuristic if needed.
|
# another heuristic if needed.
|
||||||
mach = max_speed * 0.8 / c_sound_sea_level
|
cruise_mach = max_speed.mach() * 0.8
|
||||||
return int(cls.from_mach(mach, altitude)) # knots
|
return int(mach(cruise_mach, altitude).knots)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_mach(mach: float, altitude_m: int) -> float:
|
|
||||||
"""Returns the ground speed in knots for the given mach and altitude.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mach: The mach number to convert to ground speed.
|
|
||||||
altitude_m: The altitude in meters.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ground speed corresponding to the given altitude and mach number
|
|
||||||
in knots.
|
|
||||||
"""
|
|
||||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
|
||||||
altitude_ft = altitude_m * 3.28084
|
|
||||||
if altitude_ft <= 36152:
|
|
||||||
temperature_f = 59 - 0.00356 * altitude_ft
|
|
||||||
else:
|
|
||||||
# There's another formula for altitudes over 82k feet, but we better
|
|
||||||
# not be planning waypoints that high...
|
|
||||||
temperature_f = -70
|
|
||||||
|
|
||||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
|
||||||
|
|
||||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
|
||||||
# Dependent on temperature, but varies very little (+/-0.001)
|
|
||||||
# between -40F and 180F.
|
|
||||||
heat_capacity_ratio = 1.4
|
|
||||||
|
|
||||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
|
||||||
gas_constant = 286 # m^2/s^2/K
|
|
||||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
|
||||||
# c_sound is in m/s, convert to knots.
|
|
||||||
return (c_sound * 1.944) * mach
|
|
||||||
|
|
||||||
|
|
||||||
class TravelTime:
|
class TravelTime:
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from game.theater import (
|
|||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
)
|
)
|
||||||
|
from game.utils import Distance, meters
|
||||||
from game.weather import Conditions
|
from game.weather import Conditions
|
||||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||||
|
|
||||||
@ -53,7 +54,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||||
)
|
)
|
||||||
waypoint.name = "NAV"
|
waypoint.name = "NAV"
|
||||||
waypoint.alt_type = "BARO"
|
waypoint.alt_type = "BARO"
|
||||||
@ -64,7 +67,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.TAKEOFF,
|
FlightWaypointType.TAKEOFF,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
0
|
meters(0)
|
||||||
)
|
)
|
||||||
waypoint.name = "TAKEOFF"
|
waypoint.name = "TAKEOFF"
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
@ -84,7 +87,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||||
)
|
)
|
||||||
waypoint.name = "NAV"
|
waypoint.name = "NAV"
|
||||||
waypoint.alt_type = "BARO"
|
waypoint.alt_type = "BARO"
|
||||||
@ -95,7 +100,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.LANDING_POINT,
|
FlightWaypointType.LANDING_POINT,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
0
|
meters(0)
|
||||||
)
|
)
|
||||||
waypoint.name = "LANDING"
|
waypoint.name = "LANDING"
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
@ -116,12 +121,12 @@ class WaypointBuilder:
|
|||||||
position = divert.position
|
position = divert.position
|
||||||
if isinstance(divert, OffMapSpawn):
|
if isinstance(divert, OffMapSpawn):
|
||||||
if self.is_helo:
|
if self.is_helo:
|
||||||
altitude = 500
|
altitude = meters(500)
|
||||||
else:
|
else:
|
||||||
altitude = self.doctrine.rendezvous_altitude
|
altitude = self.doctrine.rendezvous_altitude
|
||||||
altitude_type = "BARO"
|
altitude_type = "BARO"
|
||||||
else:
|
else:
|
||||||
altitude = 0
|
altitude = meters(0)
|
||||||
altitude_type = "RADIO"
|
altitude_type = "RADIO"
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
@ -142,7 +147,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "Hold"
|
waypoint.pretty_name = "Hold"
|
||||||
waypoint.description = "Wait until push time"
|
waypoint.description = "Wait until push time"
|
||||||
@ -154,7 +161,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.ingress_altitude
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "Join"
|
waypoint.pretty_name = "Join"
|
||||||
waypoint.description = "Rendezvous with package"
|
waypoint.description = "Rendezvous with package"
|
||||||
@ -166,7 +175,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.ingress_altitude
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "Split"
|
waypoint.pretty_name = "Split"
|
||||||
waypoint.description = "Depart from package"
|
waypoint.description = "Depart from package"
|
||||||
@ -179,7 +190,9 @@ class WaypointBuilder:
|
|||||||
ingress_type,
|
ingress_type,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.ingress_altitude
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||||
waypoint.description = "INGRESS on " + objective.name
|
waypoint.description = "INGRESS on " + objective.name
|
||||||
@ -193,7 +206,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.ingress_altitude
|
||||||
)
|
)
|
||||||
waypoint.pretty_name = "EGRESS from " + target.name
|
waypoint.pretty_name = "EGRESS from " + target.name
|
||||||
waypoint.description = "EGRESS from " + target.name
|
waypoint.description = "EGRESS from " + target.name
|
||||||
@ -218,7 +233,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
target.target.position.x,
|
target.target.position.x,
|
||||||
target.target.position.y,
|
target.target.position.y,
|
||||||
0
|
meters(0)
|
||||||
)
|
)
|
||||||
waypoint.description = description
|
waypoint.description = description
|
||||||
waypoint.pretty_name = description
|
waypoint.pretty_name = description
|
||||||
@ -249,7 +264,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
location.position.x,
|
location.position.x,
|
||||||
location.position.y,
|
location.position.y,
|
||||||
0
|
meters(0)
|
||||||
)
|
)
|
||||||
waypoint.description = name
|
waypoint.description = name
|
||||||
waypoint.pretty_name = name
|
waypoint.pretty_name = name
|
||||||
@ -274,7 +289,7 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.CAS,
|
FlightWaypointType.CAS,
|
||||||
position.x,
|
position.x,
|
||||||
position.y,
|
position.y,
|
||||||
500 if self.is_helo else 1000
|
meters(500) if self.is_helo else meters(1000)
|
||||||
)
|
)
|
||||||
waypoint.alt_type = "RADIO"
|
waypoint.alt_type = "RADIO"
|
||||||
waypoint.description = "Provide CAS"
|
waypoint.description = "Provide CAS"
|
||||||
@ -283,12 +298,12 @@ class WaypointBuilder:
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates a racetrack start waypoint.
|
"""Creates a racetrack start waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position: Position of the waypoint.
|
position: Position of the waypoint.
|
||||||
altitude: Altitude of the racetrack in meters.
|
altitude: Altitude of the racetrack.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.PATROL_TRACK,
|
FlightWaypointType.PATROL_TRACK,
|
||||||
@ -302,12 +317,12 @@ class WaypointBuilder:
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates a racetrack end waypoint.
|
"""Creates a racetrack end waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position: Position of the waypoint.
|
position: Position of the waypoint.
|
||||||
altitude: Altitude of the racetrack in meters.
|
altitude: Altitude of the racetrack.
|
||||||
"""
|
"""
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.PATROL,
|
FlightWaypointType.PATROL,
|
||||||
@ -321,7 +336,7 @@ class WaypointBuilder:
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def race_track(self, start: Point, end: Point,
|
def race_track(self, start: Point, end: Point,
|
||||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates two waypoint for a racetrack orbit.
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -333,7 +348,7 @@ class WaypointBuilder:
|
|||||||
self.race_track_end(end, altitude))
|
self.race_track_end(end, altitude))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
|
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates a sweep start waypoint.
|
"""Creates a sweep start waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -352,7 +367,7 @@ class WaypointBuilder:
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
|
def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates a sweep end waypoint.
|
"""Creates a sweep end waypoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -371,7 +386,7 @@ class WaypointBuilder:
|
|||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
def sweep(self, start: Point, end: Point,
|
def sweep(self, start: Point, end: Point,
|
||||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates two waypoint for a racetrack orbit.
|
"""Creates two waypoint for a racetrack orbit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -404,7 +419,9 @@ class WaypointBuilder:
|
|||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
target.position.x,
|
target.position.x,
|
||||||
target.position.y,
|
target.position.y,
|
||||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
meters(
|
||||||
|
500
|
||||||
|
) if self.is_helo else self.doctrine.ingress_altitude
|
||||||
)
|
)
|
||||||
waypoint.name = "TARGET"
|
waypoint.name = "TARGET"
|
||||||
waypoint.description = "Escort the package"
|
waypoint.description = "Escort the package"
|
||||||
|
|||||||
@ -153,7 +153,7 @@ class FlightPlanBuilder:
|
|||||||
self.rows.append([
|
self.rows.append([
|
||||||
str(waypoint.number),
|
str(waypoint.number),
|
||||||
waypoint.waypoint.pretty_name,
|
waypoint.waypoint.pretty_name,
|
||||||
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
str(int(waypoint.waypoint.alt.feet)),
|
||||||
self._waypoint_distance(waypoint.waypoint),
|
self._waypoint_distance(waypoint.waypoint),
|
||||||
self._ground_speed(waypoint.waypoint),
|
self._ground_speed(waypoint.waypoint),
|
||||||
self._format_time(waypoint.waypoint.tot),
|
self._format_time(waypoint.waypoint.tot),
|
||||||
|
|||||||
@ -441,7 +441,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
waypoint: FlightWaypoint, position: Tuple[int, int],
|
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||||
flight_plan: FlightPlan) -> None:
|
flight_plan: FlightPlan) -> None:
|
||||||
|
|
||||||
altitude = meter_to_feet(waypoint.alt)
|
altitude = int(waypoint.alt.feet)
|
||||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||||
|
|
||||||
prefix = "TOT"
|
prefix = "TOT"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class QFlightWaypointInfoBox(QGroupBox):
|
|||||||
self.flight_wpt = FlightWaypoint(0,0,0)
|
self.flight_wpt = FlightWaypoint(0,0,0)
|
||||||
self.x_position_label = QLabel(str(self.flight_wpt.x))
|
self.x_position_label = QLabel(str(self.flight_wpt.x))
|
||||||
self.y_position_label = QLabel(str(self.flight_wpt.y))
|
self.y_position_label = QLabel(str(self.flight_wpt.y))
|
||||||
self.alt_label = QLabel(str(self.flight_wpt.alt))
|
self.alt_label = QLabel(str(int(self.flight_wpt.alt.feet)))
|
||||||
self.name_label = QLabel(str(self.flight_wpt.name))
|
self.name_label = QLabel(str(self.flight_wpt.name))
|
||||||
self.desc_label = QLabel(str(self.flight_wpt.description))
|
self.desc_label = QLabel(str(self.flight_wpt.description))
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
@ -60,7 +60,7 @@ class QFlightWaypointInfoBox(QGroupBox):
|
|||||||
self.flight_wpt = FlightWaypoint(0,0,0)
|
self.flight_wpt = FlightWaypoint(0,0,0)
|
||||||
self.x_position_label.setText(str(self.flight_wpt.x))
|
self.x_position_label.setText(str(self.flight_wpt.x))
|
||||||
self.y_position_label.setText(str(self.flight_wpt.y))
|
self.y_position_label.setText(str(self.flight_wpt.y))
|
||||||
self.alt_label.setText(str(self.flight_wpt.alt))
|
self.alt_label.setText(str(int(self.flight_wpt.alt.feet)))
|
||||||
self.name_label.setText(str(self.flight_wpt.name))
|
self.name_label.setText(str(self.flight_wpt.name))
|
||||||
self.desc_label.setText(str(self.flight_wpt.description))
|
self.desc_label.setText(str(self.flight_wpt.description))
|
||||||
self.setTitle(self.flight_wpt.name)
|
self.setTitle(self.flight_wpt.name)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class QFlightWaypointList(QTableView):
|
|||||||
|
|
||||||
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
|
||||||
|
|
||||||
altitude = meter_to_feet(waypoint.alt)
|
altitude = int(waypoint.alt.feet)
|
||||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||||
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
|
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
|
||||||
altitude_item.setEditable(False)
|
altitude_item.setEditable(False)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user