"""Battlefield front lines.""" from __future__ import annotations from dataclasses import dataclass import logging from itertools import tee from typing import Tuple, List, Union, Dict from dcs.mapping import Point from .controlpoint import ControlPoint, MissionTarget Numeric = Union[int, float] # TODO: Dedup by moving everything to using this class. FRONTLINE_MIN_CP_DISTANCE = 5000 def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) next(b, None) return zip(a, b) @dataclass class ComplexFrontLine: """ Stores data necessary for building a multi-segment frontline. "points" should be ordered from closest to farthest distance originating from start_cp.position """ start_cp: ControlPoint points: List[Point] # control_points: List[ControlPoint] @dataclass class FrontLineSegment: """ Describes a line segment of a FrontLine """ point_a: Point point_b: Point @property def attack_heading(self) -> Numeric: return self.point_a.heading_between_point(self.point_b) @property def attack_distance(self) -> Numeric: 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, control_point_a: ControlPoint, control_point_b: ControlPoint, frontline_data: Dict[str, ComplexFrontLine], ) -> None: self.control_point_a = control_point_a self.control_point_b = control_point_b self.segments: List[FrontLineSegment] = [] self._build_segments(frontline_data) self.name = f"Front line {control_point_a}/{control_point_b}" @property def position(self): return self._calculate_position() @property def control_points(self) -> Tuple[ControlPoint, ControlPoint]: """Returns a tuple of the two control points.""" return self.control_point_a, self.control_point_b @property def middle_point(self): self.point_from_a(self.attack_distance / 2) @property def attack_distance(self): return sum(i.attack_distance for i in self.segments) @property def attack_heading(self): return self.active_segment.attack_heading @property def active_segment(self) -> FrontLineSegment: """The FrontLine segment where there can be an active conflict""" if self._position_distance <= self.segments[0].attack_distance: return self.segments[0] remaining_dist = self._position_distance for segment in self.segments: if remaining_dist <= segment.attack_distance: return segment else: remaining_dist -= segment.attack_distance logging.error( "Frontline attack distance is greater than the sum of its segments" ) return self.segments[0] @property def _position_distance(self) -> float: """ The distance from point a where the conflict should occur according to the current strength of each control point """ total_strength = ( self.control_point_a.base.strength + self.control_point_b.base.strength ) if total_strength == 0: return self._adjust_for_min_dist(0) strength_pct = self.control_point_a.base.strength / total_strength return self._adjust_for_min_dist(strength_pct * self.attack_distance) def _calculate_position(self) -> Point: """ The position where the conflict should occur according to the current strength of each control point. """ return self.point_from_a(self._position_distance) def _build_segments(self, frontline_data: Dict[str, ComplexFrontLine]) -> None: control_point_ids = "|".join( [str(self.control_point_a.id), str(self.control_point_b.id)] ) reversed_cp_ids = "|".join( [str(self.control_point_b.id), str(self.control_point_a.id)] ) complex_frontlines = frontline_data if (complex_frontlines) and ( (control_point_ids in complex_frontlines) or (reversed_cp_ids in complex_frontlines) ): # The frontline segments must be stored in the correct order for the distance algorithms to work. # The points in the frontline are ordered from the id before the | to the id after. # First, check if control point id pair matches in order, and create segments if a match is found. if control_point_ids in complex_frontlines: point_pairs = pairwise(complex_frontlines[control_point_ids].points) for i in point_pairs: self.segments.append(FrontLineSegment(i[0], i[1])) # Check the reverse order and build in reverse if found. elif reversed_cp_ids in complex_frontlines: point_pairs = pairwise( reversed(complex_frontlines[reversed_cp_ids].points) ) for i in point_pairs: self.segments.append(FrontLineSegment(i[0], i[1])) # If no complex frontline has been configured, fall back to the old straight line method. else: self.segments.append( FrontLineSegment( self.control_point_a.position, self.control_point_b.position ) ) def _adjust_for_min_dist(self, distance: Numeric) -> Numeric: """ Ensures the frontline conflict is never located within the minimum distance constant of either end control point. """ if (distance > self.attack_distance / 2) and ( distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance ): distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE elif (distance < self.attack_distance / 2) and ( distance < FRONTLINE_MIN_CP_DISTANCE ): distance = FRONTLINE_MIN_CP_DISTANCE return distance def point_from_a(self, distance: Numeric) -> Point: """ Returns a point {distance} away from control_point_a along the frontline segments. """ if distance < self.segments[0].attack_distance: return self.control_point_a.position.point_from_heading( self.segments[0].attack_heading, distance ) remaining_dist = distance for segment in self.segments: if remaining_dist < segment.attack_distance: return self.control_point_a.position.point_from_heading( segment.attack_heading, remaining_dist ) else: remaining_dist -= segment.attack_distance