mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
[2/3] Improve join point placement.
This commit is contained in:
parent
e03d710d53
commit
d444d716f5
@ -1 +1,2 @@
|
||||
from .ipzonegeometry import IpZoneGeometry
|
||||
from .joinzonegeometry import JoinZoneGeometry
|
||||
|
||||
84
game/flightplan/joinzonegeometry.py
Normal file
84
game/flightplan/joinzonegeometry.py
Normal file
@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import shapely.ops
|
||||
from dcs import Point
|
||||
from shapely.geometry import Point as ShapelyPoint, Polygon
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.utils import nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class JoinZoneGeometry:
|
||||
"""Defines the zones used for finding optimal join point placement.
|
||||
|
||||
The zones themselves are stored in the class rather than just the resulting join
|
||||
point so that the zones can be drawn in the map for debugging purposes.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target: Point,
|
||||
home: Point,
|
||||
ip: Point,
|
||||
coalition: Coalition,
|
||||
theater: ConflictTheater,
|
||||
) -> None:
|
||||
# Normal join placement is based on the path from home to the IP. If no path is
|
||||
# found it means that the target is on a direct path. In that case we instead
|
||||
# want to enforce that the join point is:
|
||||
#
|
||||
# * Not closer to the target than the IP.
|
||||
# * Not too close to the home airfield.
|
||||
# * Not threatened.
|
||||
# * A minimum distance from the IP.
|
||||
# * Not too sharp a turn at the ingress point.
|
||||
self.ip = ShapelyPoint(ip.x, ip.y)
|
||||
self.threat_zone = coalition.opponent.threat_zone.all
|
||||
self.home = ShapelyPoint(home.x, home.y)
|
||||
|
||||
self.ip_bubble = self.ip.buffer(coalition.doctrine.join_distance.meters)
|
||||
|
||||
ip_distance = ip.distance_to_point(target)
|
||||
self.target_bubble = ShapelyPoint(target.x, target.y).buffer(ip_distance)
|
||||
|
||||
# The minimum distance between the home location and the IP.
|
||||
min_distance_from_home = nautical_miles(5)
|
||||
|
||||
self.home_bubble = self.home.buffer(min_distance_from_home.meters)
|
||||
|
||||
self.excluded_zone = shapely.ops.unary_union(
|
||||
[self.home_bubble, self.ip_bubble, self.target_bubble, self.threat_zone]
|
||||
)
|
||||
|
||||
ip_heading = target.heading_between_point(ip)
|
||||
|
||||
# Arbitrarily large since this is later constrained by the map boundary, and
|
||||
# we'll be picking a location close to the IP anyway. Just used to avoid real
|
||||
# distance calculations to project to the map edge.
|
||||
large_distance = nautical_miles(400).meters
|
||||
turn_limit = 40
|
||||
ip_limit_ccw = ip.point_from_heading(ip_heading - turn_limit, large_distance)
|
||||
ip_limit_cw = ip.point_from_heading(ip_heading + turn_limit, large_distance)
|
||||
|
||||
ip_direction_limit_wedge = Polygon(
|
||||
[
|
||||
(ip.x, ip.y),
|
||||
(ip_limit_ccw.x, ip_limit_ccw.y),
|
||||
(ip_limit_cw.x, ip_limit_cw.y),
|
||||
]
|
||||
)
|
||||
|
||||
self.permissible_line = (
|
||||
coalition.nav_mesh.map_bounds(theater)
|
||||
.intersection(ip_direction_limit_wedge)
|
||||
.intersection(self.excluded_zone.boundary)
|
||||
)
|
||||
|
||||
def find_best_join_point(self) -> Point:
|
||||
join, _ = shapely.ops.nearest_points(self.permissible_line, self.home)
|
||||
return Point(join.x, join.y)
|
||||
@ -20,7 +20,7 @@ from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.flightplan import IpZoneGeometry
|
||||
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -949,20 +949,22 @@ class FlightPlanBuilder:
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
|
||||
package_airfield = self.package_airfield()
|
||||
|
||||
# Start by picking the best IP for the attack.
|
||||
ingress_point = IpZoneGeometry(
|
||||
self.package.target.position,
|
||||
self.package_airfield().position,
|
||||
package_airfield.position,
|
||||
self.coalition,
|
||||
).find_best_ip()
|
||||
|
||||
# Pick the join point based on the best route to the IP.
|
||||
join_point = self.preferred_join_point(ingress_point)
|
||||
if join_point is None:
|
||||
# The entire path to the target is threatened. Use the fallback behavior for
|
||||
# now.
|
||||
self.legacy_package_waypoints_impl(ingress_point)
|
||||
return
|
||||
join_point = JoinZoneGeometry(
|
||||
self.package.target.position,
|
||||
package_airfield.position,
|
||||
ingress_point,
|
||||
self.coalition,
|
||||
self.theater,
|
||||
).find_best_join_point()
|
||||
|
||||
# And the split point based on the best route from the IP. Since that's no
|
||||
# different than the best route *to* the IP, this is the same as the join point.
|
||||
|
||||
@ -12,6 +12,7 @@ from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPo
|
||||
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.flightplan import JoinZoneGeometry
|
||||
from game.navmesh import NavMesh, NavMeshPoly
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
@ -82,6 +83,12 @@ def shapely_to_leaflet_polys(
|
||||
return [shapely_poly_to_leaflet_points(poly, theater) for poly in polys]
|
||||
|
||||
|
||||
def shapely_line_to_leaflet_points(
|
||||
line: LineString, theater: ConflictTheater
|
||||
) -> list[LeafletLatLon]:
|
||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
|
||||
|
||||
|
||||
class ControlPointJs(QObject):
|
||||
nameChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
@ -822,16 +829,20 @@ class IpZonesJs(QObject):
|
||||
def permissibleZone(self) -> list[LeafletPoly]:
|
||||
return self._permissible_zone
|
||||
|
||||
@Property(list, notify=permissibleZoneChanged)
|
||||
@Property(list, notify=safeZoneChanged)
|
||||
def safeZone(self) -> list[LeafletPoly]:
|
||||
return self._safe_zone
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> IpZonesJs:
|
||||
return IpZonesJs([], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return IpZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS or target == home:
|
||||
return IpZonesJs([], [], [], [])
|
||||
geometry = IpZoneGeometry(target.position, home.position, game.blue)
|
||||
return IpZonesJs(
|
||||
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
|
||||
@ -841,6 +852,73 @@ class IpZonesJs(QObject):
|
||||
)
|
||||
|
||||
|
||||
class JoinZonesJs(QObject):
|
||||
homeBubbleChanged = Signal()
|
||||
targetBubbleChanged = Signal()
|
||||
ipBubbleChanged = Signal()
|
||||
excludedZoneChanged = Signal()
|
||||
permissibleLineChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
home_bubble: list[LeafletPoly],
|
||||
target_bubble: list[LeafletPoly],
|
||||
ip_bubble: list[LeafletPoly],
|
||||
excluded_zone: list[LeafletPoly],
|
||||
permissible_line: list[LeafletLatLon],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._home_bubble = home_bubble
|
||||
self._target_bubble = target_bubble
|
||||
self._ip_bubble = ip_bubble
|
||||
self._excluded_zone = excluded_zone
|
||||
self._permissible_line = permissible_line
|
||||
|
||||
@Property(list, notify=homeBubbleChanged)
|
||||
def homeBubble(self) -> list[LeafletPoly]:
|
||||
return self._home_bubble
|
||||
|
||||
@Property(list, notify=targetBubbleChanged)
|
||||
def targetBubble(self) -> list[LeafletPoly]:
|
||||
return self._target_bubble
|
||||
|
||||
@Property(list, notify=ipBubbleChanged)
|
||||
def ipBubble(self) -> list[LeafletPoly]:
|
||||
return self._ip_bubble
|
||||
|
||||
@Property(list, notify=excludedZoneChanged)
|
||||
def excludedZone(self) -> list[LeafletPoly]:
|
||||
return self._excluded_zone
|
||||
|
||||
@Property(list, notify=permissibleLineChanged)
|
||||
def permissibleLine(self) -> list[LeafletLatLon]:
|
||||
return self._permissible_line
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> JoinZonesJs:
|
||||
return JoinZonesJs([], [], [], [], [])
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs:
|
||||
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
|
||||
return JoinZonesJs.empty()
|
||||
target = flight.package.target
|
||||
home = flight.departure
|
||||
if flight.package.waypoints is None:
|
||||
return JoinZonesJs.empty()
|
||||
ip = flight.package.waypoints.ingress
|
||||
geometry = JoinZoneGeometry(
|
||||
target.position, home.position, ip, game.blue, game.theater
|
||||
)
|
||||
return JoinZonesJs(
|
||||
shapely_to_leaflet_polys(geometry.home_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.target_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
|
||||
shapely_to_leaflet_polys(geometry.excluded_zone, game.theater),
|
||||
shapely_line_to_leaflet_points(geometry.permissible_line, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class MapModel(QObject):
|
||||
cleared = Signal()
|
||||
|
||||
@ -855,6 +933,7 @@ class MapModel(QObject):
|
||||
mapZonesChanged = Signal()
|
||||
unculledZonesChanged = Signal()
|
||||
ipZonesChanged = Signal()
|
||||
joinZonesChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@ -871,7 +950,8 @@ class MapModel(QObject):
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs([], [], [], [])
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
@ -895,7 +975,7 @@ class MapModel(QObject):
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._ip_zones = IpZonesJs([], [], [], [])
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self.cleared.emit()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
@ -984,11 +1064,14 @@ class MapModel(QObject):
|
||||
) + self._flights_in_ato(self.game.red.ato, blue=False)
|
||||
self.flightsChanged.emit()
|
||||
selected_flight = self._get_selected_flight()
|
||||
if selected_flight is not None:
|
||||
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
||||
if selected_flight is None:
|
||||
self._ip_zones = IpZonesJs.empty()
|
||||
self._join_zones = JoinZonesJs.empty()
|
||||
else:
|
||||
self._ip_zones = IpZonesJs([], [], [], [])
|
||||
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
|
||||
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
|
||||
self.ipZonesChanged.emit()
|
||||
self.joinZonesChanged.emit()
|
||||
|
||||
@Property(list, notify=flightsChanged)
|
||||
def flights(self) -> List[FlightJs]:
|
||||
@ -1121,6 +1204,10 @@ class MapModel(QObject):
|
||||
def ipZones(self) -> IpZonesJs:
|
||||
return self._ip_zones
|
||||
|
||||
@Property(JoinZonesJs, notify=joinZonesChanged)
|
||||
def joinZones(self) -> JoinZonesJs:
|
||||
return self._join_zones
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
@ -198,10 +198,8 @@ const exclusionZones = L.layerGroup();
|
||||
const seaZones = L.layerGroup();
|
||||
const unculledZones = L.layerGroup();
|
||||
|
||||
const homeBubble = L.layerGroup();
|
||||
const ipBubble = L.layerGroup();
|
||||
const permissibleZone = L.layerGroup();
|
||||
const safeZone = L.layerGroup();
|
||||
const ipZones = L.layerGroup();
|
||||
const joinZones = L.layerGroup().addTo(map);
|
||||
|
||||
const debugControlGroups = {
|
||||
"Blue Threat Zones": {
|
||||
@ -232,11 +230,9 @@ const debugControlGroups = {
|
||||
};
|
||||
|
||||
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
|
||||
debugControlGroups["IP Zones"] = {
|
||||
"Home bubble": homeBubble,
|
||||
"IP bubble": ipBubble,
|
||||
"Permissible zone": permissibleZone,
|
||||
"Safe zone": safeZone,
|
||||
debugControlGroups["Waypoint Zones"] = {
|
||||
"IP Zones": ipZones,
|
||||
"Join Zones": joinZones,
|
||||
};
|
||||
}
|
||||
|
||||
@ -287,7 +283,12 @@ L.control
|
||||
L.control
|
||||
.groupedLayers(null, debugControlGroups, {
|
||||
position: "topleft",
|
||||
exclusiveGroups: ["Blue Threat Zones", "Red Threat Zones", "Navmeshes"],
|
||||
exclusiveGroups: [
|
||||
"Blue Threat Zones",
|
||||
"Red Threat Zones",
|
||||
"Navmeshes",
|
||||
"Waypoint Zones",
|
||||
],
|
||||
groupCheckboxes: true,
|
||||
})
|
||||
.addTo(map);
|
||||
@ -308,6 +309,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.mapZonesChanged.connect(drawMapZones);
|
||||
game.unculledZonesChanged.connect(drawUnculledZones);
|
||||
game.ipZonesChanged.connect(drawIpZones);
|
||||
game.joinZonesChanged.connect(drawJoinZones);
|
||||
});
|
||||
|
||||
function recenterMap(center) {
|
||||
@ -992,34 +994,65 @@ function drawUnculledZones() {
|
||||
}
|
||||
|
||||
function drawIpZones() {
|
||||
homeBubble.clearLayers();
|
||||
ipBubble.clearLayers();
|
||||
permissibleZone.clearLayers();
|
||||
safeZone.clearLayers();
|
||||
ipZones.clearLayers();
|
||||
|
||||
L.polygon(game.ipZones.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(homeBubble);
|
||||
}).addTo(ipZones);
|
||||
|
||||
L.polygon(game.ipZones.ipBubble, {
|
||||
color: "#bb89ff",
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(ipBubble);
|
||||
}).addTo(ipZones);
|
||||
|
||||
L.polygon(game.ipZones.permissibleZone, {
|
||||
color: "#ffffff",
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(permissibleZone);
|
||||
}).addTo(ipZones);
|
||||
|
||||
L.polygon(game.ipZones.safeZone, {
|
||||
color: Colors.Green,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(safeZone);
|
||||
}).addTo(ipZones);
|
||||
}
|
||||
|
||||
function drawJoinZones() {
|
||||
joinZones.clearLayers();
|
||||
|
||||
L.polygon(game.joinZones.homeBubble, {
|
||||
color: Colors.Highlight,
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
|
||||
L.polygon(game.joinZones.targetBubble, {
|
||||
color: "#bb89ff",
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
|
||||
L.polygon(game.joinZones.ipBubble, {
|
||||
color: "#ffffff",
|
||||
fillOpacity: 0.1,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
|
||||
L.polygon(game.joinZones.excludedZone, {
|
||||
color: "#ffa500",
|
||||
fillOpacity: 0.2,
|
||||
stroke: false,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
|
||||
L.polyline(game.joinZones.permissibleLine, {
|
||||
color: Colors.Green,
|
||||
interactive: false,
|
||||
}).addTo(joinZones);
|
||||
}
|
||||
|
||||
function drawInitialMap() {
|
||||
@ -1034,6 +1067,7 @@ function drawInitialMap() {
|
||||
drawMapZones();
|
||||
drawUnculledZones();
|
||||
drawIpZones();
|
||||
drawJoinZones();
|
||||
}
|
||||
|
||||
function clearAllLayers() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user