Addresses #478, adding a heading class to represent headings and angles (#1387)

* Addresses #478, adding a heading class to represent headings and angles
Removed some unused code

* Fixing bad merge

* Formatting

* Fixing type issues and other merge resolution misses
This commit is contained in:
bgreman 2021-07-21 10:29:37 -04:00 committed by GitHub
parent fab550157a
commit 91d430085e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 296 additions and 189 deletions

View File

@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
from dcs import Point from dcs import Point
from game.utils import Heading
class PointWithHeading(Point): class PointWithHeading(Point):
def __init__(self) -> None: def __init__(self) -> None:
super(PointWithHeading, self).__init__(0, 0) super(PointWithHeading, self).__init__(0, 0)
self.heading = 0 self.heading: Heading = Heading.from_degrees(0)
@staticmethod @staticmethod
def from_point(point: Point, heading: int) -> PointWithHeading: def from_point(point: Point, heading: Heading) -> PointWithHeading:
p = PointWithHeading() p = PointWithHeading()
p.x = point.x p.x = point.x
p.y = point.y p.y = point.y

View File

@ -59,7 +59,7 @@ from ..point_with_heading import PointWithHeading
from ..positioned import Positioned from ..positioned import Positioned
from ..profiling import logged_duration from ..profiling import logged_duration
from ..scenery_group import SceneryGroup from ..scenery_group import SceneryGroup
from ..utils import Distance, meters from ..utils import Distance, Heading, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from . import TheaterGroundObject from . import TheaterGroundObject
@ -400,85 +400,113 @@ class MizCampaignLoader:
for static in self.offshore_strike_targets: for static in self.offshore_strike_targets:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.preset_locations.offshore_strike_locations.append( 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: for ship in self.ships:
closest, distance = self.objective_info(ship, allow_naval=True) closest, distance = self.objective_info(ship, allow_naval=True)
closest.preset_locations.ships.append( 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: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append( 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: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append( 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: for group in self.long_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.long_range_sams.append( 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: for group in self.medium_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.medium_range_sams.append( 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: for group in self.short_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.short_range_sams.append( 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: for group in self.aaa:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.aaa.append( 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: for group in self.ewrs:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append( 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: for group in self.armor_groups:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.armor_groups.append( 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: for static in self.helipads:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.helipads.append( 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: for static in self.factories:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.preset_locations.factories.append( 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: for static in self.ammunition_depots:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.preset_locations.ammunition_depots.append( 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: for static in self.strike_targets:
closest, distance = self.objective_info(static) closest, distance = self.objective_info(static)
closest.preset_locations.strike_locations.append( 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: for scenery_group in self.scenery:

View File

@ -35,6 +35,7 @@ from dcs.unit import Unit
from game import db from game import db
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
from game.utils import Heading
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
@ -335,7 +336,7 @@ class ControlPoint(MissionTarget, ABC):
@property @property
@abstractmethod @abstractmethod
def heading(self) -> int: def heading(self) -> Heading:
... ...
def __str__(self) -> str: def __str__(self) -> str:
@ -838,8 +839,8 @@ class Airfield(ControlPoint):
return len(self.airport.parking_slots) return len(self.airport.parking_slots)
@property @property
def heading(self) -> int: def heading(self) -> Heading:
return self.airport.runways[0].heading return Heading.from_degrees(self.airport.runways[0].heading)
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return not self.runway_status.damaged return not self.runway_status.damaged
@ -903,8 +904,8 @@ class NavalControlPoint(ControlPoint, ABC):
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
@property @property
def heading(self) -> int: def heading(self) -> Heading:
return 0 # TODO compute heading return Heading.from_degrees(0) # TODO compute heading
def find_main_tgo(self) -> GenericCarrierGroundObject: def find_main_tgo(self) -> GenericCarrierGroundObject:
for g in self.ground_objects: for g in self.ground_objects:
@ -933,7 +934,9 @@ class NavalControlPoint(ControlPoint, ABC):
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData: ) -> RunwayData:
# TODO: Assign TACAN and ICLS earlier so we don't need this. # 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) return dynamic_runways.get(self.name, fallback)
@property @property
@ -1071,14 +1074,16 @@ class OffMapSpawn(ControlPoint):
return True return True
@property @property
def heading(self) -> int: def heading(self) -> Heading:
return 0 return Heading.from_degrees(0)
def active_runway( def active_runway(
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData: ) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.") 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 @property
def runway_status(self) -> RunwayStatus: def runway_status(self) -> RunwayStatus:
@ -1120,7 +1125,9 @@ class Fob(ControlPoint):
self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData] self, conditions: Conditions, dynamic_runways: Dict[str, RunwayData]
) -> RunwayData: ) -> RunwayData:
logging.warning("TODO: FOBs have no runways.") 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 @property
def runway_status(self) -> RunwayStatus: def runway_status(self) -> RunwayStatus:
@ -1142,8 +1149,8 @@ class Fob(ControlPoint):
return False return False
@property @property
def heading(self) -> int: def heading(self) -> Heading:
return 0 return Heading.from_degrees(0)
@property @property
def can_deploy_ground_units(self) -> bool: def can_deploy_ground_units(self) -> bool:

View File

@ -11,7 +11,7 @@ from .controlpoint import (
ControlPoint, ControlPoint,
MissionTarget, MissionTarget,
) )
from ..utils import pairwise from ..utils import Heading, pairwise
FRONTLINE_MIN_CP_DISTANCE = 5000 FRONTLINE_MIN_CP_DISTANCE = 5000
@ -27,9 +27,9 @@ class FrontLineSegment:
point_b: Point point_b: Point
@property @property
def attack_heading(self) -> float: def attack_heading(self) -> Heading:
"""The heading of the frontline segment from player to enemy control point""" """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 @property
def attack_distance(self) -> float: def attack_distance(self) -> float:
@ -123,7 +123,7 @@ class FrontLine(MissionTarget):
return sum(i.attack_distance for i in self.segments) return sum(i.attack_distance for i in self.segments)
@property @property
def attack_heading(self) -> float: def attack_heading(self) -> Heading:
"""The heading of the active attack segment from player to enemy control point""" """The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading return self.active_segment.attack_heading
@ -150,13 +150,13 @@ class FrontLine(MissionTarget):
""" """
if distance < self.segments[0].attack_distance: if distance < self.segments[0].attack_distance:
return self.blue_cp.position.point_from_heading( return self.blue_cp.position.point_from_heading(
self.segments[0].attack_heading, distance self.segments[0].attack_heading.degrees, distance
) )
remaining_dist = distance remaining_dist = distance
for segment in self.segments: for segment in self.segments:
if remaining_dist < segment.attack_distance: if remaining_dist < segment.attack_distance:
return segment.point_a.point_from_heading( return segment.point_a.point_from_heading(
segment.attack_heading, remaining_dist segment.attack_heading.degrees, remaining_dist
) )
else: else:
remaining_dist -= segment.attack_distance remaining_dist -= segment.attack_distance

View File

@ -28,6 +28,7 @@ from game.theater.theatergroundobject import (
VehicleGroupGroundObject, VehicleGroupGroundObject,
CoastalSiteGroundObject, CoastalSiteGroundObject,
) )
from game.utils import Heading
from game.version import VERSION from game.version import VERSION
from gen import namegen from gen import namegen
from gen.coastal.coastal_group_generator import generate_coastal_group from gen.coastal.coastal_group_generator import generate_coastal_group
@ -385,7 +386,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id, group_id,
object_id, object_id,
position + template_point, position + template_point,
unit["heading"], Heading.from_degrees(unit["heading"]),
self.control_point, self.control_point,
unit["type"], unit["type"],
) )
@ -585,7 +586,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
group_id, group_id,
object_id, object_id,
point + template_point, point + template_point,
unit["heading"], Heading.from_degrees(unit["heading"]),
self.control_point, self.control_point,
unit["type"], unit["type"],
is_fob_structure=True, is_fob_structure=True,

View File

@ -17,7 +17,7 @@ from ..data.radar_db import (
TELARS, TELARS,
LAUNCHER_TRACKER_PAIRS, LAUNCHER_TRACKER_PAIRS,
) )
from ..utils import Distance, meters from ..utils import Distance, Heading, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
@ -58,7 +58,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
category: str, category: str,
group_id: int, group_id: int,
position: Point, position: Point,
heading: int, heading: Heading,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str, dcs_identifier: str,
sea_object: bool, sea_object: bool,
@ -222,7 +222,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
group_id: int, group_id: int,
object_id: int, object_id: int,
position: Point, position: Point,
heading: int, heading: Heading,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str, dcs_identifier: str,
is_fob_structure: bool = False, is_fob_structure: bool = False,
@ -310,7 +310,7 @@ class SceneryGroundObject(BuildingGroundObject):
group_id=group_id, group_id=group_id,
object_id=object_id, object_id=object_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
is_fob_structure=False, is_fob_structure=False,
@ -334,7 +334,7 @@ class FactoryGroundObject(BuildingGroundObject):
name: str, name: str,
group_id: int, group_id: int,
position: Point, position: Point,
heading: int, heading: Heading,
control_point: ControlPoint, control_point: ControlPoint,
) -> None: ) -> None:
super().__init__( super().__init__(
@ -385,7 +385,7 @@ class CarrierGroundObject(GenericCarrierGroundObject):
category="CARRIER", category="CARRIER",
group_id=group_id, group_id=group_id,
position=control_point.position, position=control_point.position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="CARRIER", dcs_identifier="CARRIER",
sea_object=True, sea_object=True,
@ -406,7 +406,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
category="LHA", category="LHA",
group_id=group_id, group_id=group_id,
position=control_point.position, position=control_point.position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="LHA", dcs_identifier="LHA",
sea_object=True, sea_object=True,
@ -428,7 +428,7 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
category="missile", category="missile",
group_id=group_id, group_id=group_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
sea_object=False, sea_object=False,
@ -450,7 +450,7 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
group_id: int, group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
heading: int, heading: Heading,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@ -497,7 +497,7 @@ class SamGroundObject(IadsGroundObject):
category="aa", category="aa",
group_id=group_id, group_id=group_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
sea_object=False, sea_object=False,
@ -565,7 +565,7 @@ class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
category="armor", category="armor",
group_id=group_id, group_id=group_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
sea_object=False, sea_object=False,
@ -593,7 +593,7 @@ class EwrGroundObject(IadsGroundObject):
category="ewr", category="ewr",
group_id=group_id, group_id=group_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="EWR", dcs_identifier="EWR",
sea_object=False, sea_object=False,
@ -627,7 +627,7 @@ class ShipGroundObject(NavalGroundObject):
category="ship", category="ship",
group_id=group_id, group_id=group_id,
position=position, position=position,
heading=0, heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
sea_object=True, sea_object=True,

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import itertools import itertools
import math import math
import random
from collections import Iterable from collections import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union, Any, TypeVar from typing import Union, Any, TypeVar
@ -20,15 +21,6 @@ INHG_TO_HPA = 33.86389
INHG_TO_MMHG = 25.400002776728 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) @dataclass(frozen=True, order=True)
class Distance: class Distance:
distance_in_meters: float distance_in_meters: float
@ -184,6 +176,60 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5) 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) @dataclass(frozen=True, order=True)
class Pressure: class Pressure:
pressure_in_inches_hg: float pressure_in_inches_hg: float

View File

@ -81,7 +81,7 @@ from game.theater.missiontarget import MissionTarget
from game.theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import MultiGroupTransport from game.transfers import MultiGroupTransport
from game.unitmap import UnitMap 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.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.flights.flight import ( from gen.flights.flight import (

View File

@ -17,6 +17,9 @@ from dcs.task import (
) )
from dcs.unittype import UnitType 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 .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE 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) alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) 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.red_cp.position.heading_between_point(
self.conflict.blue_cp.position self.conflict.blue_cp.position
) )
+ TANKER_HEADING_OFFSET * i + TANKER_HEADING_OFFSET * i
) )
tanker_position = player_cp.position.point_from_heading( tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE tanker_heading.degrees, TANKER_DISTANCE
) )
tanker_group = self.mission.refuel_flight( tanker_group = self.mission.refuel_flight(
country=country, country=country,

View File

@ -32,7 +32,7 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap 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 ( from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE, DISTANCE_FROM_FRONTLINE,
CombatGroup, CombatGroup,
@ -130,7 +130,7 @@ class GroundConflictGenerator:
self.player_stance, self.player_stance,
player_groups, player_groups,
enemy_groups, enemy_groups,
self.conflict.heading + 90, self.conflict.heading.right,
self.conflict.blue_cp, self.conflict.blue_cp,
self.conflict.red_cp, self.conflict.red_cp,
) )
@ -138,7 +138,7 @@ class GroundConflictGenerator:
self.enemy_stance, self.enemy_stance,
enemy_groups, enemy_groups,
player_groups, player_groups,
self.conflict.heading - 90, self.conflict.heading.left,
self.conflict.red_cp, self.conflict.red_cp,
self.conflict.blue_cp, self.conflict.blue_cp,
) )
@ -182,7 +182,11 @@ class GroundConflictGenerator:
) )
def gen_infantry_group_for_group( 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: ) -> None:
infantry_position = self.conflict.find_ground_position( infantry_position = self.conflict.find_ground_position(
@ -217,7 +221,7 @@ class GroundConflictGenerator:
u.dcs_unit_type, u.dcs_unit_type,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading.degrees,
move_formation=PointAction.OffRoad, move_formation=PointAction.OffRoad,
) )
return return
@ -244,7 +248,7 @@ class GroundConflictGenerator:
units[0].dcs_unit_type, units[0].dcs_unit_type,
position=infantry_position, position=infantry_position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading.degrees,
move_formation=PointAction.OffRoad, move_formation=PointAction.OffRoad,
) )
@ -256,17 +260,19 @@ class GroundConflictGenerator:
unit.dcs_unit_type, unit.dcs_unit_type,
position=position, position=position,
group_size=1, group_size=1,
heading=forward_heading, heading=forward_heading.degrees,
move_formation=PointAction.OffRoad, move_formation=PointAction.OffRoad,
) )
def _set_reform_waypoint( def _set_reform_waypoint(
self, dcs_group: VehicleGroup, forward_heading: int self, dcs_group: VehicleGroup, forward_heading: Heading
) -> None: ) -> None:
"""Setting a waypoint close to the spawn position allows the group to reform gracefully """Setting a waypoint close to the spawn position allows the group to reform gracefully
rather than spin 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) dcs_group.add_waypoint(reform_point)
def _plan_artillery_action( def _plan_artillery_action(
@ -274,7 +280,7 @@ class GroundConflictGenerator:
stance: CombatStance, stance: CombatStance,
gen_group: CombatGroup, gen_group: CombatGroup,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
forward_heading: int, forward_heading: Heading,
target: Point, target: Point,
) -> bool: ) -> bool:
""" """
@ -308,7 +314,7 @@ class GroundConflictGenerator:
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3) dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
) )
dcs_group.add_waypoint( 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, PointAction.OffRoad,
) )
dcs_group.points[2].tasks.append(Hold()) dcs_group.points[2].tasks.append(Hold())
@ -336,7 +342,7 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(artillery_fallback) self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units: 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 True
return False return False
@ -345,7 +351,7 @@ class GroundConflictGenerator:
stance: CombatStance, stance: CombatStance,
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
forward_heading: int, forward_heading: Heading,
to_cp: ControlPoint, to_cp: ControlPoint,
) -> bool: ) -> bool:
""" """
@ -378,9 +384,7 @@ class GroundConflictGenerator:
else: else:
# We use an offset heading here because DCS doesn't always # We use an offset heading here because DCS doesn't always
# force vehicles to move if there's no heading change. # force vehicles to move if there's no heading change.
offset_heading = forward_heading - 2 offset_heading = forward_heading - Heading.from_degrees(2)
if offset_heading < 0:
offset_heading = 358
attack_point = self.find_offensive_point( attack_point = self.find_offensive_point(
dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE dcs_group, offset_heading, AGGRESIVE_MOVE_DISTANCE
) )
@ -398,9 +402,7 @@ class GroundConflictGenerator:
else: else:
# We use an offset heading here because DCS doesn't always # We use an offset heading here because DCS doesn't always
# force vehicles to move if there's no heading change. # force vehicles to move if there's no heading change.
offset_heading = forward_heading - 1 offset_heading = forward_heading - Heading.from_degrees(1)
if offset_heading < 0:
offset_heading = 359
attack_point = self.find_offensive_point( attack_point = self.find_offensive_point(
dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE
) )
@ -436,7 +438,7 @@ class GroundConflictGenerator:
self, self,
stance: CombatStance, stance: CombatStance,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
forward_heading: int, forward_heading: Heading,
to_cp: ControlPoint, to_cp: ControlPoint,
) -> bool: ) -> bool:
""" """
@ -473,7 +475,7 @@ class GroundConflictGenerator:
stance: CombatStance, stance: CombatStance,
ally_groups: List[Tuple[VehicleGroup, CombatGroup]], ally_groups: List[Tuple[VehicleGroup, CombatGroup]],
enemy_groups: List[Tuple[VehicleGroup, CombatGroup]], enemy_groups: List[Tuple[VehicleGroup, CombatGroup]],
forward_heading: int, forward_heading: Heading,
from_cp: ControlPoint, from_cp: ControlPoint,
to_cp: ControlPoint, to_cp: ControlPoint,
) -> None: ) -> None:
@ -514,12 +516,14 @@ class GroundConflictGenerator:
else: else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading) retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading( reposition_point = retreat_point.point_from_heading(
forward_heading, 10 forward_heading.degrees, 10
) # Another point to make the unit face the enemy ) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OffRoad) dcs_group.add_waypoint(retreat_point, PointAction.OffRoad)
dcs_group.add_waypoint(reposition_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 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 # Force unit heading
for unit in dcs_group.units: for unit in dcs_group.units:
unit.heading = forward_heading unit.heading = forward_heading.degrees
dcs_group.manualHeading = True dcs_group.manualHeading = True
# We add a new retreat waypoint # We add a new retreat waypoint
@ -563,7 +567,7 @@ class GroundConflictGenerator:
def find_retreat_point( def find_retreat_point(
self, self,
dcs_group: VehicleGroup, dcs_group: VehicleGroup,
frontline_heading: int, frontline_heading: Heading,
distance: int = RETREAT_DISTANCE, distance: int = RETREAT_DISTANCE,
) -> Point: ) -> Point:
""" """
@ -573,14 +577,14 @@ class GroundConflictGenerator:
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading( 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): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
return self.conflict.theater.nearest_land_pos(desired_point) return self.conflict.theater.nearest_land_pos(desired_point)
def find_offensive_point( def find_offensive_point(
self, dcs_group: VehicleGroup, frontline_heading: int, distance: int self, dcs_group: VehicleGroup, frontline_heading: Heading, distance: int
) -> Point: ) -> Point:
""" """
Find a point to attack Find a point to attack
@ -590,7 +594,7 @@ class GroundConflictGenerator:
:return: dcs.mapping.Point object with the desired position :return: dcs.mapping.Point object with the desired position
""" """
desired_point = dcs_group.points[0].position.point_from_heading( 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): if self.conflict.theater.is_on_land(desired_point):
return desired_point return desired_point
@ -688,14 +692,14 @@ class GroundConflictGenerator:
conflict_position: Point, conflict_position: Point,
combat_width: int, combat_width: int,
distance_from_frontline: int, distance_from_frontline: int,
heading: int, heading: Heading,
spawn_heading: int, spawn_heading: Heading,
) -> Optional[Point]: ) -> Optional[Point]:
shifted = conflict_position.point_from_heading( 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( desired_point = shifted.point_from_heading(
spawn_heading, distance_from_frontline spawn_heading.degrees, distance_from_frontline
) )
return Conflict.find_ground_position( return Conflict.find_ground_position(
desired_point, combat_width, heading, self.conflict.theater desired_point, combat_width, heading, self.conflict.theater
@ -704,17 +708,13 @@ class GroundConflictGenerator:
def _generate_groups( def _generate_groups(
self, self,
groups: list[CombatGroup], groups: list[CombatGroup],
frontline_vector: Tuple[Point, int, int], frontline_vector: Tuple[Point, Heading, int],
is_player: bool, is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]: ) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them""" """Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = [] positioned_groups = []
position, heading, combat_width = frontline_vector position, heading, combat_width = frontline_vector
spawn_heading = ( spawn_heading = heading.left if is_player else heading.right
int(heading_sum(heading, -90))
if is_player
else int(heading_sum(heading, 90))
)
country = self.game.coalition_for(is_player).country_name country = self.game.coalition_for(is_player).country_name
for group in groups: for group in groups:
if group.role == CombatGroupRole.ARTILLERY: if group.role == CombatGroupRole.ARTILLERY:
@ -737,7 +737,7 @@ class GroundConflictGenerator:
group.unit_type, group.unit_type,
group.size, group.size,
final_position, final_position,
heading=opposite_heading(spawn_heading), heading=spawn_heading.opposite,
) )
if is_player: if is_player:
g.set_skill(Skill(self.game.settings.player_skill)) g.set_skill(Skill(self.game.settings.player_skill))
@ -750,7 +750,7 @@ class GroundConflictGenerator:
g, g,
is_player, is_player,
self.mission.country(country), self.mission.country(country),
opposite_heading(spawn_heading), spawn_heading.opposite,
) )
else: else:
logging.warning(f"Unable to get valid position for {group}") logging.warning(f"Unable to get valid position for {group}")
@ -764,7 +764,7 @@ class GroundConflictGenerator:
count: int, count: int,
at: Point, at: Point,
move_formation: PointAction = PointAction.OffRoad, move_formation: PointAction = PointAction.OffRoad,
heading: int = 0, heading: Heading = Heading.from_degrees(0),
) -> VehicleGroup: ) -> VehicleGroup:
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
@ -778,7 +778,7 @@ class GroundConflictGenerator:
unit_type.dcs_unit_type, unit_type.dcs_unit_type,
position=at, position=at,
group_size=count, group_size=count,
heading=heading, heading=heading.degrees,
move_formation=move_formation, move_formation=move_formation,
) )

