diff --git a/game/point_with_heading.py b/game/point_with_heading.py index a87914a1..7eed4da2 100644 --- a/game/point_with_heading.py +++ b/game/point_with_heading.py @@ -1,15 +1,16 @@ from __future__ import annotations from dcs import Point +from game.utils import Heading class PointWithHeading(Point): def __init__(self) -> None: super(PointWithHeading, self).__init__(0, 0) - self.heading = 0 + self.heading: Heading = Heading.from_degrees(0) @staticmethod - def from_point(point: Point, heading: int) -> PointWithHeading: + def from_point(point: Point, heading: Heading) -> PointWithHeading: p = PointWithHeading() p.x = point.x p.y = point.y diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 95e53ac9..8e88bda2 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -59,7 +59,7 @@ from ..point_with_heading import PointWithHeading from ..positioned import Positioned from ..profiling import logged_duration from ..scenery_group import SceneryGroup -from ..utils import Distance, meters +from ..utils import Distance, Heading, meters if TYPE_CHECKING: from . import TheaterGroundObject @@ -400,85 +400,113 @@ class MizCampaignLoader: for static in self.offshore_strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.offshore_strike_locations.append( - PointWithHeading.from_point(static.position, static.units[0].heading) + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) ) for ship in self.ships: closest, distance = self.objective_info(ship, allow_naval=True) closest.preset_locations.ships.append( - PointWithHeading.from_point(ship.position, ship.units[0].heading) + PointWithHeading.from_point( + ship.position, Heading.from_degrees(ship.units[0].heading) + ) ) for group in self.missile_sites: closest, distance = self.objective_info(group) closest.preset_locations.missile_sites.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.coastal_defenses: closest, distance = self.objective_info(group) closest.preset_locations.coastal_defenses.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.long_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.long_range_sams.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.medium_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.medium_range_sams.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.short_range_sams: closest, distance = self.objective_info(group) closest.preset_locations.short_range_sams.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.aaa: closest, distance = self.objective_info(group) closest.preset_locations.aaa.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.ewrs: closest, distance = self.objective_info(group) closest.preset_locations.ewrs.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for group in self.armor_groups: closest, distance = self.objective_info(group) closest.preset_locations.armor_groups.append( - PointWithHeading.from_point(group.position, group.units[0].heading) + PointWithHeading.from_point( + group.position, Heading.from_degrees(group.units[0].heading) + ) ) for static in self.helipads: closest, distance = self.objective_info(static) closest.helipads.append( - PointWithHeading.from_point(static.position, static.units[0].heading) + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) ) for static in self.factories: closest, distance = self.objective_info(static) closest.preset_locations.factories.append( - PointWithHeading.from_point(static.position, static.units[0].heading) + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) ) for static in self.ammunition_depots: closest, distance = self.objective_info(static) closest.preset_locations.ammunition_depots.append( - PointWithHeading.from_point(static.position, static.units[0].heading) + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) ) for static in self.strike_targets: closest, distance = self.objective_info(static) closest.preset_locations.strike_locations.append( - PointWithHeading.from_point(static.position, static.units[0].heading) + PointWithHeading.from_point( + static.position, Heading.from_degrees(static.units[0].heading) + ) ) for scenery_group in self.scenery: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 075f4f5e..12786d32 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -35,6 +35,7 @@ from dcs.unit import Unit from game import db from game.point_with_heading import PointWithHeading from game.scenery_group import SceneryGroup +from game.utils import Heading from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.combat_stance import CombatStance from gen.runways import RunwayAssigner, RunwayData @@ -335,7 +336,7 @@ class ControlPoint(MissionTarget, ABC): @property @abstractmethod - def heading(self) -> int: + def heading(self) -> Heading: ... def __str__(self) -> str: @@ -838,8 +839,8 @@ class Airfield(ControlPoint): return len(self.airport.parking_slots) @property - def heading(self) -> int: - return self.airport.runways[0].heading + def heading(self) -> Heading: + return Heading.from_degrees(self.airport.runways[0].heading) def runway_is_operational(self) -> bool: return not self.runway_status.damaged @@ -903,8 +904,8 @@ class NavalControlPoint(ControlPoint, ABC): yield from super().mission_types(for_player) @property - def heading(self) -> int: - return 0 # TODO compute heading + def heading(self) -> Heading: + return Heading.from_degrees(0) # TODO compute heading def find_main_tgo(self) -> GenericCarrierGroundObject: for g in self.ground_objects: @@ -933,7 +934,9 @@ class NavalControlPoint(ControlPoint, ABC): self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] ) -> RunwayData: # TODO: Assign TACAN and ICLS earlier so we don't need this. - fallback = RunwayData(self.full_name, runway_heading=0, runway_name="") + fallback = RunwayData( + self.full_name, runway_heading=Heading.from_degrees(0), runway_name="" + ) return dynamic_runways.get(self.name, fallback) @property @@ -1071,14 +1074,16 @@ class OffMapSpawn(ControlPoint): return True @property - def heading(self) -> int: - return 0 + def heading(self) -> Heading: + return Heading.from_degrees(0) def active_runway( self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] ) -> RunwayData: logging.warning("TODO: Off map spawns have no runways.") - return RunwayData(self.full_name, runway_heading=0, runway_name="") + return RunwayData( + self.full_name, runway_heading=Heading.from_degrees(0), runway_name="" + ) @property def runway_status(self) -> RunwayStatus: @@ -1120,7 +1125,9 @@ class Fob(ControlPoint): self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] ) -> RunwayData: logging.warning("TODO: FOBs have no runways.") - return RunwayData(self.full_name, runway_heading=0, runway_name="") + return RunwayData( + self.full_name, runway_heading=Heading.from_degrees(0), runway_name="" + ) @property def runway_status(self) -> RunwayStatus: @@ -1142,8 +1149,8 @@ class Fob(ControlPoint): return False @property - def heading(self) -> int: - return 0 + def heading(self) -> Heading: + return Heading.from_degrees(0) @property def can_deploy_ground_units(self) -> bool: diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 2f1b6067..98aa88f6 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -11,7 +11,7 @@ from .controlpoint import ( ControlPoint, MissionTarget, ) -from ..utils import pairwise +from ..utils import Heading, pairwise FRONTLINE_MIN_CP_DISTANCE = 5000 @@ -27,9 +27,9 @@ class FrontLineSegment: point_b: Point @property - def attack_heading(self) -> float: + def attack_heading(self) -> Heading: """The heading of the frontline segment from player to enemy control point""" - return self.point_a.heading_between_point(self.point_b) + return Heading.from_degrees(self.point_a.heading_between_point(self.point_b)) @property def attack_distance(self) -> float: @@ -123,7 +123,7 @@ class FrontLine(MissionTarget): return sum(i.attack_distance for i in self.segments) @property - def attack_heading(self) -> float: + def attack_heading(self) -> Heading: """The heading of the active attack segment from player to enemy control point""" return self.active_segment.attack_heading @@ -150,13 +150,13 @@ class FrontLine(MissionTarget): """ if distance < self.segments[0].attack_distance: return self.blue_cp.position.point_from_heading( - self.segments[0].attack_heading, distance + self.segments[0].attack_heading.degrees, distance ) remaining_dist = distance for segment in self.segments: if remaining_dist < segment.attack_distance: return segment.point_a.point_from_heading( - segment.attack_heading, remaining_dist + segment.attack_heading.degrees, remaining_dist ) else: remaining_dist -= segment.attack_distance diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index aee758e9..61cc25af 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -28,6 +28,7 @@ from game.theater.theatergroundobject import ( VehicleGroupGroundObject, CoastalSiteGroundObject, ) +from game.utils import Heading from game.version import VERSION from gen import namegen from gen.coastal.coastal_group_generator import generate_coastal_group @@ -385,7 +386,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): group_id, object_id, position + template_point, - unit["heading"], + Heading.from_degrees(unit["heading"]), self.control_point, unit["type"], ) @@ -585,7 +586,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): group_id, object_id, point + template_point, - unit["heading"], + Heading.from_degrees(unit["heading"]), self.control_point, unit["type"], is_fob_structure=True, diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index f063a1ea..d3bfae64 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -17,7 +17,7 @@ from ..data.radar_db import ( TELARS, LAUNCHER_TRACKER_PAIRS, ) -from ..utils import Distance, meters +from ..utils import Distance, Heading, meters if TYPE_CHECKING: from .controlpoint import ControlPoint @@ -58,7 +58,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]): category: str, group_id: int, position: Point, - heading: int, + heading: Heading, control_point: ControlPoint, dcs_identifier: str, sea_object: bool, @@ -222,7 +222,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]): group_id: int, object_id: int, position: Point, - heading: int, + heading: Heading, control_point: ControlPoint, dcs_identifier: str, is_fob_structure: bool = False, @@ -310,7 +310,7 @@ class SceneryGroundObject(BuildingGroundObject): group_id=group_id, object_id=object_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier=dcs_identifier, is_fob_structure=False, @@ -334,7 +334,7 @@ class FactoryGroundObject(BuildingGroundObject): name: str, group_id: int, position: Point, - heading: int, + heading: Heading, control_point: ControlPoint, ) -> None: super().__init__( @@ -385,7 +385,7 @@ class CarrierGroundObject(GenericCarrierGroundObject): category="CARRIER", group_id=group_id, position=control_point.position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="CARRIER", sea_object=True, @@ -406,7 +406,7 @@ class LhaGroundObject(GenericCarrierGroundObject): category="LHA", group_id=group_id, position=control_point.position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="LHA", sea_object=True, @@ -428,7 +428,7 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]): category="missile", group_id=group_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="AA", sea_object=False, @@ -450,7 +450,7 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]): group_id: int, position: Point, control_point: ControlPoint, - heading: int, + heading: Heading, ) -> None: super().__init__( name=name, @@ -497,7 +497,7 @@ class SamGroundObject(IadsGroundObject): category="aa", group_id=group_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="AA", sea_object=False, @@ -565,7 +565,7 @@ class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]): category="armor", group_id=group_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="AA", sea_object=False, @@ -593,7 +593,7 @@ class EwrGroundObject(IadsGroundObject): category="ewr", group_id=group_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="EWR", sea_object=False, @@ -627,7 +627,7 @@ class ShipGroundObject(NavalGroundObject): category="ship", group_id=group_id, position=position, - heading=0, + heading=Heading.from_degrees(0), control_point=control_point, dcs_identifier="AA", sea_object=True, diff --git a/game/utils.py b/game/utils.py index b21b11df..119a741a 100644 --- a/game/utils.py +++ b/game/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations import itertools import math +import random from collections import Iterable from dataclasses import dataclass from typing import Union, Any, TypeVar @@ -20,15 +21,6 @@ INHG_TO_HPA = 33.86389 INHG_TO_MMHG = 25.400002776728 -def heading_sum(h: int, a: int) -> int: - h += a - return h % 360 - - -def opposite_heading(h: int) -> int: - return heading_sum(h, 180) - - @dataclass(frozen=True, order=True) class Distance: distance_in_meters: float @@ -184,6 +176,60 @@ def mach(value: float, altitude: Distance) -> Speed: 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 = 0) -> 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 diff --git a/gen/aircraft.py b/gen/aircraft.py index 144344df..998696ac 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -81,7 +81,7 @@ from game.theater.missiontarget import MissionTarget from game.theater.theatergroundobject import TheaterGroundObject from game.transfers import MultiGroupTransport from game.unitmap import UnitMap -from game.utils import Distance, meters, nautical_miles, pairwise +from game.utils import Distance, Heading, meters, nautical_miles, pairwise from gen.ato import AirTaskingOrder, Package from gen.callsigns import create_group_callsign_from_unit from gen.flights.flight import ( diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 409a0959..72f4fecc 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -17,6 +17,9 @@ from dcs.task import ( ) from dcs.unittype import UnitType +from game.utils import Heading +from .flights.ai_flight_planner_db import AEWC_CAPABLE +from .naming import namegen from .callsigns import callsign_for_support_unit from .conflictgen import Conflict from .flights.ai_flight_planner_db import AEWC_CAPABLE @@ -122,14 +125,14 @@ class AirSupportConflictGenerator: alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type) freq = self.radio_registry.alloc_uhf() tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) - tanker_heading = ( + tanker_heading = Heading.from_degrees( self.conflict.red_cp.position.heading_between_point( self.conflict.blue_cp.position ) + TANKER_HEADING_OFFSET * i ) tanker_position = player_cp.position.point_from_heading( - tanker_heading, TANKER_DISTANCE + tanker_heading.degrees, TANKER_DISTANCE ) tanker_group = self.mission.refuel_flight( country=country, diff --git a/gen/armor.py b/gen/armor.py index 7e92169b..a000c8cb 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -32,7 +32,7 @@ from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.theater.controlpoint import ControlPoint from game.unitmap import UnitMap -from game.utils import heading_sum, opposite_heading +from game.utils import Heading from gen.ground_forces.ai_ground_planner import ( DISTANCE_FROM_FRONTLINE, CombatGroup, @@ -130,7 +130,7 @@ class GroundConflictGenerator: self.player_stance, player_groups, enemy_groups, - self.conflict.heading + 90, + self.conflict.heading.right, self.conflict.blue_cp, self.conflict.red_cp, ) @@ -138,7 +138,7 @@ class GroundConflictGenerator: self.enemy_stance, enemy_groups, player_groups, - self.conflict.heading - 90, + self.conflict.heading.left, self.conflict.red_cp, self.conflict.blue_cp, ) @@ -182,7 +182,11 @@ class GroundConflictGenerator: ) def gen_infantry_group_for_group( - self, group: VehicleGroup, is_player: bool, side: Country, forward_heading: int + self, + group: VehicleGroup, + is_player: bool, + side: Country, + forward_heading: Heading, ) -> None: infantry_position = self.conflict.find_ground_position( @@ -217,7 +221,7 @@ class GroundConflictGenerator: u.dcs_unit_type, position=infantry_position, group_size=1, - heading=forward_heading, + heading=forward_heading.degrees, move_formation=PointAction.OffRoad, ) return @@ -244,7 +248,7 @@ class GroundConflictGenerator: units[0].dcs_unit_type, position=infantry_position, group_size=1, - heading=forward_heading, + heading=forward_heading.degrees, move_formation=PointAction.OffRoad, ) @@ -256,17 +260,19 @@ class GroundConflictGenerator: unit.dcs_unit_type, position=position, group_size=1, - heading=forward_heading, + heading=forward_heading.degrees, move_formation=PointAction.OffRoad, ) def _set_reform_waypoint( - self, dcs_group: VehicleGroup, forward_heading: int + self, dcs_group: VehicleGroup, forward_heading: Heading ) -> None: """Setting a waypoint close to the spawn position allows the group to reform gracefully rather than spin """ - reform_point = dcs_group.position.point_from_heading(forward_heading, 50) + reform_point = dcs_group.position.point_from_heading( + forward_heading.degrees, 50 + ) dcs_group.add_waypoint(reform_point) def _plan_artillery_action( @@ -274,7 +280,7 @@ class GroundConflictGenerator: stance: CombatStance, gen_group: CombatGroup, dcs_group: VehicleGroup, - forward_heading: int, + forward_heading: Heading, target: Point, ) -> bool: """ @@ -308,7 +314,7 @@ class GroundConflictGenerator: dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3) ) dcs_group.add_waypoint( - dcs_group.position.point_from_heading(forward_heading, 1), + dcs_group.position.point_from_heading(forward_heading.degrees, 1), PointAction.OffRoad, ) dcs_group.points[2].tasks.append(Hold()) @@ -336,7 +342,7 @@ class GroundConflictGenerator: self.mission.triggerrules.triggers.append(artillery_fallback) for u in dcs_group.units: - u.heading = forward_heading + random.randint(-5, 5) + u.heading = (forward_heading + Heading.random(-5, 5)).degrees return True return False @@ -345,7 +351,7 @@ class GroundConflictGenerator: stance: CombatStance, enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], dcs_group: VehicleGroup, - forward_heading: int, + forward_heading: Heading, to_cp: ControlPoint, ) -> bool: """ @@ -378,9 +384,7 @@ class GroundConflictGenerator: else: # We use an offset heading here because DCS doesn't always # force vehicles to move if there's no heading change. - offset_heading = forward_heading - 2 - if offset_heading < 0: - offset_heading = 358 + offset_heading = forward_heading - Heading.from_degrees(2) attack_point = self.find_offensive_point( dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE ) @@ -398,9 +402,7 @@ class GroundConflictGenerator: else: # We use an offset heading here because DCS doesn't always # force vehicles to move if there's no heading change. - offset_heading = forward_heading - 1 - if offset_heading < 0: - offset_heading = 359 + offset_heading = forward_heading - Heading.from_degrees(1) attack_point = self.find_offensive_point( dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE ) @@ -436,7 +438,7 @@ class GroundConflictGenerator: self, stance: CombatStance, dcs_group: VehicleGroup, - forward_heading: int, + forward_heading: Heading, to_cp: ControlPoint, ) -> bool: """ @@ -473,7 +475,7 @@ class GroundConflictGenerator: stance: CombatStance, ally_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], - forward_heading: int, + forward_heading: Heading, from_cp: ControlPoint, to_cp: ControlPoint, ) -> None: @@ -514,12 +516,14 @@ class GroundConflictGenerator: else: retreat_point = self.find_retreat_point(dcs_group, forward_heading) reposition_point = retreat_point.point_from_heading( - forward_heading, 10 + forward_heading.degrees, 10 ) # Another point to make the unit face the enemy dcs_group.add_waypoint(retreat_point, PointAction.OffRoad) dcs_group.add_waypoint(reposition_point, PointAction.OffRoad) - def add_morale_trigger(self, dcs_group: VehicleGroup, forward_heading: int) -> None: + def add_morale_trigger( + self, dcs_group: VehicleGroup, forward_heading: Heading + ) -> None: """ This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS """ @@ -532,7 +536,7 @@ class GroundConflictGenerator: # Force unit heading for unit in dcs_group.units: - unit.heading = forward_heading + unit.heading = forward_heading.degrees dcs_group.manualHeading = True # We add a new retreat waypoint @@ -563,7 +567,7 @@ class GroundConflictGenerator: def find_retreat_point( self, dcs_group: VehicleGroup, - frontline_heading: int, + frontline_heading: Heading, distance: int = RETREAT_DISTANCE, ) -> Point: """ @@ -573,14 +577,14 @@ class GroundConflictGenerator: :return: dcs.mapping.Point object with the desired position """ desired_point = dcs_group.points[0].position.point_from_heading( - heading_sum(frontline_heading, +180), distance + frontline_heading.opposite.degrees, distance ) if self.conflict.theater.is_on_land(desired_point): return desired_point return self.conflict.theater.nearest_land_pos(desired_point) def find_offensive_point( - self, dcs_group: VehicleGroup, frontline_heading: int, distance: int + self, dcs_group: VehicleGroup, frontline_heading: Heading, distance: int ) -> Point: """ Find a point to attack @@ -590,7 +594,7 @@ class GroundConflictGenerator: :return: dcs.mapping.Point object with the desired position """ desired_point = dcs_group.points[0].position.point_from_heading( - frontline_heading, distance + frontline_heading.degrees, distance ) if self.conflict.theater.is_on_land(desired_point): return desired_point @@ -688,14 +692,14 @@ class GroundConflictGenerator: conflict_position: Point, combat_width: int, distance_from_frontline: int, - heading: int, - spawn_heading: int, + heading: Heading, + spawn_heading: Heading, ) -> Optional[Point]: shifted = conflict_position.point_from_heading( - heading, random.randint(0, combat_width) + heading.degrees, random.randint(0, combat_width) ) desired_point = shifted.point_from_heading( - spawn_heading, distance_from_frontline + spawn_heading.degrees, distance_from_frontline ) return Conflict.find_ground_position( desired_point, combat_width, heading, self.conflict.theater @@ -704,17 +708,13 @@ class GroundConflictGenerator: def _generate_groups( self, groups: list[CombatGroup], - frontline_vector: Tuple[Point, int, int], + frontline_vector: Tuple[Point, Heading, int], is_player: bool, ) -> List[Tuple[VehicleGroup, CombatGroup]]: """Finds valid positions for planned groups and generates a pydcs group for them""" positioned_groups = [] position, heading, combat_width = frontline_vector - spawn_heading = ( - int(heading_sum(heading, -90)) - if is_player - else int(heading_sum(heading, 90)) - ) + spawn_heading = heading.left if is_player else heading.right country = self.game.coalition_for(is_player).country_name for group in groups: if group.role == CombatGroupRole.ARTILLERY: @@ -737,7 +737,7 @@ class GroundConflictGenerator: group.unit_type, group.size, final_position, - heading=opposite_heading(spawn_heading), + heading=spawn_heading.opposite, ) if is_player: g.set_skill(Skill(self.game.settings.player_skill)) @@ -750,7 +750,7 @@ class GroundConflictGenerator: g, is_player, self.mission.country(country), - opposite_heading(spawn_heading), + spawn_heading.opposite, ) else: logging.warning(f"Unable to get valid position for {group}") @@ -764,7 +764,7 @@ class GroundConflictGenerator: count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, - heading: int = 0, + heading: Heading = Heading.from_degrees(0), ) -> VehicleGroup: if side == self.conflict.attackers_country: @@ -778,7 +778,7 @@ class GroundConflictGenerator: unit_type.dcs_unit_type, position=at, group_size=count, - heading=heading, + heading=heading.degrees, move_formation=move_formation, ) diff --git a/gen/coastal/silkworm.py b/gen/coastal/silkworm.py index 6712762a..b0fb98c5 100644 --- a/gen/coastal/silkworm.py +++ b/gen/coastal/silkworm.py @@ -3,6 +3,7 @@ from dcs.vehicles import MissilesSS, Unarmed, AirDefence from game import Game from game.factions.faction import Faction from game.theater.theatergroundobject import CoastalSiteGroundObject +from game.utils import Heading from gen.sam.group_generator import VehicleGroupGenerator @@ -59,5 +60,5 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]): "STRELA#0", self.position.x + 200, self.position.y + 15, - 90, + Heading.from_degrees(90), ) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 5576805a..6693367e 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -9,7 +9,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.controlpoint import ControlPoint -from game.utils import heading_sum, opposite_heading +from game.utils import Heading FRONTLINE_LENGTH = 80000 @@ -25,7 +25,7 @@ class Conflict: attackers_country: Country, defenders_country: Country, position: Point, - heading: Optional[int] = None, + heading: Optional[Heading] = None, size: Optional[int] = None, ): @@ -55,28 +55,28 @@ class Conflict: @classmethod def frontline_position( cls, frontline: FrontLine, theater: ConflictTheater - ) -> Tuple[Point, int]: - attack_heading = int(frontline.attack_heading) + ) -> Tuple[Point, Heading]: + attack_heading = frontline.attack_heading position = cls.find_ground_position( frontline.position, FRONTLINE_LENGTH, - heading_sum(attack_heading, 90), + attack_heading.right, theater, ) if position is None: raise RuntimeError("Could not find front line position") - return position, opposite_heading(attack_heading) + return position, attack_heading.opposite @classmethod def frontline_vector( cls, front_line: FrontLine, theater: ConflictTheater - ) -> Tuple[Point, int, int]: + ) -> Tuple[Point, Heading, int]: """ Returns a vector for a valid frontline location avoiding exclusion zones. """ center_position, heading = cls.frontline_position(front_line, theater) - left_heading = heading_sum(heading, -90) - right_heading = heading_sum(heading, 90) + left_heading = heading.left + right_heading = heading.right left_position = cls.extend_ground_position( center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater ) @@ -113,10 +113,14 @@ class Conflict: @classmethod def extend_ground_position( - cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater + cls, + initial: Point, + max_distance: int, + heading: Heading, + theater: ConflictTheater, ) -> Point: """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" - extended = initial.point_from_heading(heading, max_distance) + extended = initial.point_from_heading(heading.degrees, max_distance) if theater.landmap is None: # TODO: Why is this possible? return extended @@ -133,14 +137,14 @@ class Conflict: return extended # Otherwise extend the front line only up to the intersection. - return initial.point_from_heading(heading, p0.distance(intersection)) + return initial.point_from_heading(heading.degrees, p0.distance(intersection)) @classmethod def find_ground_position( cls, initial: Point, max_distance: int, - heading: int, + heading: Heading, theater: ConflictTheater, coerce: bool = True, ) -> Optional[Point]: @@ -153,10 +157,10 @@ class Conflict: if theater.is_on_land(pos): return pos for distance in range(0, int(max_distance), 100): - pos = initial.point_from_heading(heading, distance) + pos = initial.point_from_heading(heading.degrees, distance) if theater.is_on_land(pos): return pos - pos = initial.point_from_heading(opposite_heading(heading), distance) + pos = initial.point_from_heading(heading.opposite.degrees, distance) if theater.is_on_land(pos): return pos if coerce: diff --git a/gen/fleet/carrier_group.py b/gen/fleet/carrier_group.py index b25902a9..74ca4c67 100644 --- a/gen/fleet/carrier_group.py +++ b/gen/fleet/carrier_group.py @@ -1,6 +1,7 @@ import random from gen.sam.group_generator import ShipGroupGenerator +from game.utils import Heading from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG @@ -54,7 +55,7 @@ class CarrierGroupGenerator(ShipGroupGenerator): ) # Add Ticonderoga escort - if self.heading >= 180: + if self.heading >= Heading.from_degrees(180): self.add_unit( TICONDEROG, "USS Hué City", diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index fde43ee9..d3559442 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -37,8 +37,10 @@ from game.theater.theatergroundobject import ( NavalGroundObject, BuildingGroundObject, ) + from game.threatzones import ThreatZones -from game.utils import Distance, Speed, feet, meters, nautical_miles, knots +from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots + from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime @@ -1151,10 +1153,11 @@ class FlightPlanBuilder: """ assert self.package.waypoints is not None target = self.package.target.position - - heading = self.package.waypoints.join.heading_between_point(target) + heading = Heading.from_degrees( + self.package.waypoints.join.heading_between_point(target) + ) start_pos = target.point_from_heading( - heading, -self.doctrine.sweep_distance.meters + heading.degrees, -self.doctrine.sweep_distance.meters ) builder = WaypointBuilder(flight, self.coalition) @@ -1249,7 +1252,9 @@ class FlightPlanBuilder: else: raise PlanningError("Could not find any enemy airfields") - heading = location.position.heading_between_point(closest_airfield.position) + heading = Heading.from_degrees( + location.position.heading_between_point(closest_airfield.position) + ) position = ShapelyPoint( self.package.target.position.x, self.package.target.position.y @@ -1285,20 +1290,20 @@ class FlightPlanBuilder: ) end = location.position.point_from_heading( - heading, + heading.degrees, random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)), ) diameter = random.randint( int(self.doctrine.cap_min_track_length.meters), int(self.doctrine.cap_max_track_length.meters), ) - start = end.point_from_heading(heading - 180, diameter) + start = end.point_from_heading(heading.opposite.degrees, diameter) return start, end def aewc_orbit(self, location: MissionTarget) -> Point: closest_boundary = self.threat_zones.closest_boundary(location.position) - heading_to_threat_boundary = location.position.heading_between_point( - closest_boundary + heading_to_threat_boundary = Heading.from_degrees( + location.position.heading_between_point(closest_boundary) ) distance_to_threat = meters( location.position.distance_to_point(closest_boundary) @@ -1312,7 +1317,7 @@ class FlightPlanBuilder: orbit_distance = distance_to_threat - threat_buffer return location.position.point_from_heading( - orbit_heading, orbit_distance.meters + orbit_heading.degrees, orbit_distance.meters ) def racetrack_for_frontline( @@ -1320,9 +1325,9 @@ class FlightPlanBuilder: ) -> Tuple[Point, Point]: # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater) - center = ingress.point_from_heading(heading, distance / 2) + center = ingress.point_from_heading(heading.degrees, distance / 2) orbit_center = center.point_from_heading( - heading - 90, + heading.left.degrees, random.randint( int(nautical_miles(6).meters), int(nautical_miles(15).meters) ), @@ -1335,8 +1340,8 @@ class FlightPlanBuilder: combat_width = 35000 radius = combat_width * 1.25 - start = orbit_center.point_from_heading(heading, radius) - end = orbit_center.point_from_heading(heading + 180, radius) + start = orbit_center.point_from_heading(heading.degrees, radius) + end = orbit_center.point_from_heading(heading.opposite.degrees, radius) if end.distance_to_point(origin) < start.distance_to_point(origin): start, end = end, start @@ -1530,8 +1535,8 @@ class FlightPlanBuilder: raise InvalidObjectiveLocation(flight.flight_type, location) ingress, heading, distance = Conflict.frontline_vector(location, self.theater) - center = ingress.point_from_heading(heading, distance / 2) - egress = ingress.point_from_heading(heading, distance) + center = ingress.point_from_heading(heading.degrees, distance / 2) + egress = ingress.point_from_heading(heading.degrees, distance) ingress_distance = ingress.distance_to_point(flight.departure.position) egress_distance = egress.distance_to_point(flight.departure.position) @@ -1566,8 +1571,8 @@ class FlightPlanBuilder: location = self.package.target closest_boundary = self.threat_zones.closest_boundary(location.position) - heading_to_threat_boundary = location.position.heading_between_point( - closest_boundary + heading_to_threat_boundary = Heading.from_degrees( + location.position.heading_between_point(closest_boundary) ) distance_to_threat = meters( location.position.distance_to_point(closest_boundary) @@ -1582,16 +1587,16 @@ class FlightPlanBuilder: orbit_distance = distance_to_threat - threat_buffer racetrack_center = location.position.point_from_heading( - orbit_heading, orbit_distance.meters + orbit_heading.degrees, orbit_distance.meters ) racetrack_half_distance = Distance.from_nautical_miles(20).meters racetrack_start = racetrack_center.point_from_heading( - orbit_heading + 90, racetrack_half_distance + orbit_heading.right.degrees, racetrack_half_distance ) racetrack_end = racetrack_center.point_from_heading( - orbit_heading - 90, racetrack_half_distance + orbit_heading.left.degrees, racetrack_half_distance ) builder = WaypointBuilder(flight, self.coalition) diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index c7b7ca53..69d76998 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -55,7 +55,7 @@ from game.theater.theatergroundobject import ( SceneryGroundObject, ) from game.unitmap import UnitMap -from game.utils import feet, knots, mps +from game.utils import Heading, feet, knots, mps from .radios import RadioFrequency, RadioRegistry from .runways import RunwayData from .tacan import TacanBand, TacanChannel, TacanRegistry @@ -166,7 +166,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject] if targets: target = random.choice(targets) real_target = target.point_from_heading( - random.randint(0, 360), random.randint(0, 2500) + Heading.random().degrees, random.randint(0, 2500) ) vg.points[0].add_task(FireAtPoint(real_target)) logging.info("Set up fire task for missile group.") @@ -246,7 +246,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]): name=self.ground_object.group_name, _type=unit_type, position=self.ground_object.position, - heading=self.ground_object.heading, + heading=self.ground_object.heading.degrees, ) self._register_fortification(group) @@ -256,7 +256,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]): name=self.ground_object.group_name, _type=static_type, position=self.ground_object.position, - heading=self.ground_object.heading, + heading=self.ground_object.heading.degrees, dead=self.ground_object.is_dead, ) self._register_building(group) @@ -387,7 +387,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO # time as the recovery window. brc = self.steam_into_wind(ship_group) self.activate_beacons(ship_group, tacan, tacan_callsign, icls) - self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls) + self.add_runway_data( + brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls + ) self._register_unit_group(group, ship_group) def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]: @@ -422,14 +424,14 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO ship.set_frequency(atc_channel.hertz) return ship - def steam_into_wind(self, group: ShipGroup) -> Optional[int]: - wind = self.game.conditions.weather.wind.at_0m - brc = wind.direction + 180 + def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]: + wind = self.game.conditions.weather.wind.at_0m.direction + brc = Heading.from_degrees(wind.direction).opposite # Aim for 25kts over the deck. carrier_speed = knots(25) - mps(wind.speed) for attempt in range(5): point = group.points[0].position.point_from_heading( - brc, 100000 - attempt * 20000 + brc.degrees, 100000 - attempt * 20000 ) if self.game.theater.is_in_sea(point): group.points[0].speed = carrier_speed.meters_per_second @@ -459,7 +461,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO def add_runway_data( self, - brc: int, + brc: Heading, atc: RadioFrequency, tacan: TacanChannel, callsign: str, @@ -593,7 +595,7 @@ class HelipadGenerator: logging.info("Generating helipad : " + name) pad = SingleHeliPad(name=(name + "_unit")) pad.position = Point(helipad.x, helipad.y) - pad.heading = helipad.heading + pad.heading = helipad.heading.degrees # pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign sg = unitgroup.StaticGroup(self.m.next_group_id(), name) sg.add_unit(pad) diff --git a/gen/missiles/scud_site.py b/gen/missiles/scud_site.py index ca7f9b94..c57b43e3 100644 --- a/gen/missiles/scud_site.py +++ b/gen/missiles/scud_site.py @@ -5,6 +5,7 @@ from dcs.vehicles import Unarmed, MissilesSS, AirDefence from game import Game from game.factions.faction import Faction from game.theater.theatergroundobject import MissileSiteGroundObject +from game.utils import Heading from gen.sam.group_generator import VehicleGroupGenerator @@ -63,5 +64,5 @@ class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]): "STRELA#0", self.position.x + 200, self.position.y + 15, - 90, + Heading.from_degrees(90), ) diff --git a/gen/missiles/v1_group.py b/gen/missiles/v1_group.py index 9d377754..e42a94fe 100644 --- a/gen/missiles/v1_group.py +++ b/gen/missiles/v1_group.py @@ -5,6 +5,7 @@ from dcs.vehicles import Unarmed, MissilesSS, AirDefence from game import Game from game.factions.faction import Faction from game.theater.theatergroundobject import MissileSiteGroundObject +from game.utils import Heading from gen.sam.group_generator import VehicleGroupGenerator @@ -65,5 +66,5 @@ class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]): "Blitz#0", self.position.x + 200, self.position.y + 15, - 90, + Heading.from_degrees(90), ) diff --git a/gen/runways.py b/gen/runways.py index dfb0cebe..ef9ab52f 100644 --- a/gen/runways.py +++ b/gen/runways.py @@ -8,6 +8,7 @@ from typing import Iterator, Optional from dcs.terrain.terrain import Airport from game.weather import Conditions +from game.utils import Heading from .airfields import AIRFIELD_DATA from .radios import RadioFrequency from .tacan import TacanChannel @@ -16,7 +17,7 @@ from .tacan import TacanChannel @dataclass(frozen=True) class RunwayData: airfield_name: str - runway_heading: int + runway_heading: Heading runway_name: str atc: Optional[RadioFrequency] = None tacan: Optional[TacanChannel] = None @@ -26,7 +27,7 @@ class RunwayData: @classmethod def for_airfield( - cls, airport: Airport, runway_heading: int, runway_name: str + cls, airport: Airport, runway_heading: Heading, runway_name: str ) -> RunwayData: """Creates RunwayData for the given runway of an airfield. @@ -66,12 +67,14 @@ class RunwayData: runway_number = runway.heading // 10 runway_side = ["", "L", "R"][runway.leftright] runway_name = f"{runway_number:02}{runway_side}" - yield cls.for_airfield(airport, runway.heading, runway_name) + yield cls.for_airfield( + airport, Heading.from_degrees(runway.heading), runway_name + ) # pydcs only exposes one runway per physical runway, so to expose # both sides of the runway we need to generate the other. - heading = (runway.heading + 180) % 360 - runway_number = heading // 10 + heading = Heading.from_degrees(runway.heading).opposite + runway_number = heading.degrees // 10 runway_side = ["", "R", "L"][runway.leftright] runway_name = f"{runway_number:02}{runway_side}" yield cls.for_airfield(airport, heading, runway_name) @@ -81,10 +84,10 @@ class RunwayAssigner: def __init__(self, conditions: Conditions): self.conditions = conditions - def angle_off_headwind(self, runway: RunwayData) -> int: - wind = self.conditions.weather.wind.at_0m.direction - ideal_heading = (wind + 180) % 360 - return abs(runway.runway_heading - ideal_heading) + def angle_off_headwind(self, runway: RunwayData) -> Heading: + wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction) + ideal_heading = wind.opposite + return runway.runway_heading.angle_between(ideal_heading) def get_preferred_runway(self, airport: Airport) -> RunwayData: """Returns the preferred runway for the given airport. diff --git a/gen/sam/aaa_flak.py b/gen/sam/aaa_flak.py index 68dee391..0e27a8d2 100644 --- a/gen/sam/aaa_flak.py +++ b/gen/sam/aaa_flak.py @@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import ( AirDefenseRange, AirDefenseGroupGenerator, ) +from game.utils import Heading GFLAK = [ AirDefence.Flak38, @@ -88,7 +89,7 @@ class FlakGenerator(AirDefenseGroupGenerator): "BLITZ#" + str(index), self.position.x + 125 + 15 * i + random.randint(1, 5), self.position.y + 15 * j + random.randint(1, 5), - 75, + Heading.from_degrees(75), ) @classmethod diff --git a/gen/sam/aaa_ww2_ally_flak.py b/gen/sam/aaa_ww2_ally_flak.py index 5fc18ddc..4eed42f4 100644 --- a/gen/sam/aaa_ww2_ally_flak.py +++ b/gen/sam/aaa_ww2_ally_flak.py @@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import ( AirDefenseRange, AirDefenseGroupGenerator, ) +from game.utils import Heading class AllyWW2FlakGenerator(AirDefenseGroupGenerator): @@ -53,28 +54,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator): "CMD#1", self.position.x, self.position.y - 20, - random.randint(0, 360), + Heading.random(), ) self.add_unit( Unarmed.M30_CC, "LOG#1", self.position.x, self.position.y + 20, - random.randint(0, 360), + Heading.random(), ) self.add_unit( Unarmed.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, - random.randint(0, 360), + Heading.random(), ) self.add_unit( Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, - random.randint(0, 360), + Heading.random(), ) @classmethod diff --git a/gen/sam/cold_war_flak.py b/gen/sam/cold_war_flak.py index 788482ec..bb538434 100644 --- a/gen/sam/cold_war_flak.py +++ b/gen/sam/cold_war_flak.py @@ -41,7 +41,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator): "SHO#1", self.position.x - 40, self.position.y - 40, - self.heading + 180, + self.heading.opposite, ), self.add_unit( AirDefence.S_60_Type59_Artillery, @@ -57,7 +57,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator): "SHO#3", self.position.x - 80, self.position.y - 40, - self.heading + 180, + self.heading.opposite, ), self.add_unit( AirDefence.ZU_23_Emplacement_Closed, @@ -113,7 +113,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator): "SHO#1", self.position.x - 40, self.position.y - 40, - self.heading + 180, + self.heading.opposite, ), self.add_unit( AirDefence.S_60_Type59_Artillery, @@ -129,7 +129,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator): "SHO#3", self.position.x - 80, self.position.y - 40, - self.heading + 180, + self.heading.opposite, ), self.add_unit( AirDefence.ZU_23_Emplacement_Closed, diff --git a/gen/sam/freya_ewr.py b/gen/sam/freya_ewr.py index 7c61a25c..e484d53e 100644 --- a/gen/sam/freya_ewr.py +++ b/gen/sam/freya_ewr.py @@ -4,6 +4,7 @@ from gen.sam.airdefensegroupgenerator import ( AirDefenseRange, AirDefenseGroupGenerator, ) +from game.utils import Heading class FreyaGenerator(AirDefenseGroupGenerator): @@ -101,7 +102,7 @@ class FreyaGenerator(AirDefenseGroupGenerator): "Inf#3", self.position.x + 20, self.position.y - 24, - self.heading + 45, + self.heading + Heading.from_degrees(45), ) @classmethod diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index 2fb800f8..e8137e19 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -16,6 +16,7 @@ from dcs.unittype import VehicleType, UnitType, ShipType from game.dcs.groundunittype import GroundUnitType from game.factions.faction import Faction from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject +from game.utils import Heading if TYPE_CHECKING: from game.game import Game @@ -37,7 +38,7 @@ class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]): self.game = game self.go = ground_object self.position = ground_object.position - self.heading = random.randint(0, 359) + self.heading: Heading = Heading.random() self.price = 0 self.vg: GroupT = group @@ -53,7 +54,7 @@ class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]): name: str, pos_x: float, pos_y: float, - heading: int, + heading: Heading, ) -> UnitT: return self.add_unit_to_group( self.vg, unit_type, name, Point(pos_x, pos_y), heading @@ -65,7 +66,7 @@ class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]): unit_type: UnitTypeT, name: str, position: Point, - heading: int, + heading: Heading, ) -> UnitT: raise NotImplementedError @@ -91,11 +92,11 @@ class VehicleGroupGenerator( unit_type: Type[VehicleType], name: str, position: Point, - heading: int, + heading: Heading, ) -> Vehicle: unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id) unit.position = position - unit.heading = heading + unit.heading = heading.degrees group.add_unit(unit) # get price of unit to calculate the real price of the whole group @@ -109,7 +110,7 @@ class VehicleGroupGenerator( def get_circular_position( self, num_units: int, launcher_distance: int, coverage: int = 90 - ) -> Iterable[tuple[float, float, int]]: + ) -> Iterable[tuple[float, float, Heading]]: """ Given a position on the map, array a group of units in a circle a uniform distance from the unit :param num_units: @@ -131,9 +132,9 @@ class VehicleGroupGenerator( positions = [] if num_units % 2 == 0: - current_offset = self.heading - ((coverage / (num_units - 1)) / 2) + current_offset = self.heading.degrees - ((coverage / (num_units - 1)) / 2) else: - current_offset = self.heading + current_offset = self.heading.degrees current_offset -= outer_offset * (math.ceil(num_units / 2) - 1) for _ in range(1, num_units + 1): x: float = self.position.x + launcher_distance * math.cos( @@ -142,8 +143,7 @@ class VehicleGroupGenerator( y: float = self.position.y + launcher_distance * math.sin( math.radians(current_offset) ) - heading = current_offset - positions.append((x, y, int(heading))) + positions.append((x, y, Heading.from_degrees(current_offset))) current_offset += outer_offset return positions @@ -172,10 +172,10 @@ class ShipGroupGenerator( unit_type: Type[ShipType], name: str, position: Point, - heading: int, + heading: Heading, ) -> Ship: unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type) unit.position = position - unit.heading = heading + unit.heading = heading.degrees group.add_unit(unit) return unit diff --git a/gen/visualgen.py b/gen/visualgen.py index 83be4859..3a11652e 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -86,7 +86,7 @@ class VisualGenerator: continue for offset in range(0, distance, self.game.settings.perf_smoke_spacing): - position = plane_start.point_from_heading(heading, offset) + position = plane_start.point_from_heading(heading.degrees, offset) for k, v in FRONT_SMOKE_TYPE_CHANCES.items(): if random.randint(0, 100) <= k: diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index fce7a5d9..24024bc1 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -417,12 +417,12 @@ class FrontLineJs(QObject): def extents(self) -> List[LeafletLatLon]: a = self.theater.point_to_ll( self.front_line.position.point_from_heading( - self.front_line.attack_heading + 90, nautical_miles(2).meters + self.front_line.attack_heading.right.degrees, nautical_miles(2).meters ) ) b = self.theater.point_to_ll( self.front_line.position.point_from_heading( - self.front_line.attack_heading + 270, nautical_miles(2).meters + self.front_line.attack_heading.left.degrees, nautical_miles(2).meters ) ) return [[a.latitude, a.longitude], [b.latitude, b.longitude]]