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 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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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 (

View File

@ -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,

View File

@ -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,
)

View File

@ -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),
)

View File

@ -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:

View File

@ -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",

View File

@ -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)

View File

@ -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)

View File

@ -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),
)

View File

@ -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),
)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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]]