View File

@ -3,6 +3,7 @@ from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import CoastalSiteGroundObject from game.theater.theatergroundobject import CoastalSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator from gen.sam.group_generator import VehicleGroupGenerator
@ -59,5 +60,5 @@ class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,
90, Heading.from_degrees(90),
) )

View File

@ -9,7 +9,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint
from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from game.utils import heading_sum, opposite_heading from game.utils import Heading
FRONTLINE_LENGTH = 80000 FRONTLINE_LENGTH = 80000
@ -25,7 +25,7 @@ class Conflict:
attackers_country: Country, attackers_country: Country,
defenders_country: Country, defenders_country: Country,
position: Point, position: Point,
heading: Optional[int] = None, heading: Optional[Heading] = None,
size: Optional[int] = None, size: Optional[int] = None,
): ):
@ -55,28 +55,28 @@ class Conflict:
@classmethod @classmethod
def frontline_position( def frontline_position(
cls, frontline: FrontLine, theater: ConflictTheater cls, frontline: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int]: ) -> Tuple[Point, Heading]:
attack_heading = int(frontline.attack_heading) attack_heading = frontline.attack_heading
position = cls.find_ground_position( position = cls.find_ground_position(
frontline.position, frontline.position,
FRONTLINE_LENGTH, FRONTLINE_LENGTH,
heading_sum(attack_heading, 90), attack_heading.right,
theater, theater,
) )
if position is None: if position is None:
raise RuntimeError("Could not find front line position") raise RuntimeError("Could not find front line position")
return position, opposite_heading(attack_heading) return position, attack_heading.opposite
@classmethod @classmethod
def frontline_vector( def frontline_vector(
cls, front_line: FrontLine, theater: ConflictTheater 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. Returns a vector for a valid frontline location avoiding exclusion zones.
""" """
center_position, heading = cls.frontline_position(front_line, theater) center_position, heading = cls.frontline_position(front_line, theater)
left_heading = heading_sum(heading, -90) left_heading = heading.left
right_heading = heading_sum(heading, 90) right_heading = heading.right
left_position = cls.extend_ground_position( left_position = cls.extend_ground_position(
center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater
) )
@ -113,10 +113,14 @@ class Conflict:
@classmethod @classmethod
def extend_ground_position( 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: ) -> Point:
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" """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: if theater.landmap is None:
# TODO: Why is this possible? # TODO: Why is this possible?
return extended return extended
@ -133,14 +137,14 @@ class Conflict:
return extended return extended
# Otherwise extend the front line only up to the intersection. # 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 @classmethod
def find_ground_position( def find_ground_position(
cls, cls,
initial: Point, initial: Point,
max_distance: int, max_distance: int,
heading: int, heading: Heading,
theater: ConflictTheater, theater: ConflictTheater,
coerce: bool = True, coerce: bool = True,
) -> Optional[Point]: ) -> Optional[Point]:
@ -153,10 +157,10 @@ class Conflict:
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
for distance in range(0, int(max_distance), 100): 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): if theater.is_on_land(pos):
return 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): if theater.is_on_land(pos):
return pos return pos
if coerce: if coerce:

