"""Battlefield front lines.""" from __future__ import annotations from dataclasses import dataclass import logging import json from pathlib import Path from itertools import tee from typing import Tuple, List, Union, Dict, Optional 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. """ frontline_data: Optional[Dict[str, ComplexFrontLine]] = None def __init__( self, control_point_a: ControlPoint, control_point_b: ControlPoint, ) -> None: self.control_point_a = control_point_a self.control_point_b = control_point_b self.segments: List[FrontLineSegment] = [] self._build_segments() self.name = f"Front line {control_point_a}/{control_point_b}" print(f"FRONTLINE SEGMENTS {len(self.segments)}") @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) @classmethod def load_json_frontlines(cls, terrain_name: str) -> None: try: path = Path(f"resources/frontlines/{terrain_name.lower()}.json") with open(path, "r") as file: logging.debug(f"Loading frontline from {path}...") data = json.load(file) cls.frontline_data = { frontline: ComplexFrontLine( data[frontline]["start_cp"], [Point(i[0], i[1]) for i in data[frontline]["points"]], ) for frontline in data } print(cls.frontline_data) except OSError: logging.warning(f"Unable to load preset frontlines for {terrain_name}") 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) -> 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.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