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 \