View File

@ -1,6 +1,7 @@
import random import random
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from game.utils import Heading
from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
@ -54,7 +55,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
) )
# Add Ticonderoga escort # Add Ticonderoga escort
if self.heading >= 180: if self.heading >= Heading.from_degrees(180):
self.add_unit( self.add_unit(
TICONDEROG, TICONDEROG,
"USS Hué City", "USS Hué City",

View File

@ -37,8 +37,10 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
BuildingGroundObject, BuildingGroundObject,
) )
from game.threatzones import ThreatZones 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 .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@ -1151,10 +1153,11 @@ class FlightPlanBuilder:
""" """
assert self.package.waypoints is not None assert self.package.waypoints is not None
target = self.package.target.position target = self.package.target.position
heading = Heading.from_degrees(
heading = self.package.waypoints.join.heading_between_point(target) self.package.waypoints.join.heading_between_point(target)
)
start_pos = target.point_from_heading( start_pos = target.point_from_heading(
heading, -self.doctrine.sweep_distance.meters heading.degrees, -self.doctrine.sweep_distance.meters
) )
builder = WaypointBuilder(flight, self.coalition) builder = WaypointBuilder(flight, self.coalition)
@ -1249,7 +1252,9 @@ class FlightPlanBuilder:
else: else:
raise PlanningError("Could not find any enemy airfields") 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( position = ShapelyPoint(
self.package.target.position.x, self.package.target.position.y self.package.target.position.x, self.package.target.position.y
@ -1285,20 +1290,20 @@ class FlightPlanBuilder:
) )
end = location.position.point_from_heading( end = location.position.point_from_heading(
heading, heading.degrees,
random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)), random.randint(int(min_cap_distance.meters), int(max_cap_distance.meters)),
) )
diameter = random.randint( diameter = random.randint(
int(self.doctrine.cap_min_track_length.meters), int(self.doctrine.cap_min_track_length.meters),
int(self.doctrine.cap_max_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 return start, end
def aewc_orbit(self, location: MissionTarget) -> Point: def aewc_orbit(self, location: MissionTarget) -> Point:
closest_boundary = self.threat_zones.closest_boundary(location.position) closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point( heading_to_threat_boundary = Heading.from_degrees(
closest_boundary location.position.heading_between_point(closest_boundary)
) )
distance_to_threat = meters( distance_to_threat = meters(
location.position.distance_to_point(closest_boundary) location.position.distance_to_point(closest_boundary)
@ -1312,7 +1317,7 @@ class FlightPlanBuilder:
orbit_distance = distance_to_threat - threat_buffer orbit_distance = distance_to_threat - threat_buffer
return location.position.point_from_heading( return location.position.point_from_heading(
orbit_heading, orbit_distance.meters orbit_heading.degrees, orbit_distance.meters
) )
def racetrack_for_frontline( def racetrack_for_frontline(
@ -1320,9 +1325,9 @@ class FlightPlanBuilder:
) -> Tuple[Point, Point]: ) -> Tuple[Point, Point]:
# Find targets waypoints # Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater) 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( orbit_center = center.point_from_heading(
heading - 90, heading.left.degrees,
random.randint( random.randint(
int(nautical_miles(6).meters), int(nautical_miles(15).meters) int(nautical_miles(6).meters), int(nautical_miles(15).meters)
), ),
@ -1335,8 +1340,8 @@ class FlightPlanBuilder:
combat_width = 35000 combat_width = 35000
radius = combat_width * 1.25 radius = combat_width * 1.25
start = orbit_center.point_from_heading(heading, radius) start = orbit_center.point_from_heading(heading.degrees, radius)
end = orbit_center.point_from_heading(heading + 180, radius) end = orbit_center.point_from_heading(heading.opposite.degrees, radius)
if end.distance_to_point(origin) < start.distance_to_point(origin): if end.distance_to_point(origin) < start.distance_to_point(origin):
start, end = end, start start, end = end, start
@ -1530,8 +1535,8 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector(location, self.theater) ingress, heading, distance = Conflict.frontline_vector(location, self.theater)
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading.degrees, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading.degrees, distance)
ingress_distance = ingress.distance_to_point(flight.departure.position) ingress_distance = ingress.distance_to_point(flight.departure.position)
egress_distance = egress.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 location = self.package.target
closest_boundary = self.threat_zones.closest_boundary(location.position) closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point( heading_to_threat_boundary = Heading.from_degrees(
closest_boundary location.position.heading_between_point(closest_boundary)
) )
distance_to_threat = meters( distance_to_threat = meters(
location.position.distance_to_point(closest_boundary) location.position.distance_to_point(closest_boundary)
@ -1582,16 +1587,16 @@ class FlightPlanBuilder:
orbit_distance = distance_to_threat - threat_buffer orbit_distance = distance_to_threat - threat_buffer
racetrack_center = location.position.point_from_heading( 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_half_distance = Distance.from_nautical_miles(20).meters
racetrack_start = racetrack_center.point_from_heading( 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( 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) builder = WaypointBuilder(flight, self.coalition)

View File

@ -55,7 +55,7 @@ from game.theater.theatergroundobject import (
SceneryGroundObject, SceneryGroundObject,
) )
from game.unitmap import UnitMap 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 .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
@ -166,7 +166,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
if targets: if targets:
target = random.choice(targets) target = random.choice(targets)
real_target = target.point_from_heading( 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)) vg.points[0].add_task(FireAtPoint(real_target))
logging.info("Set up fire task for missile group.") logging.info("Set up fire task for missile group.")
@ -246,7 +246,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
name=self.ground_object.group_name, name=self.ground_object.group_name,
_type=unit_type, _type=unit_type,
position=self.ground_object.position, position=self.ground_object.position,
heading=self.ground_object.heading, heading=self.ground_object.heading.degrees,
) )
self._register_fortification(group) self._register_fortification(group)
@ -256,7 +256,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
name=self.ground_object.group_name, name=self.ground_object.group_name,
_type=static_type, _type=static_type,
position=self.ground_object.position, position=self.ground_object.position,
heading=self.ground_object.heading, heading=self.ground_object.heading.degrees,
dead=self.ground_object.is_dead, dead=self.ground_object.is_dead,
) )
self._register_building(group) self._register_building(group)
@ -387,7 +387,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
# time as the recovery window. # time as the recovery window.
brc = self.steam_into_wind(ship_group) brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls) 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) self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]: def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
@ -422,14 +424,14 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
ship.set_frequency(atc_channel.hertz) ship.set_frequency(atc_channel.hertz)
return ship return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[int]: def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m wind = self.game.conditions.weather.wind.at_0m.direction
brc = wind.direction + 180 brc = Heading.from_degrees(wind.direction).opposite
# Aim for 25kts over the deck. # Aim for 25kts over the deck.
carrier_speed = knots(25) - mps(wind.speed) carrier_speed = knots(25) - mps(wind.speed)
for attempt in range(5): for attempt in range(5):
point = group.points[0].position.point_from_heading( 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): if self.game.theater.is_in_sea(point):
group.points[0].speed = carrier_speed.meters_per_second group.points[0].speed = carrier_speed.meters_per_second
@ -459,7 +461,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
def add_runway_data( def add_runway_data(
self, self,
brc: int, brc: Heading,
atc: RadioFrequency, atc: RadioFrequency,
tacan: TacanChannel, tacan: TacanChannel,
callsign: str, callsign: str,
@ -593,7 +595,7 @@ class HelipadGenerator:
logging.info("Generating helipad : " + name) logging.info("Generating helipad : " + name)
pad = SingleHeliPad(name=(name + "_unit")) pad = SingleHeliPad(name=(name + "_unit"))
pad.position = Point(helipad.x, helipad.y) 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 # pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
sg = unitgroup.StaticGroup(self.m.next_group_id(), name) sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad) sg.add_unit(pad)

View File

@ -5,6 +5,7 @@ from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject from game.theater.theatergroundobject import MissileSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator from gen.sam.group_generator import VehicleGroupGenerator
@ -63,5 +64,5 @@ class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,
90, Heading.from_degrees(90),
) )

