From 2ac818dcddc26dfbd37fe98fbe9acf71ce18cd7e Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 19 Dec 2020 21:18:18 -0800 Subject: [PATCH] Convert to new unit APIs, remove old APIs. There are probably plenty of raw ints around that never used the old conversion APIs, but we'll just need to fix those when we see them. Fixes https://github.com/Khopa/dcs_liberation/issues/558 --- game/data/doctrine.py | 123 ++++++++---------- game/procurement.py | 5 +- game/theater/conflicttheater.py | 8 +- game/utils.py | 56 +++----- game/weather.py | 5 +- gen/aircraft.py | 27 ++-- gen/ato.py | 3 +- gen/environmentgen.py | 2 +- gen/flights/ai_flight_planner.py | 38 ++---- gen/flights/closestairfields.py | 5 +- gen/flights/flight.py | 2 +- gen/flights/flightplan.py | 92 +++++++------ gen/flights/traveltime.py | 18 +-- gen/groundobjectsgen.py | 8 +- gen/kneeboard.py | 19 +-- qt_ui/widgets/QConditionsWidget.py | 39 +++--- qt_ui/widgets/map/QLiberationMap.py | 26 ++-- .../flight/waypoints/QFlightWaypointList.py | 1 - 18 files changed, 224 insertions(+), 253 deletions(-) diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 2b385f2c..939b60cf 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from datetime import timedelta -from game.utils import Distance, feet, nm_to_meter, feet_to_meter +from game.utils import Distance, feet, nautical_miles @dataclass(frozen=True) @@ -12,31 +12,28 @@ class Doctrine: strike: bool antiship: bool - strike_max_range: int - sead_max_range: int - rendezvous_altitude: Distance - hold_distance: int - push_distance: int - join_distance: int - split_distance: int - ingress_egress_distance: int + hold_distance: Distance + push_distance: Distance + join_distance: Distance + split_distance: Distance + ingress_egress_distance: Distance ingress_altitude: Distance - egress_altitude: int + egress_altitude: Distance - min_patrol_altitude: int - max_patrol_altitude: int - pattern_altitude: int + min_patrol_altitude: Distance + max_patrol_altitude: Distance + pattern_altitude: Distance cap_duration: timedelta - cap_min_track_length: int - cap_max_track_length: int - cap_min_distance_from_cp: int - cap_max_distance_from_cp: int + cap_min_track_length: Distance + cap_max_track_length: Distance + cap_min_distance_from_cp: Distance + cap_max_distance_from_cp: Distance cas_duration: timedelta - sweep_distance: int + sweep_distance: Distance MODERN_DOCTRINE = Doctrine( @@ -45,26 +42,24 @@ MODERN_DOCTRINE = Doctrine( sead=True, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, rendezvous_altitude=feet(25000), - hold_distance=nm_to_meter(15), - push_distance=nm_to_meter(20), - join_distance=nm_to_meter(20), - split_distance=nm_to_meter(20), - ingress_egress_distance=nm_to_meter(45), + hold_distance=nautical_miles(15), + push_distance=nautical_miles(20), + join_distance=nautical_miles(20), + split_distance=nautical_miles(20), + ingress_egress_distance=nautical_miles(45), ingress_altitude=feet(20000), - egress_altitude=feet_to_meter(20000), - min_patrol_altitude=feet_to_meter(15000), - max_patrol_altitude=feet_to_meter(33000), - pattern_altitude=feet_to_meter(5000), + egress_altitude=feet(20000), + min_patrol_altitude=feet(15000), + max_patrol_altitude=feet(33000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(15), - cap_max_track_length=nm_to_meter(40), - cap_min_distance_from_cp=nm_to_meter(10), - cap_max_distance_from_cp=nm_to_meter(40), + cap_min_track_length=nautical_miles(15), + cap_max_track_length=nautical_miles(40), + cap_min_distance_from_cp=nautical_miles(10), + cap_max_distance_from_cp=nautical_miles(40), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(60), + sweep_distance=nautical_miles(60), ) COLDWAR_DOCTRINE = Doctrine( @@ -73,26 +68,24 @@ COLDWAR_DOCTRINE = Doctrine( sead=True, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, rendezvous_altitude=feet(22000), - hold_distance=nm_to_meter(10), - push_distance=nm_to_meter(10), - join_distance=nm_to_meter(10), - split_distance=nm_to_meter(10), - ingress_egress_distance=nm_to_meter(30), + hold_distance=nautical_miles(10), + push_distance=nautical_miles(10), + join_distance=nautical_miles(10), + split_distance=nautical_miles(10), + ingress_egress_distance=nautical_miles(30), ingress_altitude=feet(18000), - egress_altitude=feet_to_meter(18000), - min_patrol_altitude=feet_to_meter(10000), - max_patrol_altitude=feet_to_meter(24000), - pattern_altitude=feet_to_meter(5000), + egress_altitude=feet(18000), + min_patrol_altitude=feet(10000), + max_patrol_altitude=feet(24000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(12), - cap_max_track_length=nm_to_meter(24), - cap_min_distance_from_cp=nm_to_meter(8), - cap_max_distance_from_cp=nm_to_meter(25), + cap_min_track_length=nautical_miles(12), + cap_max_track_length=nautical_miles(24), + cap_min_distance_from_cp=nautical_miles(8), + cap_max_distance_from_cp=nautical_miles(25), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(40), + sweep_distance=nautical_miles(40), ) WWII_DOCTRINE = Doctrine( @@ -101,24 +94,22 @@ WWII_DOCTRINE = Doctrine( sead=False, strike=True, antiship=True, - strike_max_range=1500000, - sead_max_range=1500000, - hold_distance=nm_to_meter(5), - push_distance=nm_to_meter(5), - join_distance=nm_to_meter(5), - split_distance=nm_to_meter(5), + hold_distance=nautical_miles(5), + push_distance=nautical_miles(5), + join_distance=nautical_miles(5), + split_distance=nautical_miles(5), rendezvous_altitude=feet(10000), - ingress_egress_distance=nm_to_meter(7), + ingress_egress_distance=nautical_miles(7), ingress_altitude=feet(8000), - egress_altitude=feet_to_meter(8000), - min_patrol_altitude=feet_to_meter(4000), - max_patrol_altitude=feet_to_meter(15000), - pattern_altitude=feet_to_meter(5000), + egress_altitude=feet(8000), + min_patrol_altitude=feet(4000), + max_patrol_altitude=feet(15000), + pattern_altitude=feet(5000), cap_duration=timedelta(minutes=30), - cap_min_track_length=nm_to_meter(8), - cap_max_track_length=nm_to_meter(18), - cap_min_distance_from_cp=nm_to_meter(0), - cap_max_distance_from_cp=nm_to_meter(5), + cap_min_track_length=nautical_miles(8), + cap_max_track_length=nautical_miles(18), + cap_min_distance_from_cp=nautical_miles(0), + cap_max_distance_from_cp=nautical_miles(5), cas_duration=timedelta(minutes=30), - sweep_distance=nm_to_meter(10), + sweep_distance=nautical_miles(10), ) diff --git a/game/procurement.py b/game/procurement.py index 6852c534..e528df01 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -6,11 +6,12 @@ import random from typing import Iterator, List, Optional, TYPE_CHECKING, Type from dcs.task import CAP, CAS -from dcs.unittype import FlyingType, UnitType, VehicleType +from dcs.unittype import FlyingType, VehicleType from game import db from game.factions.faction import Faction from game.theater import ControlPoint, MissionTarget +from game.utils import Distance from gen.flights.ai_flight_planner_db import ( capable_aircraft_for_task, preferred_aircraft_for_task, @@ -25,7 +26,7 @@ if TYPE_CHECKING: @dataclass(frozen=True) class AircraftProcurementRequest: near: MissionTarget - range: int + range: Distance task_capability: FlightType number: int diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 4e180e7a..76cb0d85 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -55,7 +55,7 @@ from .controlpoint import ( Fob, ) from .landmap import Landmap, load_landmap, poly_contains -from ..utils import nm_to_meter +from ..utils import Distance, meters, nautical_miles Numeric = Union[int, float] @@ -115,7 +115,7 @@ class MizCampaignLoader: AirDefence.SAM_SA_3_S_125_LN_5P73.id, } - BASE_DEFENSE_RADIUS = nm_to_meter(2) + BASE_DEFENSE_RADIUS = nautical_miles(2) def __init__(self, miz: Path, theater: ConflictTheater) -> None: self.theater = theater @@ -317,9 +317,9 @@ class MizCampaignLoader: self.control_points[origin.id]) return front_lines - def objective_info(self, group: Group) -> Tuple[ControlPoint, int]: + def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: closest = self.theater.closest_control_point(group.position) - distance = closest.position.distance_to_point(group.position) + distance = meters(closest.position.distance_to_point(group.position)) return closest, distance def add_preset_locations(self) -> None: diff --git a/game/utils.py b/game/utils.py index 37ec4eae..cd76ec56 100644 --- a/game/utils.py +++ b/game/utils.py @@ -29,38 +29,6 @@ def opposite_heading(h): 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 @@ -89,14 +57,20 @@ class Distance: def from_nautical_miles(cls, value: float) -> Distance: return cls(value * NM_TO_METERS) + 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 Distance(self.meters * other) + return meters(self.meters * other) def __truediv__(self, other: Union[float, int]) -> Distance: - return Distance(self.meters / other) + return meters(self.meters / other) def __floordiv__(self, other: Union[float, int]) -> Distance: - return Distance(self.meters // other) + return meters(self.meters // other) def feet(value: float) -> Distance: @@ -165,14 +139,20 @@ class Speed: 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 Speed(self.kph * other) + return kph(self.kph * other) def __truediv__(self, other: Union[float, int]) -> Speed: - return Speed(self.kph / other) + return kph(self.kph / other) def __floordiv__(self, other: Union[float, int]) -> Speed: - return Speed(self.kph // other) + return kph(self.kph // other) def knots(value: float) -> Speed: diff --git a/game/weather.py b/game/weather.py index 34b19e2d..ae9a6fd2 100644 --- a/game/weather.py +++ b/game/weather.py @@ -10,6 +10,7 @@ from typing import Optional, TYPE_CHECKING from dcs.weather import Weather as PydcsWeather, Wind from game.settings import Settings +from game.utils import Distance, meters if TYPE_CHECKING: from game.theater import ConflictTheater @@ -39,7 +40,7 @@ class Clouds: @dataclass(frozen=True) class Fog: - visibility: int + visibility: Distance thickness: int @@ -56,7 +57,7 @@ class Weather: if random.randrange(5) != 0: return None return Fog( - visibility=random.randint(2500, 5000), + visibility=meters(random.randint(2500, 5000)), thickness=random.randint(100, 500) ) diff --git a/gen/aircraft.py b/gen/aircraft.py index fa3c9a9c..5e89b575 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -85,7 +85,7 @@ from game.theater.controlpoint import ( ) from game.theater.theatergroundobject import TheaterGroundObject from game.unitmap import UnitMap -from game.utils import Distance, Speed, knots_to_kph, kph, meters, nm_to_meter +from game.utils import Distance, meters, nautical_miles from gen.airsupportgen import AirSupport from gen.ato import AirTaskingOrder, Package from gen.callsigns import create_group_callsign_from_unit @@ -110,10 +110,8 @@ from .naming import namegen if TYPE_CHECKING: from game import Game -WARM_START_HELI_AIRSPEED = kph(120) WARM_START_HELI_ALT = meters(500) WARM_START_ALTITUDE = meters(3000) -WARM_START_AIRSPEED = kph(550) RTB_ALTITUDE = meters(800) RTB_DISTANCE = 5000 @@ -832,11 +830,13 @@ class AircraftConflictGenerator: else: alt = WARM_START_ALTITUDE - speed = knots_to_kph(GroundSpeed.for_flight(flight, alt)) + speed = GroundSpeed.for_flight(flight, alt) pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000)) - logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed)) + logging.info( + "airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, + alt, int(speed.kph))) group = self.m.flight_group( country=side, name=name, @@ -844,7 +844,7 @@ class AircraftConflictGenerator: airport=None, position=pos, altitude=alt, - speed=speed, + speed=speed.kph, maintask=None, group_size=flight.count) @@ -1515,7 +1515,7 @@ class CasIngressBuilder(PydcsWaypointBuilder): logging.error( "No CAS waypoint found. Falling back to search and engage") waypoint.add_task(EngageTargets( - max_distance=nm_to_meter(10), + max_distance=int(nautical_miles(10).meters), targets=[ Targets.All.GroundUnits.GroundVehicles, Targets.All.GroundUnits.AirDefence.AAA, @@ -1564,7 +1564,7 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder): position=target.position, # Al Dhafra is 4 nm across at most. Add a little wiggle room in case # the airport position from DCS is not centered. - radius=nm_to_meter(3), + radius=int(nautical_miles(3).meters), targets=[Targets.All.Air] ) task.params["attackQtyLimit"] = False @@ -1604,7 +1604,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder): if tgroup is not None: waypoint.add_task(EngageTargetsInZone( position=tgroup.position, - radius=nm_to_meter(30), + radius=int(nautical_miles(30).meters), targets=[ Targets.All.GroundUnits.AirDefence, ]) @@ -1686,7 +1686,7 @@ class SweepIngressBuilder(PydcsWaypointBuilder): return waypoint waypoint.tasks.append(EngageTargets( - max_distance=nm_to_meter(50), + max_distance=int(nautical_miles(50).meters), targets=[Targets.All.Air.Planes.Fighters])) return waypoint @@ -1729,7 +1729,7 @@ class JoinPointBuilder(PydcsWaypointBuilder): # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted waypoint.add_task(ControlledTask(EngageTargets( # TODO: From doctrine. - max_distance=nm_to_meter(30), + max_distance=int(nautical_miles(30).meters), targets=[Targets.All.Air.Planes.Fighters] ))) @@ -1769,8 +1769,9 @@ class RaceTrackBuilder(PydcsWaypointBuilder): # later. cap_types = {FlightType.BARCAP, FlightType.TARCAP} if self.flight.flight_type in cap_types: - waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50), - targets=[Targets.All.Air])) + waypoint.tasks.append( + EngageTargets(max_distance=int(nautical_miles(50).meters), + targets=[Targets.All.Air])) racetrack = ControlledTask(OrbitAction( altitude=waypoint.alt, diff --git a/gen/ato.py b/gen/ato.py index ab104bab..cfca4584 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -17,6 +17,7 @@ from typing import Dict, List, Optional from dcs.mapping import Point from game.theater.missiontarget import MissionTarget +from game.utils import Speed from .flights.flight import Flight, FlightType from .flights.flightplan import FormationFlightPlan @@ -59,7 +60,7 @@ class Package: waypoints: Optional[PackageWaypoints] = field(default=None) @property - def formation_speed(self) -> Optional[int]: + def formation_speed(self) -> Optional[Speed]: """The speed of the package when in formation. If none of the flights in the package will join a formation, this diff --git a/gen/environmentgen.py b/gen/environmentgen.py index 7712cea5..9d645a1d 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -21,7 +21,7 @@ class EnvironmentGenerator: def set_fog(self, fog: Optional[Fog]) -> None: if fog is None: return - self.mission.weather.fog_visibility = fog.visibility + self.mission.weather.fog_visibility = fog.visibility.meters self.mission.weather.fog_thickness = fog.thickness def set_wind(self, wind: WindConditions) -> None: diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 5d6ff05f..e2de9947 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -36,22 +36,12 @@ from game.theater.theatergroundobject import ( EwrGroundObject, NavalGroundObject, VehicleGroupGroundObject, ) -from game.utils import nm_to_meter +from game.utils import Distance, nautical_miles, nautical_miles from gen import Conflict from gen.ato import Package from gen.flights.ai_flight_planner_db import ( - ANTISHIP_CAPABLE, - ANTISHIP_PREFERRED, - CAP_CAPABLE, - CAP_PREFERRED, - CAS_CAPABLE, - CAS_PREFERRED, - RUNWAY_ATTACK_CAPABLE, - RUNWAY_ATTACK_PREFERRED, - SEAD_CAPABLE, - SEAD_PREFERRED, - STRIKE_CAPABLE, - STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task, + capable_aircraft_for_task, + preferred_aircraft_for_task, ) from gen.flights.closestairfields import ( ClosestAirfields, @@ -85,7 +75,7 @@ class ProposedFlight: num_aircraft: int #: The maximum distance between the objective and the departure airfield. - max_distance: int + max_distance: Distance def __str__(self) -> str: return f"{self.task} {self.num_aircraft} ship" @@ -212,7 +202,7 @@ class PackageBuilder: def find_divert_field(self, aircraft: FlyingType, arrival: ControlPoint) -> Optional[ControlPoint]: - divert_limit = nm_to_meter(150) + divert_limit = nautical_miles(150) for airfield in self.closest_airfields.airfields_within(divert_limit): if airfield.captured != self.is_player: continue @@ -241,8 +231,8 @@ class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" # TODO: Merge into doctrine. - AIRFIELD_THREAT_RANGE = nm_to_meter(150) - SAM_THREAT_RANGE = nm_to_meter(100) + AIRFIELD_THREAT_RANGE = nautical_miles(150) + SAM_THREAT_RANGE = nautical_miles(100) def __init__(self, game: Game, is_player: bool) -> None: self.game = game @@ -467,13 +457,13 @@ class CoalitionMissionPlanner: """ # TODO: Merge into doctrine, also limit by aircraft. - MAX_CAP_RANGE = nm_to_meter(100) - MAX_CAS_RANGE = nm_to_meter(50) - MAX_ANTISHIP_RANGE = nm_to_meter(150) - MAX_BAI_RANGE = nm_to_meter(150) - MAX_OCA_RANGE = nm_to_meter(150) - MAX_SEAD_RANGE = nm_to_meter(150) - MAX_STRIKE_RANGE = nm_to_meter(150) + MAX_CAP_RANGE = nautical_miles(100) + MAX_CAS_RANGE = nautical_miles(50) + MAX_ANTISHIP_RANGE = nautical_miles(150) + MAX_BAI_RANGE = nautical_miles(150) + MAX_OCA_RANGE = nautical_miles(150) + MAX_SEAD_RANGE = nautical_miles(150) + MAX_STRIKE_RANGE = nautical_miles(150) def __init__(self, game: Game, is_player: bool) -> None: self.game = game diff --git a/gen/flights/closestairfields.py b/gen/flights/closestairfields.py index 5bba28db..0708d5c2 100644 --- a/gen/flights/closestairfields.py +++ b/gen/flights/closestairfields.py @@ -2,6 +2,7 @@ from typing import Dict, Iterator, List, Optional from game.theater import ConflictTheater, ControlPoint, MissionTarget +from game.utils import Distance class ClosestAirfields: @@ -14,14 +15,14 @@ class ClosestAirfields: all_control_points, key=lambda c: self.target.distance_to(c) ) - def airfields_within(self, meters: int) -> Iterator[ControlPoint]: + def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]: """Iterates over all airfields within the given range of the target. Note that this iterates over *all* airfields, not just friendly airfields. """ for cp in self.closest_airfields: - if cp.distance_to(self.target) < meters: + if cp.distance_to(self.target) < distance.meters: yield cp else: break diff --git a/gen/flights/flight.py b/gen/flights/flight.py index b1768d54..36e5d720 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -106,7 +106,7 @@ class FlightWaypoint: def from_pydcs(cls, point: MovingPoint, from_cp: ControlPoint) -> "FlightWaypoint": waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x, - point.position.y, point.alt) + 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. diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 6e809a9e..a984fc76 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -28,7 +28,7 @@ from game.theater import ( TheaterGroundObject, ) from game.theater.theatergroundobject import EwrGroundObject -from game.utils import Distance, meters, nm_to_meter, meter_to_nm +from game.utils import Distance, Speed, meters, nautical_miles from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime @@ -86,7 +86,7 @@ class FlightPlan: return zip(self.waypoints[:last_index], self.waypoints[1:last_index]) def best_speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: """Desired ground speed between points a and b.""" factor = 1.0 if b.waypoint_type == FlightWaypointType.ASCEND_POINT: @@ -105,11 +105,10 @@ class FlightPlan: # We don't have an exact heightmap, but we should probably be performing # *some* adjustment for NTTR since the minimum altitude of the map is # near 2000 ft MSL. - return int( - GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor) + return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: return self.best_speed_between_waypoints(a, b) @property @@ -126,16 +125,17 @@ class FlightPlan: def bingo_fuel(self) -> int: """Bingo fuel value for the FlightPlan """ - distance_to_arrival = meter_to_nm(self.max_distance_from(self.flight.arrival)) + distance_to_arrival = self.max_distance_from(self.flight.arrival) - bingo = 1000 # Minimum Emergency Fuel - bingo += 500 # Visual Traffic - bingo += 15 * distance_to_arrival + bingo = 1000.0 # Minimum Emergency Fuel + bingo += 500 # Visual Traffic + bingo += 15 * distance_to_arrival.nautical_miles # TODO: Per aircraft tweaks. if self.flight.divert is not None: - bingo += 10 * meter_to_nm(self.max_distance_from(self.flight.divert)) + max_divert_distance = self.max_distance_from(self.flight.divert) + bingo += 10 * max_divert_distance.nautical_miles return round(bingo / 100) * 100 @@ -145,13 +145,14 @@ class FlightPlan: """ return self.bingo_fuel + 1000 - def max_distance_from(self, cp: ControlPoint) -> int: + def max_distance_from(self, cp: ControlPoint) -> Distance: """Returns the farthest waypoint of the flight plan from a ControlPoint. :arg cp The ControlPoint to measure distance from. """ if not self.waypoints: - return 0 - return max([cp.position.distance_to_point(w.position) for w in self.waypoints]) + return meters(0) + return max([meters(cp.position.distance_to_point(w.position)) for w in + self.waypoints]) @property def tot_offset(self) -> timedelta: @@ -303,7 +304,7 @@ class FormationFlightPlan(LoiterFlightPlan): return self.split @cached_property - def best_flight_formation_speed(self) -> int: + def best_flight_formation_speed(self) -> Speed: """The best speed this flight is capable at all formation waypoints. To ease coordination with other flights, we aim to have a single mission @@ -319,7 +320,7 @@ class FormationFlightPlan(LoiterFlightPlan): return min(speeds) def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: if b in self.package_speed_waypoints: # Should be impossible, as any package with at least one # FormationFlightPlan flight needs a formation speed. @@ -519,7 +520,7 @@ class StrikeFlightPlan(FormationFlightPlan): } | set(self.targets) def speed_between_waypoints(self, a: FlightWaypoint, - b: FlightWaypoint) -> int: + b: FlightWaypoint) -> Speed: # FlightWaypoint is only comparable by identity, so adding # target_area_waypoint to package_speed_waypoints is useless. if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC: @@ -563,7 +564,7 @@ class StrikeFlightPlan(FormationFlightPlan): return total @property - def mission_speed(self) -> int: + def mission_speed(self) -> Speed: return GroundSpeed.for_flight(self.flight, self.ingress.alt) @property @@ -865,8 +866,8 @@ class FlightPlanBuilder: start, end = self.racetrack_for_objective(location) patrol_alt = meters(random.randint( - self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude + int(self.doctrine.min_patrol_altitude.meters), + int(self.doctrine.max_patrol_altitude.meters) )) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) @@ -893,7 +894,7 @@ class FlightPlanBuilder: heading = self._heading_to_package_airfield(target) start = target.point_from_heading(heading, - -self.doctrine.sweep_distance) + -self.doctrine.sweep_distance.meters) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) start, end = builder.sweep(start, target, @@ -930,10 +931,11 @@ class FlightPlanBuilder: closest_airfield.position ) - min_distance_from_enemy = nm_to_meter(20) - distance_to_airfield = int(closest_airfield.position.distance_to_point( - self.package.target.position - )) + min_distance_from_enemy = nautical_miles(20) + distance_to_airfield = meters( + closest_airfield.position.distance_to_point( + self.package.target.position + )) distance_to_no_fly = distance_to_airfield - min_distance_from_enemy min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, distance_to_no_fly) @@ -942,11 +944,12 @@ class FlightPlanBuilder: end = location.position.point_from_heading( heading, - random.randint(min_cap_distance, max_cap_distance) + random.randint(int(min_cap_distance.meters), + int(max_cap_distance.meters)) ) diameter = random.randint( - self.doctrine.cap_min_track_length, - self.doctrine.cap_max_track_length + int(self.doctrine.cap_min_track_length.meters), + int(self.doctrine.cap_max_track_length.meters) ) start = end.point_from_heading(heading - 180, diameter) return start, end @@ -961,7 +964,8 @@ class FlightPlanBuilder: ) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( - heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15)) + heading - 90, random.randint(int(nautical_miles(6).meters), + int(nautical_miles(15).meters)) ) combat_width = distance / 2 @@ -984,8 +988,9 @@ class FlightPlanBuilder: """ location = self.package.target - patrol_alt = meters(random.randint(self.doctrine.min_patrol_altitude, - self.doctrine.max_patrol_altitude)) + patrol_alt = meters( + random.randint(int(self.doctrine.min_patrol_altitude.meters), + int(self.doctrine.max_patrol_altitude.meters))) # Create points builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) @@ -1189,12 +1194,13 @@ class FlightPlanBuilder: # point, plan the hold point such that it retreats from the origin # airfield. return join.point_from_heading(target.heading_between_point(origin), - self.doctrine.push_distance) + self.doctrine.push_distance.meters) heading_to_join = origin.heading_between_point(join) - hold_point = origin.point_from_heading(heading_to_join, - self.doctrine.push_distance) - if hold_point.distance_to_point(join) >= self.doctrine.push_distance: + hold_point = origin.point_from_heading( + heading_to_join, self.doctrine.push_distance.meters) + hold_distance = meters(hold_point.distance_to_point(join)) + if hold_distance >= self.doctrine.push_distance: # Hold point is between the origin airfield and the join point and # spaced sufficiently. return hold_point @@ -1206,10 +1212,10 @@ class FlightPlanBuilder: # properly. origin_to_join = origin.distance_to_point(join) cos_theta = ( - (self.doctrine.hold_distance ** 2 + + (self.doctrine.hold_distance.meters ** 2 + origin_to_join ** 2 - - self.doctrine.join_distance ** 2) / - (2 * self.doctrine.hold_distance * origin_to_join) + self.doctrine.join_distance.meters ** 2) / + (2 * self.doctrine.hold_distance.meters * origin_to_join) ) try: theta = math.acos(cos_theta) @@ -1218,10 +1224,10 @@ class FlightPlanBuilder: # hold point away from the target. return origin.point_from_heading( target.heading_between_point(origin), - self.doctrine.hold_distance) + self.doctrine.hold_distance.meters) return origin.point_from_heading(heading_to_join - theta, - self.doctrine.hold_distance) + self.doctrine.hold_distance.meters) # TODO: Make a model for the waypoint builder and use that in the UI. def generate_rtb_waypoint(self, flight: Flight, @@ -1273,13 +1279,13 @@ class FlightPlanBuilder: return attack_transition.point_from_heading( self.package.target.position.heading_between_point( self.package_airfield().position), - self.doctrine.join_distance) + self.doctrine.join_distance.meters) def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: """Creates a rendezvous point that advances toward the target.""" heading = self._heading_to_package_airfield(attack_transition) return attack_transition.point_from_heading( - heading, -self.doctrine.join_distance) + heading, -self.doctrine.join_distance.meters) def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: transition_target_distance = attack_transition.distance_to_point( @@ -1307,13 +1313,13 @@ class FlightPlanBuilder: def _ingress_point(self) -> Point: heading = self._target_heading_to_package_airfield() return self.package.target.position.point_from_heading( - heading - 180 + 25, self.doctrine.ingress_egress_distance + heading - 180 + 25, self.doctrine.ingress_egress_distance.meters ) def _egress_point(self) -> Point: heading = self._target_heading_to_package_airfield() return self.package.target.position.point_from_heading( - heading - 180 - 25, self.doctrine.ingress_egress_distance + heading - 180 - 25, self.doctrine.ingress_egress_distance.meters ) def _target_heading_to_package_airfield(self) -> int: diff --git a/gen/flights/traveltime.py b/gen/flights/traveltime.py index 82e5b45b..3311fe22 100644 --- a/gen/flights/traveltime.py +++ b/gen/flights/traveltime.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import math from datetime import timedelta -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING from dcs.mapping import Point from dcs.unittype import FlyingType @@ -12,7 +12,8 @@ from game.utils import ( Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, - kph, mach, meter_to_nm, + kph, + mach, meters, ) from gen.flights.flight import Flight @@ -24,7 +25,7 @@ if TYPE_CHECKING: class GroundSpeed: @classmethod - def for_flight(cls, flight: Flight, altitude: Distance) -> int: + def for_flight(cls, flight: Flight, altitude: Distance) -> Speed: if not issubclass(flight.unit_type, FlyingType): raise TypeError("Flight has non-flying unit") @@ -38,7 +39,7 @@ class GroundSpeed: if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL: # Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and # account for heavily loaded jets. - return int(mach(0.8, altitude).knots) + return mach(0.8, altitude) # For subsonic aircraft, assume the aircraft can reasonably perform at # 80% of its maximum, and that it can maintain the same mach at altitude @@ -46,15 +47,16 @@ class GroundSpeed: # might. be sufficient given the wiggle room. We can come up with # another heuristic if needed. cruise_mach = max_speed.mach() * 0.8 - return int(mach(cruise_mach, altitude).knots) + return mach(cruise_mach, altitude) class TravelTime: @staticmethod - def between_points(a: Point, b: Point, speed: float) -> timedelta: + def between_points(a: Point, b: Point, speed: Speed) -> timedelta: error_factor = 1.1 - distance = meter_to_nm(a.distance_to_point(b)) - return timedelta(hours=distance / speed * error_factor) + distance = meters(a.distance_to_point(b)) + return timedelta( + hours=distance.nautical_miles / speed.knots * error_factor) # TODO: Most if not all of this should move into FlightPlan. diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index ea18eb46..2c9c858c 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -34,7 +34,7 @@ from game.theater.theatergroundobject import ( LhaGroundObject, ShipGroundObject, ) from game.unitmap import UnitMap -from game.utils import knots_to_kph, kph_to_mps, mps_to_kph +from game.utils import knots, mps from .radios import RadioFrequency, RadioRegistry from .runways import RunwayData from .tacan import TacanBand, TacanChannel, TacanRegistry @@ -247,13 +247,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator): wind = self.game.conditions.weather.wind.at_0m brc = wind.direction + 180 # Aim for 25kts over the deck. - carrier_speed = knots_to_kph(25) - mps_to_kph(wind.speed) + carrier_speed = knots(25) - mps(wind.speed) for attempt in range(5): point = group.points[0].position.point_from_heading( brc, 100000 - attempt * 20000) if self.game.theater.is_in_sea(point): - group.points[0].speed = kph_to_mps(carrier_speed) - group.add_waypoint(point, carrier_speed) + group.points[0].speed = carrier_speed.meters_per_second + group.add_waypoint(point, carrier_speed.kph) return brc return None diff --git a/gen/kneeboard.py b/gen/kneeboard.py index 4ee3aa28..aaf74120 100644 --- a/gen/kneeboard.py +++ b/gen/kneeboard.py @@ -33,8 +33,7 @@ from dcs.mission import Mission from dcs.unittype import FlyingType from tabulate import tabulate -from game.utils import meter_to_nm -from . import units +from game.utils import meters from .aircraft import AIRCRAFT_DATA, FlightData from .airsupportgen import AwacsInfo, TankerInfo from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator @@ -170,10 +169,10 @@ class FlightPlanBuilder: if self.last_waypoint is None: return "-" - distance = meter_to_nm(self.last_waypoint.position.distance_to_point( + distance = meters(self.last_waypoint.position.distance_to_point( waypoint.position )) - return f"{distance} NM" + return f"{distance.nautical_miles:.1f} NM" def _ground_speed(self, waypoint: FlightWaypoint) -> str: if self.last_waypoint is None: @@ -189,19 +188,11 @@ class FlightPlanBuilder: else: return "-" - distance = meter_to_nm(self.last_waypoint.position.distance_to_point( + distance = meters(self.last_waypoint.position.distance_to_point( waypoint.position )) duration = (waypoint.tot - last_time).total_seconds() / 3600 - try: - return f"{int(distance / duration)} kt" - except ZeroDivisionError: - # TODO: Improve resolution of unit conversions. - # When waypoints are very close to each other they can end up with - # identical TOTs because our unit conversion functions truncate to - # int. When waypoints have the same TOT the duration will be zero. - # https://github.com/Khopa/dcs_liberation/issues/557 - return "-" + return f"{int(distance.nautical_miles / duration)} kt" def build(self) -> List[List[str]]: return self.rows diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py index 6116586b..89cf2cdb 100644 --- a/qt_ui/widgets/QConditionsWidget.py +++ b/qt_ui/widgets/QConditionsWidget.py @@ -1,12 +1,18 @@ -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout, QFrame, QGridLayout from PySide2.QtGui import QPixmap - -from game.weather import Conditions, TimeOfDay, Weather -from game.utils import meter_to_nm, mps_to_knots +from PySide2.QtWidgets import ( + QFrame, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QVBoxLayout, +) from dcs.weather import Weather as PydcsWeather import qt_ui.uiconstants as CONST +from game.utils import mps +from game.weather import Conditions, TimeOfDay + class QTimeTurnWidget(QGroupBox): """ @@ -163,20 +169,20 @@ class QWeatherWidget(QGroupBox): def updateWinds(self): """Updates the UI with the current conditions wind info. """ - windGlSpeed = mps_to_knots(self.conditions.weather.wind.at_0m.speed or 0) + windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0) windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, '0') - self.windGLSpeedLabel.setText('{}kts'.format(windGlSpeed)) - self.windGLDirLabel.setText('{}º'.format(windGlDir)) + self.windGLSpeedLabel.setText(f'{int(windGlSpeed.knots)}kts') + self.windGLDirLabel.setText(f'{windGlDir}º') - windFL08Speed = mps_to_knots(self.conditions.weather.wind.at_2000m.speed or 0) + windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0) windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(3, '0') - self.windFL08SpeedLabel.setText('{}kts'.format(windFL08Speed)) - self.windFL08DirLabel.setText('{}º'.format(windFL08Dir)) + self.windFL08SpeedLabel.setText(f'{int(windFL08Speed.knots)}kts') + self.windFL08DirLabel.setText(f'{windFL08Dir}º') - windFL26Speed = mps_to_knots(self.conditions.weather.wind.at_8000m.speed or 0) + windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0) windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(3, '0') - self.windFL26SpeedLabel.setText('{}kts'.format(windFL26Speed)) - self.windFL26DirLabel.setText('{}º'.format(windFL26Dir)) + self.windFL26SpeedLabel.setText(f'{int(windFL26Speed.knots)}kts') + self.windFL26DirLabel.setText(f'{windFL26Dir}º') def updateForecast(self): """Updates the Forecast Text and icon with the current conditions wind info. @@ -223,11 +229,10 @@ class QWeatherWidget(QGroupBox): if not fog: self.forecastFog.setText('No fog') else: - visvibilityNm = round(meter_to_nm(fog.visibility), 1) - self.forecastFog.setText('Fog vis: {}nm'.format(visvibilityNm)) + visibility = round(fog.visibility.nautical_miles, 1) + self.forecastFog.setText(f'Fog vis: {visibility}nm') icon = [time, ('cloudy' if cloudDensity > 1 else None), 'fog'] - icon_key = "Weather_{}".format('-'.join(filter(None.__ne__, icon))) icon = CONST.ICONS.get(icon_key) or CONST.ICONS['Weather_night-partly-cloudy'] self.weather_icon.setPixmap(icon) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 3c3be38e..a21a4058 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -5,8 +5,8 @@ import logging import math from typing import Iterable, Iterator, List, Optional, Tuple -from PySide2 import QtWidgets, QtCore -from PySide2.QtCore import QPointF, Qt, QLineF, QRectF +from PySide2 import QtCore, QtWidgets +from PySide2.QtCore import QLineF, QPointF, QRectF, Qt from PySide2.QtGui import ( QBrush, QColor, @@ -14,13 +14,15 @@ from PySide2.QtGui import ( QPen, QPixmap, QPolygonF, - QWheelEvent, ) + QWheelEvent, +) from PySide2.QtWidgets import ( QFrame, QGraphicsItem, QGraphicsOpacityEffect, QGraphicsScene, - QGraphicsView, QGraphicsSceneMouseEvent, + QGraphicsSceneMouseEvent, + QGraphicsView, ) from dcs import Point from dcs.mapping import point_from_heading @@ -32,7 +34,7 @@ from game.theater.conflicttheater import FrontLine, ReferencePoint from game.theater.theatergroundobject import ( TheaterGroundObject, ) -from game.utils import meter_to_feet, nm_to_meter, meter_to_nm +from game.utils import Distance, meters, nautical_miles from game.weather import TimeOfDay from gen import Conflict from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType @@ -45,7 +47,7 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.windows.GameUpdateSignal import GameUpdateSignal -MAX_SHIP_DISTANCE = 80 +MAX_SHIP_DISTANCE = nautical_miles(80) def binomial(i: int, n: int) -> float: """Binomial coefficient""" @@ -171,7 +173,7 @@ class QLiberationMap(QGraphicsView): self.game = game if self.game is not None: logging.debug("Reloading Map Canvas") - self.nm_to_pixel_ratio = self.km_to_pixel(float(nm_to_meter(1)) / 1000.0) + self.nm_to_pixel_ratio = self.distance_to_pixels(nautical_miles(1)) self.reload_scene() """ @@ -567,7 +569,7 @@ class QLiberationMap(QGraphicsView): BIG_LINE = 5 SMALL_LINE = 2 - dist = self.km_to_pixel(nm_to_meter(scale_distance_nm)/1000.0) + dist = self.distance_to_pixels(nautical_miles(scale_distance_nm)) self.scene().addRect(POS_X, POS_Y-PADDING, PADDING*2 + dist, BIG_LINE*2+3*PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"]) l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE*2, POS_X + PADDING + dist, POS_Y + BIG_LINE*2) @@ -663,12 +665,12 @@ class QLiberationMap(QGraphicsView): Point(offset.x / scale.x, offset.y / scale.y)) return point_a.world_coordinates - scaled - def km_to_pixel(self, km): + def distance_to_pixels(self, distance: Distance) -> int: p1 = Point(0, 0) - p2 = Point(0, 1000*km) + p2 = Point(0, distance.meters) p1a = Point(*self._transform_point(p1)) p2a = Point(*self._transform_point(p2)) - return p1a.distance_to_point(p2a) + return int(p1a.distance_to_point(p2a)) def highlight_color(self, transparent: Optional[bool] = False) -> QColor: return QColor(255, 255, 0, 20 if transparent else 255) @@ -820,7 +822,7 @@ class QLiberationMap(QGraphicsView): distance = self.selected_cp.control_point.position.distance_to_point( world_destination ) - if meter_to_nm(distance) > MAX_SHIP_DISTANCE: + if meters(distance) > MAX_SHIP_DISTANCE: return False return self.game.theater.is_in_sea(world_destination) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py index 61fa8cc9..bca07dbc 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointList.py @@ -4,7 +4,6 @@ from PySide2.QtCore import QItemSelectionModel, QPoint from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtWidgets import QHeaderView, QTableView -from game.utils import meter_to_feet from gen.ato import Package from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \