mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Clean up front line code.
The routes do not need be be recreated each time we create a `FrontLine`. The front lines follow the convoy routes, which are static. Add the convoy route data to the `ControlPoint` the way we do for shipping lanes and have `FrontLine` load the data from there.
This commit is contained in:
parent
67289bbba2
commit
e721a234e1
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, List, Optional, Set, TYPE_CHECKING
|
from typing import Iterable, List, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
from dcs.action import DoScript, DoScriptFile
|
from dcs.action import DoScript, DoScriptFile
|
||||||
@ -94,7 +94,7 @@ class Operation:
|
|||||||
)
|
)
|
||||||
return Conflict(
|
return Conflict(
|
||||||
cls.game.theater,
|
cls.game.theater,
|
||||||
FrontLine(player_cp, enemy_cp, cls.game.theater),
|
FrontLine(player_cp, enemy_cp),
|
||||||
cls.game.player_name,
|
cls.game.player_name,
|
||||||
cls.game.enemy_name,
|
cls.game.enemy_name,
|
||||||
cls.game.player_country,
|
cls.game.player_country,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from .base import *
|
from .base import *
|
||||||
from .conflicttheater import *
|
from .conflicttheater import *
|
||||||
from .controlpoint import *
|
from .controlpoint import *
|
||||||
|
from .frontline import FrontLine
|
||||||
from .missiontarget import MissionTarget
|
from .missiontarget import MissionTarget
|
||||||
from .theatergroundobject import SamGroundObject
|
from .theatergroundobject import SamGroundObject
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
from dcs.countries import (
|
from dcs.countries import (
|
||||||
@ -44,7 +43,6 @@ from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
|||||||
from pyproj import CRS, Transformer
|
from pyproj import CRS, Transformer
|
||||||
from shapely import geometry, ops
|
from shapely import geometry, ops
|
||||||
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
from .controlpoint import (
|
from .controlpoint import (
|
||||||
Airfield,
|
Airfield,
|
||||||
Carrier,
|
Carrier,
|
||||||
@ -54,12 +52,11 @@ from .controlpoint import (
|
|||||||
MissionTarget,
|
MissionTarget,
|
||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
)
|
)
|
||||||
|
from .frontline import FrontLine
|
||||||
from .landmap import Landmap, load_landmap, poly_contains
|
from .landmap import Landmap, load_landmap, poly_contains
|
||||||
from .projections import TransverseMercator
|
from .projections import TransverseMercator
|
||||||
from ..point_with_heading import PointWithHeading
|
from ..point_with_heading import PointWithHeading
|
||||||
from ..utils import Distance, meters, nautical_miles, pairwise
|
from ..utils import Distance, meters, nautical_miles
|
||||||
|
|
||||||
Numeric = Union[int, float]
|
|
||||||
|
|
||||||
SIZE_TINY = 150
|
SIZE_TINY = 150
|
||||||
SIZE_SMALL = 600
|
SIZE_SMALL = 600
|
||||||
@ -71,8 +68,6 @@ IMPORTANCE_LOW = 1
|
|||||||
IMPORTANCE_MEDIUM = 1.2
|
IMPORTANCE_MEDIUM = 1.2
|
||||||
IMPORTANCE_HIGH = 1.4
|
IMPORTANCE_HIGH = 1.4
|
||||||
|
|
||||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
|
||||||
|
|
||||||
|
|
||||||
class MizCampaignLoader:
|
class MizCampaignLoader:
|
||||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||||
@ -313,14 +308,11 @@ class MizCampaignLoader:
|
|||||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
|
|
||||||
@cached_property
|
def add_supply_routes(self) -> None:
|
||||||
def front_lines(self) -> Dict[str, ComplexFrontLine]:
|
|
||||||
# Dict of front line ID to a front line.
|
|
||||||
front_lines = {}
|
|
||||||
for group in self.front_line_path_groups:
|
for group in self.front_line_path_groups:
|
||||||
# The unit will have its first waypoint at the source CP and the
|
# The unit will have its first waypoint at the source CP and the final
|
||||||
# final waypoint at the destination CP. Intermediate waypoints
|
# waypoint at the destination CP. Each waypoint defines the path of the
|
||||||
# define the curve of the front line.
|
# cargo ship.
|
||||||
waypoints = [p.position for p in group.points]
|
waypoints = [p.position for p in group.points]
|
||||||
origin = self.theater.closest_control_point(waypoints[0])
|
origin = self.theater.closest_control_point(waypoints[0])
|
||||||
if origin is None:
|
if origin is None:
|
||||||
@ -333,21 +325,17 @@ class MizCampaignLoader:
|
|||||||
f"No control point near the final waypoint of {group.name}"
|
f"No control point near the final waypoint of {group.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
convoy_origin = waypoints[0]
|
# TODO: Snapping? Probably should be in the UI instead?
|
||||||
convoy_destination = waypoints[-1]
|
# convoy_origin = waypoints[0]
|
||||||
|
# convoy_destination = waypoints[-1]
|
||||||
|
#
|
||||||
|
# waypoints[0] = origin.position
|
||||||
|
# waypoints[-1] = destination.position
|
||||||
|
|
||||||
# Snap the begin and end points to the control points.
|
self.control_points[origin.id].create_convoy_route(destination, waypoints)
|
||||||
waypoints[0] = origin.position
|
self.control_points[destination.id].create_convoy_route(
|
||||||
waypoints[-1] = destination.position
|
origin, list(reversed(waypoints))
|
||||||
front_line_id = f"{origin.id}|{destination.id}"
|
|
||||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
|
||||||
self.control_points[origin.id].connect(
|
|
||||||
self.control_points[destination.id], convoy_origin
|
|
||||||
)
|
)
|
||||||
self.control_points[destination.id].connect(
|
|
||||||
self.control_points[origin.id], convoy_destination
|
|
||||||
)
|
|
||||||
return front_lines
|
|
||||||
|
|
||||||
def add_shipping_lanes(self) -> None:
|
def add_shipping_lanes(self) -> None:
|
||||||
for group in self.shipping_lane_groups:
|
for group in self.shipping_lane_groups:
|
||||||
@ -466,8 +454,8 @@ class MizCampaignLoader:
|
|||||||
for control_point in self.control_points.values():
|
for control_point in self.control_points.values():
|
||||||
self.theater.add_controlpoint(control_point)
|
self.theater.add_controlpoint(control_point)
|
||||||
self.add_preset_locations()
|
self.add_preset_locations()
|
||||||
|
self.add_supply_routes()
|
||||||
self.add_shipping_lanes()
|
self.add_shipping_lanes()
|
||||||
self.theater.set_frontline_data(self.front_lines)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -492,35 +480,15 @@ class ConflictTheater:
|
|||||||
land_poly = None # type: Polygon
|
land_poly = None # type: Polygon
|
||||||
"""
|
"""
|
||||||
daytime_map: Dict[str, Tuple[int, int]]
|
daytime_map: Dict[str, Tuple[int, int]]
|
||||||
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.controlpoints: List[ControlPoint] = []
|
self.controlpoints: List[ControlPoint] = []
|
||||||
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
|
||||||
"""
|
"""
|
||||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||||
for x in self.landmap[1]:
|
for x in self.landmap[1]:
|
||||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
|
|
||||||
if self._frontline_data is None:
|
|
||||||
self.load_frontline_data_from_file()
|
|
||||||
return self._frontline_data
|
|
||||||
|
|
||||||
def load_frontline_data_from_file(self) -> None:
|
|
||||||
if self._frontline_data is not None:
|
|
||||||
logging.warning("Replacing existing frontline data from file")
|
|
||||||
self._frontline_data = FrontLine.load_json_frontlines(self)
|
|
||||||
if self._frontline_data is None:
|
|
||||||
self._frontline_data = {}
|
|
||||||
|
|
||||||
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
|
|
||||||
if self._frontline_data is not None:
|
|
||||||
logging.warning("Replacing existing frontline data")
|
|
||||||
self._frontline_data = data
|
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint):
|
def add_controlpoint(self, point: ControlPoint):
|
||||||
self.controlpoints.append(point)
|
self.controlpoints.append(point)
|
||||||
|
|
||||||
@ -607,7 +575,7 @@ class ConflictTheater:
|
|||||||
for enemy_cp in [
|
for enemy_cp in [
|
||||||
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp)
|
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp)
|
||||||
]:
|
]:
|
||||||
yield FrontLine(player_cp, enemy_cp, self)
|
yield FrontLine(player_cp, enemy_cp)
|
||||||
|
|
||||||
def enemy_points(self) -> List[ControlPoint]:
|
def enemy_points(self) -> List[ControlPoint]:
|
||||||
return list(self.control_points_for(player=False))
|
return list(self.control_points_for(player=False))
|
||||||
@ -872,219 +840,3 @@ class SyriaTheater(ConflictTheater):
|
|||||||
from .syria import PARAMETERS
|
from .syria import PARAMETERS
|
||||||
|
|
||||||
return PARAMETERS
|
return PARAMETERS
|
||||||
|
|
||||||
|
|
||||||
@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]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FrontLineSegment:
|
|
||||||
"""
|
|
||||||
Describes a line segment of a FrontLine
|
|
||||||
"""
|
|
||||||
|
|
||||||
point_a: Point
|
|
||||||
point_b: Point
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_heading(self) -> Numeric:
|
|
||||||
"""The heading of the frontline segment from player to enemy control point"""
|
|
||||||
return self.point_a.heading_between_point(self.point_b)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_distance(self) -> Numeric:
|
|
||||||
"""Length of the segment"""
|
|
||||||
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,
|
|
||||||
blue_point: ControlPoint,
|
|
||||||
red_point: ControlPoint,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
) -> None:
|
|
||||||
self.blue_cp = blue_point
|
|
||||||
self.red_cp = red_point
|
|
||||||
self.segments: List[FrontLineSegment] = []
|
|
||||||
self.theater = theater
|
|
||||||
self._build_segments()
|
|
||||||
self.name = f"Front line {blue_point}/{red_point}"
|
|
||||||
|
|
||||||
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
|
||||||
if player:
|
|
||||||
return self.red_cp
|
|
||||||
return self.blue_cp
|
|
||||||
|
|
||||||
def is_friendly(self, to_player: bool) -> bool:
|
|
||||||
"""Returns True if the objective is in friendly territory."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
yield from [
|
|
||||||
FlightType.CAS,
|
|
||||||
FlightType.AEWC,
|
|
||||||
# TODO: FlightType.TROOP_TRANSPORT
|
|
||||||
# TODO: FlightType.EVAC
|
|
||||||
]
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
|
||||||
"""
|
|
||||||
The position where the conflict should occur
|
|
||||||
according to the current strength of each control point.
|
|
||||||
"""
|
|
||||||
return self.point_from_a(self._position_distance)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def points(self) -> Iterator[Point]:
|
|
||||||
yield self.segments[0].point_a
|
|
||||||
for segment in self.segments:
|
|
||||||
yield segment.point_b
|
|
||||||
|
|
||||||
@property
|
|
||||||
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
|
||||||
"""Returns a tuple of the two control points."""
|
|
||||||
return self.blue_cp, self.red_cp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_distance(self):
|
|
||||||
"""The total distance of all segments"""
|
|
||||||
return sum(i.attack_distance for i in self.segments)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def attack_heading(self):
|
|
||||||
"""The heading of the active attack segment from player to enemy control point"""
|
|
||||||
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]
|
|
||||||
|
|
||||||
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.blue_cp.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 segment.point_a.point_from_heading(
|
|
||||||
segment.attack_heading, remaining_dist
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
remaining_dist -= segment.attack_distance
|
|
||||||
|
|
||||||
@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.blue_cp.base.strength + self.red_cp.base.strength
|
|
||||||
if self.blue_cp.base.strength == 0:
|
|
||||||
return self._adjust_for_min_dist(0)
|
|
||||||
if self.red_cp.base.strength == 0:
|
|
||||||
return self._adjust_for_min_dist(self.attack_distance)
|
|
||||||
strength_pct = self.blue_cp.base.strength / total_strength
|
|
||||||
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
|
||||||
|
|
||||||
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 _build_segments(self) -> None:
|
|
||||||
"""Create line segments for the frontline"""
|
|
||||||
control_point_ids = "|".join(
|
|
||||||
[str(self.blue_cp.id), str(self.red_cp.id)]
|
|
||||||
) # from_cp.id|to_cp.id
|
|
||||||
reversed_cp_ids = "|".join([str(self.red_cp.id), str(self.blue_cp.id)])
|
|
||||||
complex_frontlines = self.theater.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.blue_cp.position, self.red_cp.position)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_json_frontlines(
|
|
||||||
theater: ConflictTheater,
|
|
||||||
) -> Optional[Dict[str, ComplexFrontLine]]:
|
|
||||||
"""Load complex frontlines from json"""
|
|
||||||
try:
|
|
||||||
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
|
|
||||||
with open(path, "r") as file:
|
|
||||||
logging.debug(f"Loading frontline from {path}...")
|
|
||||||
data = json.load(file)
|
|
||||||
return {
|
|
||||||
frontline: ComplexFrontLine(
|
|
||||||
data[frontline]["start_cp"],
|
|
||||||
[Point(i[0], i[1]) for i in data[frontline]["points"]],
|
|
||||||
)
|
|
||||||
for frontline in data
|
|
||||||
}
|
|
||||||
except OSError:
|
|
||||||
logging.warning(
|
|
||||||
f"Unable to load preset frontlines for {theater.terrain.name}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|||||||
@ -273,8 +273,8 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.has_frontline = has_frontline
|
self.has_frontline = has_frontline
|
||||||
self.connected_points: List[ControlPoint] = []
|
self.connected_points: List[ControlPoint] = []
|
||||||
|
self.convoy_routes: Dict[ControlPoint, List[Point]] = {}
|
||||||
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
|
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
|
||||||
self.convoy_spawns: Dict[ControlPoint, Point] = {}
|
|
||||||
self.base: Base = Base()
|
self.base: Base = Base()
|
||||||
self.cptype = cptype
|
self.cptype = cptype
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
@ -416,11 +416,21 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def connect(self, to: ControlPoint, convoy_location: Point) -> None:
|
def connect(self, to: ControlPoint) -> None:
|
||||||
self.connected_points.append(to)
|
self.connected_points.append(to)
|
||||||
self.convoy_spawns[to] = convoy_location
|
|
||||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||||
|
|
||||||
|
def convoy_origin_for(self, destination: ControlPoint) -> Point:
|
||||||
|
return self.convoy_route_to(destination)[0]
|
||||||
|
|
||||||
|
def convoy_route_to(self, destination: ControlPoint) -> List[Point]:
|
||||||
|
return self.convoy_routes[destination]
|
||||||
|
|
||||||
|
def create_convoy_route(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
||||||
|
self.connected_points.append(to)
|
||||||
|
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||||
|
self.convoy_routes[to] = waypoints
|
||||||
|
|
||||||
def create_shipping_lane(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
def create_shipping_lane(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
||||||
self.shipping_lanes[to] = waypoints
|
self.shipping_lanes[to] = waypoints
|
||||||
|
|
||||||
|
|||||||
179
game/theater/frontline.py
Normal file
179
game/theater/frontline.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterator, List, Tuple
|
||||||
|
|
||||||
|
from dcs.mapping import Point
|
||||||
|
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
from .controlpoint import (
|
||||||
|
ControlPoint,
|
||||||
|
MissionTarget,
|
||||||
|
)
|
||||||
|
from ..utils import pairwise
|
||||||
|
|
||||||
|
|
||||||
|
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FrontLineSegment:
|
||||||
|
"""
|
||||||
|
Describes a line segment of a FrontLine
|
||||||
|
"""
|
||||||
|
|
||||||
|
point_a: Point
|
||||||
|
point_b: Point
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_heading(self) -> float:
|
||||||
|
"""The heading of the frontline segment from player to enemy control point"""
|
||||||
|
return self.point_a.heading_between_point(self.point_b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_distance(self) -> float:
|
||||||
|
"""Length of the segment"""
|
||||||
|
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,
|
||||||
|
blue_point: ControlPoint,
|
||||||
|
red_point: ControlPoint,
|
||||||
|
) -> None:
|
||||||
|
self.blue_cp = blue_point
|
||||||
|
self.red_cp = red_point
|
||||||
|
try:
|
||||||
|
route = blue_point.convoy_route_to(red_point)
|
||||||
|
except KeyError:
|
||||||
|
# Some campaigns are air only and the mission generator currently relies on
|
||||||
|
# *some* "front line" being drawn between these two. In this case there will
|
||||||
|
# be no supply route to follow. Just create an arbitrary route between the
|
||||||
|
# two points.
|
||||||
|
route = [blue_point.position, red_point.position]
|
||||||
|
# Snap the beginning and end points to the CPs rather than the convoy waypoints,
|
||||||
|
# which are on roads.
|
||||||
|
route[0] = blue_point.position
|
||||||
|
route[-1] = red_point.position
|
||||||
|
self.segments: List[FrontLineSegment] = [
|
||||||
|
FrontLineSegment(a, b) for a, b in pairwise(route)
|
||||||
|
]
|
||||||
|
self.name = f"Front line {blue_point}/{red_point}"
|
||||||
|
|
||||||
|
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||||
|
if player:
|
||||||
|
return self.red_cp
|
||||||
|
return self.blue_cp
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
"""Returns True if the objective is in friendly territory."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
yield from [
|
||||||
|
FlightType.CAS,
|
||||||
|
FlightType.AEWC,
|
||||||
|
# TODO: FlightType.TROOP_TRANSPORT
|
||||||
|
# TODO: FlightType.EVAC
|
||||||
|
]
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self):
|
||||||
|
"""
|
||||||
|
The position where the conflict should occur
|
||||||
|
according to the current strength of each control point.
|
||||||
|
"""
|
||||||
|
return self.point_from_a(self._position_distance)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def points(self) -> Iterator[Point]:
|
||||||
|
yield self.segments[0].point_a
|
||||||
|
for segment in self.segments:
|
||||||
|
yield segment.point_b
|
||||||
|
|
||||||
|
@property
|
||||||
|
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||||
|
"""Returns a tuple of the two control points."""
|
||||||
|
return self.blue_cp, self.red_cp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_distance(self):
|
||||||
|
"""The total distance of all segments"""
|
||||||
|
return sum(i.attack_distance for i in self.segments)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack_heading(self):
|
||||||
|
"""The heading of the active attack segment from player to enemy control point"""
|
||||||
|
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]
|
||||||
|
|
||||||
|
def point_from_a(self, distance: float) -> Point:
|
||||||
|
"""
|
||||||
|
Returns a point {distance} away from control_point_a along the frontline segments.
|
||||||
|
"""
|
||||||
|
if distance < self.segments[0].attack_distance:
|
||||||
|
return self.blue_cp.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 segment.point_a.point_from_heading(
|
||||||
|
segment.attack_heading, remaining_dist
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remaining_dist -= segment.attack_distance
|
||||||
|
|
||||||
|
@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.blue_cp.base.strength + self.red_cp.base.strength
|
||||||
|
if self.blue_cp.base.strength == 0:
|
||||||
|
return self._adjust_for_min_dist(0)
|
||||||
|
if self.red_cp.base.strength == 0:
|
||||||
|
return self._adjust_for_min_dist(self.attack_distance)
|
||||||
|
strength_pct = self.blue_cp.base.strength / total_strength
|
||||||
|
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
|
||||||
|
|
||||||
|
def _adjust_for_min_dist(self, distance: float) -> float:
|
||||||
|
"""
|
||||||
|
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
|
||||||
@ -338,11 +338,11 @@ class Convoy(MultiGroupTransport):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def route_start(self) -> Point:
|
def route_start(self) -> Point:
|
||||||
return self.origin.convoy_spawns[self.destination]
|
return self.origin.convoy_origin_for(self.destination)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def route_end(self) -> Point:
|
def route_end(self) -> Point:
|
||||||
return self.destination.convoy_spawns[self.origin]
|
return self.destination.convoy_origin_for(self.origin)
|
||||||
|
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
return f"In a convoy from {self.origin} to {self.destination}"
|
return f"In a convoy from {self.origin} to {self.destination}"
|
||||||
|
|||||||
@ -915,9 +915,9 @@ class QLiberationMap(QGraphicsView):
|
|||||||
convoys.append(convoy)
|
convoys.append(convoy)
|
||||||
|
|
||||||
if a.captured:
|
if a.captured:
|
||||||
frontline = FrontLine(a, b, self.game.theater)
|
frontline = FrontLine(a, b)
|
||||||
else:
|
else:
|
||||||
frontline = FrontLine(b, a, self.game.theater)
|
frontline = FrontLine(b, a)
|
||||||
if a.front_is_active(b):
|
if a.front_is_active(b):
|
||||||
if DisplayOptions.actual_frontline_pos:
|
if DisplayOptions.actual_frontline_pos:
|
||||||
self.draw_actual_frontline(scene, frontline, convoys)
|
self.draw_actual_frontline(scene, frontline, convoys)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user