213 lines
7.2 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 ..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,
) -> None:
self.id = uuid.uuid4()
self.blue_cp = blue_point
self.red_cp = red_point
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: bool) -> ControlPoint:
if player:
return self.blue_cp
return self.red_cp
def control_point_hostile_to(self, player: bool) -> ControlPoint:
return self.control_point_friendly_to(not player)
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> 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:
return a, b
return b, a