Eclipse/Druss99 31c80dfd02 refactor of previous commits
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
2025-10-19 19:34:38 +02:00

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