from __future__ import annotations from dataclasses import dataclass from functools import cached_property from typing import Optional, Tuple from dcs.mapping import Point from shapely.geometry import LineString, Point as ShapelyPoint from shapely.ops import nearest_points from game.settings import Settings from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.controlpoint import ControlPoint from game.utils import Heading, dcs_to_shapely_point @dataclass(frozen=True) class FrontLineBounds: left_position: Point right_position: Point @cached_property def length(self) -> int: return int(self.left_position.distance_to_point(self.right_position)) @cached_property def center(self) -> Point: return (self.left_position + self.right_position) / 2 @cached_property def heading_from_left_to_right(self) -> Heading: return Heading( int(self.left_position.heading_between_point(self.right_position)) ) class FrontLineConflictDescription: def __init__( self, theater: ConflictTheater, front_line: FrontLine, position: Point, heading: Optional[Heading] = None, size: Optional[int] = None, ): self.front_line = front_line self.theater = theater self.position = position self.heading = heading self.size = size @property def blue_cp(self) -> ControlPoint: return self.front_line.blue_cp @property def red_cp(self) -> ControlPoint: return self.front_line.red_cp @classmethod def frontline_position( cls, frontline: FrontLine, theater: ConflictTheater, settings: Settings ) -> Tuple[Point, Heading]: attack_heading = frontline.blue_forward_heading position = cls.find_ground_position( frontline.position, settings.max_frontline_width * 1000, attack_heading.right, theater, ) return position, attack_heading.opposite @classmethod def frontline_bounds( cls, front_line: FrontLine, theater: ConflictTheater ) -> FrontLineBounds: """ Returns a vector for a valid frontline location avoiding exclusion zones. """ settings = front_line.coalition.game.settings center_position, heading = cls.frontline_position(front_line, theater, settings) left_heading = heading.left right_heading = heading.right left_position = cls.extend_ground_position( center_position, int(settings.max_frontline_width * 1000 / 2), left_heading, theater, ) right_position = cls.extend_ground_position( center_position, int(settings.max_frontline_width * 1000 / 2), right_heading, theater, ) return FrontLineBounds(left_position, right_position) @classmethod def frontline_cas_conflict( cls, front_line: FrontLine, theater: ConflictTheater ) -> FrontLineConflictDescription: # TODO: Break apart the front-line and air conflict descriptions. # We're wastefully not caching the front-line bounds here because air conflicts # can't compute bounds, only a position. bounds = cls.frontline_bounds(front_line, theater) conflict = cls( theater=theater, front_line=front_line, position=bounds.left_position, heading=bounds.heading_from_left_to_right, size=bounds.length, ) return conflict @classmethod def extend_ground_position( cls, initial: Point, max_distance: int, heading: Heading, theater: ConflictTheater, ) -> Point: """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" extended = initial.point_from_heading(heading.degrees, max_distance) if theater.landmap is None: # TODO: Why is this possible? return extended p0 = ShapelyPoint(initial.x, initial.y) p1 = ShapelyPoint(extended.x, extended.y) line = LineString([p0, p1]) intersection = line.intersection(theater.landmap.inclusion_zone_only.boundary) if intersection.is_empty: # Max extent does not intersect with the boundary of the inclusion # zone, so the full front line is usable. This does assume that the # front line was centered on a valid location. return extended # Otherwise extend the front line only up to the intersection. return initial.point_from_heading(heading.degrees, p0.distance(intersection)) @classmethod def find_ground_position( cls, initial: Point, max_distance: int, heading: Heading, theater: ConflictTheater, ) -> Point: """Finds a valid ground position for the front line center. Checks for positions along the front line first. If none succeed, the nearest land position to the initial point is used. """ if theater.landmap is None: return initial line = LineString( [ dcs_to_shapely_point( initial.point_from_heading(heading.degrees, max_distance) ), dcs_to_shapely_point( initial.point_from_heading(heading.opposite.degrees, max_distance) ), ] ) masked_front_line = theater.landmap.inclusion_zone_only.intersection(line) if masked_front_line.is_empty: return theater.nearest_land_pos(initial) nearest_good, _ = nearest_points( masked_front_line, dcs_to_shapely_point(initial) ) return initial.new_in_same_map(nearest_good.x, nearest_good.y)