mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This doesn't do anything yet, but sets up the data model handling for frozen combat. The next step is to show combat in the map view, since that will be helpful when debugging the step after that one: resolving frozen combat. This would benefit from caching the Shapely data for SAM threat zones. Right now it's generating them once per tick and the stuttering is visible at max speed. https://github.com/dcs-liberation/dcs_liberation/issues/1680
290 lines
7.6 KiB
Python
290 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
import math
|
|
import random
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from typing import TypeVar, Union
|
|
|
|
from dcs import Point
|
|
from shapely.geometry import Point as ShapelyPoint
|
|
|
|
METERS_TO_FEET = 3.28084
|
|
FEET_TO_METERS = 1 / METERS_TO_FEET
|
|
NM_TO_METERS = 1852
|
|
METERS_TO_NM = 1 / NM_TO_METERS
|
|
|
|
KNOTS_TO_KPH = 1.852
|
|
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
|
MS_TO_KPH = 3.6
|
|
KPH_TO_MS = 1 / MS_TO_KPH
|
|
|
|
INHG_TO_HPA = 33.86389
|
|
INHG_TO_MMHG = 25.400002776728
|
|
|
|
LBS_TO_KG = 0.453592
|
|
|
|
|
|
@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)
|
|
|
|
@classmethod
|
|
def inf(cls) -> Distance:
|
|
return cls.from_meters(math.inf)
|
|
|
|
def __add__(self, other: Distance) -> Distance:
|
|
return meters(self.meters + other.meters)
|
|
|
|
def __sub__(self, other: Distance) -> Distance:
|
|
return meters(self.meters - other.meters)
|
|
|
|
def __mul__(self, other: Union[float, int]) -> Distance:
|
|
return meters(self.meters * other)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __truediv__(self, other: Union[float, int]) -> Distance:
|
|
return meters(self.meters / other)
|
|
|
|
def __floordiv__(self, other: Union[float, int]) -> Distance:
|
|
return meters(self.meters // other)
|
|
|
|
def __bool__(self) -> bool:
|
|
return not math.isclose(self.meters, 0.0)
|
|
|
|
|
|
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 __add__(self, other: Speed) -> Speed:
|
|
return kph(self.kph + other.kph)
|
|
|
|
def __sub__(self, other: Speed) -> Speed:
|
|
return kph(self.kph - other.kph)
|
|
|
|
def __mul__(self, other: Union[float, int]) -> Speed:
|
|
return kph(self.kph * other)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __truediv__(self, other: Union[float, int]) -> Speed:
|
|
return kph(self.kph / other)
|
|
|
|
def __floordiv__(self, other: Union[float, int]) -> Speed:
|
|
return kph(self.kph // other)
|
|
|
|
def __bool__(self) -> bool:
|
|
return not math.isclose(self.kph, 0.0)
|
|
|
|
|
|
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)
|
|
|
|
|
|
@dataclass(frozen=True, order=True)
|
|
class Heading:
|
|
heading_in_degrees: int
|
|
|
|
@property
|
|
def degrees(self) -> int:
|
|
return Heading.reduce_angle(self.heading_in_degrees)
|
|
|
|
@property
|
|
def radians(self) -> float:
|
|
return math.radians(Heading.reduce_angle(self.heading_in_degrees))
|
|
|
|
@property
|
|
def opposite(self) -> Heading:
|
|
return self + Heading.from_degrees(180)
|
|
|
|
@property
|
|
def right(self) -> Heading:
|
|
return self + Heading.from_degrees(90)
|
|
|
|
@property
|
|
def left(self) -> Heading:
|
|
return self - Heading.from_degrees(90)
|
|
|
|
def angle_between(self, other: Heading) -> Heading:
|
|
angle_between = abs(self.degrees - other.degrees)
|
|
if angle_between > 180:
|
|
angle_between = 360 - angle_between
|
|
return Heading.from_degrees(angle_between)
|
|
|
|
@staticmethod
|
|
def reduce_angle(angle: int) -> int:
|
|
return angle % 360
|
|
|
|
@classmethod
|
|
def from_degrees(cls, angle: Union[int, float]) -> Heading:
|
|
return cls(Heading.reduce_angle(round(angle)))
|
|
|
|
@classmethod
|
|
def from_radians(cls, angle: Union[int, float]) -> Heading:
|
|
deg = round(math.degrees(angle))
|
|
return cls(Heading.reduce_angle(deg))
|
|
|
|
@classmethod
|
|
def random(cls, min_angle: int = 0, max_angle: int = 359) -> Heading:
|
|
return Heading.from_degrees(random.randint(min_angle, max_angle))
|
|
|
|
def __add__(self, other: Heading) -> Heading:
|
|
return Heading.from_degrees(self.degrees + other.degrees)
|
|
|
|
def __sub__(self, other: Heading) -> Heading:
|
|
return Heading.from_degrees(self.degrees - other.degrees)
|
|
|
|
|
|
@dataclass(frozen=True, order=True)
|
|
class Pressure:
|
|
pressure_in_inches_hg: float
|
|
|
|
@property
|
|
def inches_hg(self) -> float:
|
|
return self.pressure_in_inches_hg
|
|
|
|
@property
|
|
def mm_hg(self) -> float:
|
|
return self.pressure_in_inches_hg * INHG_TO_MMHG
|
|
|
|
@property
|
|
def hecto_pascals(self) -> float:
|
|
return self.pressure_in_inches_hg * INHG_TO_HPA
|
|
|
|
|
|
def inches_hg(value: float) -> Pressure:
|
|
return Pressure(value)
|
|
|
|
|
|
PairwiseT = TypeVar("PairwiseT")
|
|
|
|
|
|
def pairwise(iterable: Iterable[PairwiseT]) -> Iterable[tuple[PairwiseT, PairwiseT]]:
|
|
"""
|
|
itertools recipe
|
|
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
|
"""
|
|
a, b = itertools.tee(iterable)
|
|
next(b, None)
|
|
return zip(a, b)
|
|
|
|
|
|
def interpolate(value1: float, value2: float, factor: float, clamp: bool) -> float:
|
|
"""Inerpolate between two values, factor 0-1"""
|
|
interpolated = value1 + (value2 - value1) * factor
|
|
|
|
if clamp:
|
|
bigger_value = max(value1, value2)
|
|
smaller_value = min(value1, value2)
|
|
return min(bigger_value, max(smaller_value, interpolated))
|
|
else:
|
|
return interpolated
|
|
|
|
|
|
def dcs_to_shapely_point(point: Point) -> ShapelyPoint:
|
|
return ShapelyPoint(point.x, point.y)
|