View File

@ -5,6 +5,7 @@ from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from game import Game from game import Game
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject from game.theater.theatergroundobject import MissileSiteGroundObject
from game.utils import Heading
from gen.sam.group_generator import VehicleGroupGenerator from gen.sam.group_generator import VehicleGroupGenerator
@ -65,5 +66,5 @@ class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
"Blitz#0", "Blitz#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,
90, Heading.from_degrees(90),
) )

View File

@ -8,6 +8,7 @@ from typing import Iterator, Optional
from dcs.terrain.terrain import Airport from dcs.terrain.terrain import Airport
from game.weather import Conditions from game.weather import Conditions
from game.utils import Heading
from .airfields import AIRFIELD_DATA from .airfields import AIRFIELD_DATA
from .radios import RadioFrequency from .radios import RadioFrequency
from .tacan import TacanChannel from .tacan import TacanChannel
@ -16,7 +17,7 @@ from .tacan import TacanChannel
@dataclass(frozen=True) @dataclass(frozen=True)
class RunwayData: class RunwayData:
airfield_name: str airfield_name: str
runway_heading: int runway_heading: Heading
runway_name: str runway_name: str
atc: Optional[RadioFrequency] = None atc: Optional[RadioFrequency] = None
tacan: Optional[TacanChannel] = None tacan: Optional[TacanChannel] = None
@ -26,7 +27,7 @@ class RunwayData:
@classmethod @classmethod
def for_airfield( def for_airfield(
cls, airport: Airport, runway_heading: int, runway_name: str cls, airport: Airport, runway_heading: Heading, runway_name: str
) -> RunwayData: ) -> RunwayData:
"""Creates RunwayData for the given runway of an airfield. """Creates RunwayData for the given runway of an airfield.
@ -66,12 +67,14 @@ class RunwayData:
runway_number = runway.heading // 10 runway_number = runway.heading // 10
runway_side = ["", "L", "R"][runway.leftright] runway_side = ["", "L", "R"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}" 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 # pydcs only exposes one runway per physical runway, so to expose
# both sides of the runway we need to generate the other. # both sides of the runway we need to generate the other.
heading = (runway.heading + 180) % 360 heading = Heading.from_degrees(runway.heading).opposite
runway_number = heading // 10 runway_number = heading.degrees // 10
runway_side = ["", "R", "L"][runway.leftright] runway_side = ["", "R", "L"][runway.leftright]
runway_name = f"{runway_number:02}{runway_side}" runway_name = f"{runway_number:02}{runway_side}"
yield cls.for_airfield(airport, heading, runway_name) yield cls.for_airfield(airport, heading, runway_name)
@ -81,10 +84,10 @@ class RunwayAssigner:
def __init__(self, conditions: Conditions): def __init__(self, conditions: Conditions):
self.conditions = conditions self.conditions = conditions
def angle_off_headwind(self, runway: RunwayData) -> int: def angle_off_headwind(self, runway: RunwayData) -> Heading:
wind = self.conditions.weather.wind.at_0m.direction wind = Heading.from_degrees(self.conditions.weather.wind.at_0m.direction)
ideal_heading = (wind + 180) % 360 ideal_heading = wind.opposite
return abs(runway.runway_heading - ideal_heading) return runway.runway_heading.angle_between(ideal_heading)
def get_preferred_runway(self, airport: Airport) -> RunwayData: def get_preferred_runway(self, airport: Airport) -> RunwayData:
"""Returns the preferred runway for the given airport. """Returns the preferred runway for the given airport.

View File

@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange, AirDefenseRange,
AirDefenseGroupGenerator, AirDefenseGroupGenerator,
) )
from game.utils import Heading
GFLAK = [ GFLAK = [
AirDefence.Flak38, AirDefence.Flak38,
@ -88,7 +89,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
"BLITZ#" + str(index), "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5), self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5), self.position.y + 15 * j + random.randint(1, 5),
75, Heading.from_degrees(75),
) )
@classmethod @classmethod

View File

@ -6,6 +6,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange, AirDefenseRange,
AirDefenseGroupGenerator, AirDefenseGroupGenerator,
) )
from game.utils import Heading
class AllyWW2FlakGenerator(AirDefenseGroupGenerator): class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
@ -53,28 +54,28 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
"CMD#1", "CMD#1",
self.position.x, self.position.x,
self.position.y - 20, self.position.y - 20,
random.randint(0, 360), Heading.random(),
) )
self.add_unit( self.add_unit(
Unarmed.M30_CC, Unarmed.M30_CC,
"LOG#1", "LOG#1",
self.position.x, self.position.x,
self.position.y + 20, self.position.y + 20,
random.randint(0, 360), Heading.random(),
) )
self.add_unit( self.add_unit(
Unarmed.M4_Tractor, Unarmed.M4_Tractor,
"LOG#2", "LOG#2",
self.position.x + 20, self.position.x + 20,
self.position.y, self.position.y,
random.randint(0, 360), Heading.random(),
) )
self.add_unit( self.add_unit(
Unarmed.Bedford_MWD, Unarmed.Bedford_MWD,
"LOG#3", "LOG#3",
self.position.x - 20, self.position.x - 20,
self.position.y, self.position.y,
random.randint(0, 360), Heading.random(),
) )
@classmethod @classmethod

View File

@ -41,7 +41,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#1", "SHO#1",
self.position.x - 40, self.position.x - 40,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading.opposite,
), ),
self.add_unit( self.add_unit(
AirDefence.S_60_Type59_Artillery, AirDefence.S_60_Type59_Artillery,
@ -57,7 +57,7 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#3", "SHO#3",
self.position.x - 80, self.position.x - 80,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading.opposite,
), ),
self.add_unit( self.add_unit(
AirDefence.ZU_23_Emplacement_Closed, AirDefence.ZU_23_Emplacement_Closed,
@ -113,7 +113,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#1", "SHO#1",
self.position.x - 40, self.position.x - 40,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading.opposite,
), ),
self.add_unit( self.add_unit(
AirDefence.S_60_Type59_Artillery, AirDefence.S_60_Type59_Artillery,
@ -129,7 +129,7 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"SHO#3", "SHO#3",
self.position.x - 80, self.position.x - 80,
self.position.y - 40, self.position.y - 40,
self.heading + 180, self.heading.opposite,
), ),
self.add_unit( self.add_unit(
AirDefence.ZU_23_Emplacement_Closed, AirDefence.ZU_23_Emplacement_Closed,

View File

@ -4,6 +4,7 @@ from gen.sam.airdefensegroupgenerator import (
AirDefenseRange, AirDefenseRange,
AirDefenseGroupGenerator, AirDefenseGroupGenerator,
) )
from game.utils import Heading
class FreyaGenerator(AirDefenseGroupGenerator): class FreyaGenerator(AirDefenseGroupGenerator):
@ -101,7 +102,7 @@ class FreyaGenerator(AirDefenseGroupGenerator):
"Inf#3", "Inf#3",
self.position.x + 20, self.position.x + 20,
self.position.y - 24, self.position.y - 24,
self.heading + 45, self.heading + Heading.from_degrees(45),
) )
@classmethod @classmethod

