mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
* Added a separate Doctrine page in settings with the following new options: - Minimum number of aircraft for autoplanner to plan OCA packages against - Airbase threat range (nmi) - TARCAP threat buffer distance (nmi) - AEW&C threat buffer distance (nmi) - Theater tanker threat buffer distance (nmi) Implemented handling for the OPFOR autoplanner aggressiveness in objectivefinder.py vulnerable_control_points(). * * Added three new options in Settings: - Autoplanner plans refueling flights for Strike packages - Autoplanner plans refueling flights for OCA packages - Autoplanner plans refueling flights for DEAD packages Fixed a bug in faction.py where F-16Ds were not correctly removed from the faction when the F-16I/F-16D mod was not selected. * Renamed Maximum frontline length -> Maximum frontline width.
175 lines
5.8 KiB
Python
175 lines
5.8 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 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, 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_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, 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,
|
|
) -> 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)
|