mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add shipping lane support to campaign files.
These don't actually do anything yet, this is just the campaign support and UI. https://github.com/Khopa/dcs_liberation/issues/826
This commit is contained in:
parent
97b73e1a01
commit
5e67ce0ab2
@ -5,12 +5,8 @@ import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from itertools import tee
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
|
||||
|
||||
from shapely import geometry
|
||||
from shapely import ops
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import (
|
||||
@ -21,9 +17,10 @@ from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import F_15C
|
||||
from dcs.ships import (
|
||||
Bulker_Handy_Wind,
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.terrain import (
|
||||
@ -43,20 +40,21 @@ from dcs.unitgroup import (
|
||||
VehicleGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
from shapely import geometry, ops
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from .controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
Lha,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
from ..utils import Distance, meters, nautical_miles, pairwise
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
@ -73,16 +71,6 @@ IMPORTANCE_HIGH = 1.4
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
"""
|
||||
a, b = tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||
RED_COUNTRY = CombinedJointTaskForcesRed()
|
||||
@ -92,6 +80,7 @@ class MizCampaignLoader:
|
||||
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||
@ -315,6 +304,12 @@ class MizCampaignLoader:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue=True).ship_group:
|
||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def front_lines(self) -> Dict[str, ComplexFrontLine]:
|
||||
# Dict of front line ID to a front line.
|
||||
@ -351,6 +346,28 @@ class MizCampaignLoader:
|
||||
)
|
||||
return front_lines
|
||||
|
||||
def add_shipping_lanes(self) -> None:
|
||||
for group in self.shipping_lane_groups:
|
||||
# The unit will have its first waypoint at the source CP and the final
|
||||
# waypoint at the destination CP. Each waypoint defines the path of the
|
||||
# cargo ship.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}"
|
||||
)
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}"
|
||||
)
|
||||
|
||||
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
|
||||
self.control_points[destination.id].create_shipping_lane(
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
@ -446,6 +463,7 @@ class MizCampaignLoader:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.add_shipping_lanes()
|
||||
self.theater.set_frontline_data(self.front_lines)
|
||||
|
||||
|
||||
@ -877,6 +895,12 @@ class FrontLine(MissionTarget):
|
||||
"""
|
||||
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."""
|
||||
|
||||
@ -267,6 +267,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
self.connected_points: List[ControlPoint] = []
|
||||
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
|
||||
self.convoy_spawns: Dict[ControlPoint, Point] = {}
|
||||
self.base: Base = Base()
|
||||
self.cptype = cptype
|
||||
@ -397,6 +398,9 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.convoy_spawns[to] = convoy_location
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
|
||||
def create_shipping_lane(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
||||
self.shipping_lanes[to] = waypoints
|
||||
|
||||
@abstractmethod
|
||||
def runway_is_operational(self) -> bool:
|
||||
"""
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
@ -178,3 +179,13 @@ def mach(value: float, altitude: Distance) -> Speed:
|
||||
|
||||
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
"""
|
||||
a, b = itertools.tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
@ -39,4 +39,9 @@ VERSION = _build_version_string()
|
||||
#: * Factories (Workshop_A) define factory objectives. Only control points with
|
||||
#: factories will be able to recruit ground units, so they should exist in sufficient
|
||||
#: number and be protected by IADS.
|
||||
CAMPAIGN_FORMAT_VERSION = 2
|
||||
#:
|
||||
#: Version 3
|
||||
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
|
||||
#: are navigable by ships and have a route to another port area. DCS ships *will not*
|
||||
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
|
||||
CAMPAIGN_FORMAT_VERSION = 3
|
||||
|
||||
@ -45,7 +45,7 @@ from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.transfers import Convoy
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.utils import Distance, meters, nautical_miles, pairwise
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict, Package
|
||||
from gen.flights.flight import (
|
||||
@ -67,12 +67,16 @@ from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
||||
from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.widgets.map.ShippingLaneSegment import ShippingLaneSegment
|
||||
from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
|
||||
|
||||
MapPoint = Tuple[float, float]
|
||||
|
||||
|
||||
def binomial(i: int, n: int) -> float:
|
||||
"""Binomial coefficient"""
|
||||
return math.factorial(n) / float(math.factorial(i) * math.factorial(n - i))
|
||||
@ -821,47 +825,64 @@ class QLiberationMap(QGraphicsView):
|
||||
)
|
||||
)
|
||||
|
||||
def bezier_points(
|
||||
self, points: Iterable[Point]
|
||||
) -> Iterator[Tuple[MapPoint, MapPoint]]:
|
||||
# Thanks to Alquimista for sharing a python implementation of the bezier
|
||||
# algorithm this is adapted from.
|
||||
# https://gist.github.com/Alquimista/1274149#file-bezdraw-py
|
||||
bezier_fixed_points = []
|
||||
for a, b in pairwise(points):
|
||||
bezier_fixed_points.append(self._transform_point(a))
|
||||
bezier_fixed_points.append(self._transform_point(b))
|
||||
|
||||
old_point = bezier_fixed_points[0]
|
||||
for point in bezier_curve_range(
|
||||
int(len(bezier_fixed_points) * 2), bezier_fixed_points
|
||||
):
|
||||
yield old_point, point
|
||||
old_point = point
|
||||
|
||||
def draw_bezier_frontline(
|
||||
self,
|
||||
scene: QGraphicsScene,
|
||||
frontline: FrontLine,
|
||||
convoys: List[Convoy],
|
||||
) -> None:
|
||||
"""
|
||||
Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from.
|
||||
https://gist.github.com/Alquimista/1274149#file-bezdraw-py
|
||||
"""
|
||||
bezier_fixed_points = []
|
||||
for segment in frontline.segments:
|
||||
bezier_fixed_points.append(self._transform_point(segment.point_a))
|
||||
bezier_fixed_points.append(self._transform_point(segment.point_b))
|
||||
|
||||
old_point = bezier_fixed_points[0]
|
||||
for point in bezier_curve_range(
|
||||
int(len(bezier_fixed_points) * 2), bezier_fixed_points
|
||||
):
|
||||
for a, b in self.bezier_points(frontline.points):
|
||||
scene.addItem(
|
||||
SupplyRouteSegment(
|
||||
old_point[0],
|
||||
old_point[1],
|
||||
point[0],
|
||||
point[1],
|
||||
a[0],
|
||||
a[1],
|
||||
b[0],
|
||||
b[1],
|
||||
frontline.control_point_a,
|
||||
frontline.control_point_b,
|
||||
convoys,
|
||||
)
|
||||
)
|
||||
old_point = point
|
||||
|
||||
def draw_supply_routes(self) -> None:
|
||||
if not DisplayOptions.lines:
|
||||
return
|
||||
|
||||
seen = set()
|
||||
for cp in self.game.theater.controlpoints:
|
||||
seen.add(cp)
|
||||
for connected in cp.connected_points:
|
||||
if connected in seen:
|
||||
continue
|
||||
if DisplayOptions.lines:
|
||||
self.draw_supply_route_between(cp, connected)
|
||||
self.draw_supply_route_between(cp, connected)
|
||||
for destination, shipping_lane in cp.shipping_lanes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
if cp.is_friendly(destination.captured):
|
||||
self.draw_shipping_lane_between(cp, destination)
|
||||
|
||||
def draw_shipping_lane_between(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
scene = self.scene()
|
||||
for pa, pb in self.bezier_points(a.shipping_lanes[b]):
|
||||
scene.addItem(ShippingLaneSegment(pa[0], pa[1], pb[0], pb[1], a, b))
|
||||
|
||||
def draw_supply_route_between(self, a: ControlPoint, b: ControlPoint) -> None:
|
||||
scene = self.scene()
|
||||
|
||||
68
qt_ui/widgets/map/ShippingLaneSegment.py
Normal file
68
qt_ui/widgets/map/ShippingLaneSegment.py
Normal file
@ -0,0 +1,68 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QColor, QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QGraphicsItem,
|
||||
QGraphicsLineItem,
|
||||
)
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from qt_ui.uiconstants import COLORS
|
||||
|
||||
|
||||
class ShippingLaneSegment(QGraphicsLineItem):
|
||||
def __init__(
|
||||
self,
|
||||
x0: float,
|
||||
y0: float,
|
||||
x1: float,
|
||||
y1: float,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
parent: Optional[QGraphicsItem] = None,
|
||||
) -> None:
|
||||
super().__init__(x0, y0, x1, y1, parent)
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
self.ships = []
|
||||
self.setPen(self.make_pen())
|
||||
self.setToolTip(self.make_tooltip())
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
@property
|
||||
def has_ships(self) -> bool:
|
||||
return bool(self.ships)
|
||||
|
||||
def make_tooltip(self) -> str:
|
||||
if not self.has_ships:
|
||||
return "No ships present in this shipping lane."
|
||||
|
||||
ships = []
|
||||
for ship in self.ships:
|
||||
units = "units" if ship.size > 1 else "unit"
|
||||
ships.append(
|
||||
f"{ship.size} {units} transferring from {ship.origin} to "
|
||||
f"{ship.destination}."
|
||||
)
|
||||
return "\n".join(ships)
|
||||
|
||||
@property
|
||||
def line_color(self) -> QColor:
|
||||
if self.control_point_a.captured:
|
||||
return COLORS["dark_blue"]
|
||||
else:
|
||||
return COLORS["dark_red"]
|
||||
|
||||
@property
|
||||
def line_style(self) -> Qt.PenStyle:
|
||||
if self.has_ships:
|
||||
return Qt.PenStyle.SolidLine
|
||||
return Qt.PenStyle.DotLine
|
||||
|
||||
def make_pen(self) -> QPen:
|
||||
pen = QPen(brush=self.line_color)
|
||||
pen.setColor(self.line_color)
|
||||
pen.setStyle(self.line_style)
|
||||
pen.setWidth(2)
|
||||
return pen
|
||||
Loading…
x
Reference in New Issue
Block a user