View File

@ -16,6 +16,7 @@ from dcs.unittype import VehicleType, UnitType, ShipType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject
from game.utils import Heading
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -37,7 +38,7 @@ class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading: Heading = Heading.random()
self.price = 0 self.price = 0
self.vg: GroupT = group self.vg: GroupT = group
@ -53,7 +54,7 @@ class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
name: str, name: str,
pos_x: float, pos_x: float,
pos_y: float, pos_y: float,
heading: int, heading: Heading,
) -> UnitT: ) -> UnitT:
return self.add_unit_to_group( return self.add_unit_to_group(
self.vg, unit_type, name, Point(pos_x, pos_y), heading 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, unit_type: UnitTypeT,
name: str, name: str,
position: Point, position: Point,
heading: int, heading: Heading,
) -> UnitT: ) -> UnitT:
raise NotImplementedError raise NotImplementedError
@ -91,11 +92,11 @@ class VehicleGroupGenerator(
unit_type: Type[VehicleType], unit_type: Type[VehicleType],
name: str, name: str,
position: Point, position: Point,
heading: int, heading: Heading,
) -> Vehicle: ) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id) unit = Vehicle(self.game.next_unit_id(), f"{group.name}|{name}", unit_type.id)
unit.position = position unit.position = position
unit.heading = heading unit.heading = heading.degrees
group.add_unit(unit) group.add_unit(unit)
# get price of unit to calculate the real price of the whole group # get price of unit to calculate the real price of the whole group
@ -109,7 +110,7 @@ class VehicleGroupGenerator(
def get_circular_position( def get_circular_position(
self, num_units: int, launcher_distance: int, coverage: int = 90 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 Given a position on the map, array a group of units in a circle a uniform distance from the unit
:param num_units: :param num_units:
@ -131,9 +132,9 @@ class VehicleGroupGenerator(
positions = [] positions = []
if num_units % 2 == 0: 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: else:
current_offset = self.heading current_offset = self.heading.degrees
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1) current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for _ in range(1, num_units + 1): for _ in range(1, num_units + 1):
x: float = self.position.x + launcher_distance * math.cos( x: float = self.position.x + launcher_distance * math.cos(
@ -142,8 +143,7 @@ class VehicleGroupGenerator(
y: float = self.position.y + launcher_distance * math.sin( y: float = self.position.y + launcher_distance * math.sin(
math.radians(current_offset) math.radians(current_offset)
) )
heading = current_offset positions.append((x, y, Heading.from_degrees(current_offset)))
positions.append((x, y, int(heading)))
current_offset += outer_offset current_offset += outer_offset
return positions return positions
@ -172,10 +172,10 @@ class ShipGroupGenerator(
unit_type: Type[ShipType], unit_type: Type[ShipType],
name: str, name: str,
position: Point, position: Point,
heading: int, heading: Heading,
) -> Ship: ) -> Ship:
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type) unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
unit.position = position unit.position = position
unit.heading = heading unit.heading = heading.degrees
group.add_unit(unit) group.add_unit(unit)
return unit return unit

View File

@ -86,7 +86,7 @@ class VisualGenerator:
continue continue
for offset in range(0, distance, self.game.settings.perf_smoke_spacing): 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(): for k, v in FRONT_SMOKE_TYPE_CHANCES.items():
if random.randint(0, 100) <= k: if random.randint(0, 100) <= k:

View File

@ -417,12 +417,12 @@ class FrontLineJs(QObject):
def extents(self) -> List[LeafletLatLon]: def extents(self) -> List[LeafletLatLon]:
a = self.theater.point_to_ll( a = self.theater.point_to_ll(
self.front_line.position.point_from_heading( 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( b = self.theater.point_to_ll(
self.front_line.position.point_from_heading( 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]] return [[a.latitude, a.longitude], [b.latitude, b.longitude]]