dcs-retribution/game/missiongenerator/frontlineconflictdescription.py

169 lines
5.6 KiB
Python

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 game.settings import Settings
from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint
from game.utils import Heading
@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_length * 1000,
attack_heading.right,
theater,
)
if position is None:
raise RuntimeError("Could not find front line position")
return position, attack_heading.opposite
@classmethod
def frontline_bounds(
cls, front_line: FrontLine, theater: ConflictTheater, settings: Settings
) -> FrontLineBounds:
"""
Returns a vector for a valid frontline location avoiding exclusion zones.
"""
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_length * 1000 / 2),
left_heading,
theater,
)
right_position = cls.extend_ground_position(
center_position,
int(settings.max_frontline_length * 1000 / 2),
right_heading,
theater,
)
return FrontLineBounds(left_position, right_position)
@classmethod
def frontline_cas_conflict(
cls, front_line: FrontLine, theater: ConflictTheater, settings: Settings
) -> 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, settings)
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,
) -> Optional[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.
"""
pos = initial
if theater.is_on_land(pos):
return pos
for distance in range(0, int(max_distance), 100):
pos = initial.point_from_heading(heading.degrees, distance)
if theater.is_on_land(pos):
return pos
pos = initial.point_from_heading(heading.opposite.degrees, distance)
if theater.is_on_land(pos):
return pos
pos = theater.nearest_land_pos(initial)
return pos