mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
refactor to enum typing and many other fixes fix tests attempt to fix some typescript more typescript fixes more typescript test fixes revert all API changes update to pydcs mypy fixes Use properties to check if player is blue/red/neutral update requirements.txt black -_- bump pydcs and fix mypy add opponent property bump pydcs
220 lines
7.4 KiB
Python
220 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
|
|
|
|
from dcs.mapping import Point
|
|
|
|
from .missiontarget import MissionTarget
|
|
from .player import Player
|
|
from ..lasercodes.lasercode import LaserCode
|
|
from ..utils import Heading, pairwise
|
|
|
|
if TYPE_CHECKING:
|
|
from game.ato import FlightType
|
|
from .controlpoint import ControlPoint, Coalition
|
|
|
|
|
|
FRONTLINE_MIN_CP_DISTANCE = 5000
|
|
|
|
|
|
@dataclass
|
|
class FrontLineSegment:
|
|
"""
|
|
Describes a line segment of a FrontLine
|
|
"""
|
|
|
|
point_a: Point
|
|
point_b: Point
|
|
|
|
@property
|
|
def blue_forward_heading(self) -> Heading:
|
|
"""The heading toward the start of the next red segment or red base."""
|
|
return Heading.from_degrees(self.point_a.heading_between_point(self.point_b))
|
|
|
|
@property
|
|
def length(self) -> float:
|
|
"""Length of the segment"""
|
|
return self.point_a.distance_to_point(self.point_b)
|
|
|
|
|
|
class FrontLine(MissionTarget):
|
|
"""Defines a front line location between two control points.
|
|
Front lines are the area where ground combat happens.
|
|
Overwrites the entirety of MissionTarget __init__ method to allow for
|
|
dynamic position calculation.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
blue_point: ControlPoint,
|
|
red_point: ControlPoint,
|
|
laser_code: LaserCode,
|
|
) -> None:
|
|
self.id = uuid.uuid4()
|
|
self.blue_cp = blue_point
|
|
self.red_cp = red_point
|
|
self.laser_code = laser_code
|
|
try:
|
|
route = list(blue_point.convoy_route_to(red_point))
|
|
except KeyError:
|
|
# Some campaigns are air only and the mission generator currently relies on
|
|
# *some* "front line" being drawn between these two. In this case there will
|
|
# be no supply route to follow. Just create an arbitrary route between the
|
|
# two points.
|
|
route = [blue_point.position, red_point.position]
|
|
# Snap the beginning and end points to the CPs rather than the convoy waypoints,
|
|
# which are on roads.
|
|
route[0] = blue_point.position
|
|
route[-1] = red_point.position
|
|
self.segments: List[FrontLineSegment] = [
|
|
FrontLineSegment(a, b) for a, b in pairwise(route)
|
|
]
|
|
super().__init__(
|
|
f"Front line {blue_point}/{red_point}", self._compute_position()
|
|
)
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
if not isinstance(other, FrontLine):
|
|
return False
|
|
return (self.blue_cp, self.red_cp) == (other.blue_cp, other.red_cp)
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(id(self))
|
|
|
|
def _compute_position(self) -> Point:
|
|
return self.point_along_route_from_blue(self._blue_route_progress)
|
|
|
|
def update_position(self) -> None:
|
|
self.position = self._compute_position()
|
|
|
|
def control_point_friendly_to(self, player: Player) -> ControlPoint:
|
|
if player.is_blue:
|
|
return self.blue_cp
|
|
return self.red_cp
|
|
|
|
def control_point_hostile_to(self, player: Player) -> ControlPoint:
|
|
if player.is_blue:
|
|
return self.red_cp
|
|
else:
|
|
return self.blue_cp
|
|
|
|
def is_friendly(self, to_player: Player) -> bool:
|
|
"""Returns True if the objective is in friendly territory."""
|
|
return False
|
|
|
|
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
|
|
from game.ato import FlightType
|
|
|
|
yield from [
|
|
FlightType.CAS,
|
|
FlightType.AEWC,
|
|
FlightType.REFUELING,
|
|
# TODO: FlightType.TROOP_TRANSPORT
|
|
# TODO: FlightType.EVAC
|
|
]
|
|
yield from super().mission_types(for_player)
|
|
|
|
@property
|
|
def points(self) -> Iterator[Point]:
|
|
yield self.segments[0].point_a
|
|
for segment in self.segments:
|
|
yield segment.point_b
|
|
|
|
@property
|
|
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
|
"""Returns a tuple of the two control points."""
|
|
return self.blue_cp, self.red_cp
|
|
|
|
@property
|
|
def coalition(self) -> Coalition:
|
|
return self.blue_cp.coalition
|
|
|
|
@property
|
|
def route_length(self) -> float:
|
|
"""The total distance of all segments"""
|
|
return sum(i.length for i in self.segments)
|
|
|
|
@property
|
|
def blue_forward_heading(self) -> Heading:
|
|
"""The heading toward the start of the next red segment or red base."""
|
|
return self.active_segment.blue_forward_heading
|
|
|
|
@property
|
|
def active_segment(self) -> FrontLineSegment:
|
|
"""The FrontLine segment where there can be an active conflict"""
|
|
if self._blue_route_progress <= self.segments[0].length:
|
|
return self.segments[0]
|
|
|
|
distance_to_segment = self._blue_route_progress
|
|
for segment in self.segments:
|
|
if distance_to_segment <= segment.length:
|
|
return segment
|
|
else:
|
|
distance_to_segment -= segment.length
|
|
logging.error(
|
|
"Frontline attack distance is greater than the sum of its segments"
|
|
)
|
|
return self.segments[0]
|
|
|
|
def point_along_route_from_blue(self, distance: float) -> Point:
|
|
"""Returns a point {distance} away from control_point_a along the route."""
|
|
if distance < self.segments[0].length:
|
|
return self.blue_cp.position.point_from_heading(
|
|
self.segments[0].blue_forward_heading.degrees, distance
|
|
)
|
|
remaining_dist = distance
|
|
for segment in self.segments:
|
|
if remaining_dist < segment.length:
|
|
return segment.point_a.point_from_heading(
|
|
segment.blue_forward_heading.degrees, remaining_dist
|
|
)
|
|
else:
|
|
remaining_dist -= segment.length
|
|
raise RuntimeError(
|
|
f"Could not find front line point {distance} from {self.blue_cp}"
|
|
)
|
|
|
|
@property
|
|
def _blue_route_progress(self) -> float:
|
|
"""
|
|
The distance from point "a" where the conflict should occur
|
|
according to the current strength of each control point
|
|
"""
|
|
total_strength = self.blue_cp.base.strength + self.red_cp.base.strength
|
|
if self.blue_cp.base.strength == 0:
|
|
return self._adjust_for_min_dist(0)
|
|
if self.red_cp.base.strength == 0:
|
|
return self._adjust_for_min_dist(self.route_length)
|
|
strength_pct = self.blue_cp.base.strength / total_strength
|
|
return self._adjust_for_min_dist(strength_pct * self.route_length)
|
|
|
|
def _adjust_for_min_dist(self, distance: float) -> float:
|
|
"""
|
|
Ensures the frontline conflict is never located within the minimum distance
|
|
constant of either end control point.
|
|
"""
|
|
if (distance > self.route_length / 2) and (
|
|
distance + FRONTLINE_MIN_CP_DISTANCE > self.route_length
|
|
):
|
|
distance = self.route_length - FRONTLINE_MIN_CP_DISTANCE
|
|
elif (distance < self.route_length / 2) and (
|
|
distance < FRONTLINE_MIN_CP_DISTANCE
|
|
):
|
|
distance = FRONTLINE_MIN_CP_DISTANCE
|
|
return distance
|
|
|
|
@staticmethod
|
|
def sort_control_points(
|
|
a: ControlPoint, b: ControlPoint
|
|
) -> tuple[ControlPoint, ControlPoint]:
|
|
if a.is_friendly_to(b):
|
|
raise ValueError(
|
|
"Cannot sort control points that are friendly to each other"
|
|
)
|
|
if a.captured.is_blue:
|
|
return a, b
|
|
return b, a
|