diff --git a/changelog.md b/changelog.md
index 5051dbfd..f89e9bfe 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,7 @@ Saves from 8.x are not compatible with 9.0.0.
## Features/Improvements
+* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
## Fixes
diff --git a/client/src/api/_liberationApi.ts b/client/src/api/_liberationApi.ts
index a1c35b43..abcf3d50 100644
--- a/client/src/api/_liberationApi.ts
+++ b/client/src/api/_liberationApi.ts
@@ -384,6 +384,8 @@ export type IpZones = {
ipBubble: LatLng[][];
permissibleZone: LatLng[][];
safeZones: LatLng[][][];
+ preferredThreatenedZones: LatLng[][][];
+ tolerableThreatenedLines: LatLng[][];
};
export type JoinZones = {
homeBubble: LatLng[][];
diff --git a/client/src/components/waypointdebugzones/IpZones.tsx b/client/src/components/waypointdebugzones/IpZones.tsx
index 39beef27..fe93e1b5 100644
--- a/client/src/components/waypointdebugzones/IpZones.tsx
+++ b/client/src/components/waypointdebugzones/IpZones.tsx
@@ -1,5 +1,5 @@
import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
-import { LayerGroup, Polygon } from "react-leaflet";
+import { LayerGroup, Polygon, Polyline } from "react-leaflet";
interface IpZonesProps {
flightId: string;
@@ -56,6 +56,29 @@ function IpZones(props: IpZonesProps) {
/>
);
})}
+
+ {data.preferredThreatenedZones.map((zone, idx) => {
+ return (
+
+ );
+ })}
+
+ {data.tolerableThreatenedLines.map((line, idx) => {
+ return (
+
+ );
+ })}
>
);
}
diff --git a/game/flightplan/ipzonegeometry.py b/game/flightplan/ipzonegeometry.py
index 70b5a072..3d8ea923 100644
--- a/game/flightplan/ipzonegeometry.py
+++ b/game/flightplan/ipzonegeometry.py
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
-from shapely.geometry import MultiPolygon, Point as ShapelyPoint
+from shapely.geometry import MultiPolygon, Point as ShapelyPoint, MultiLineString
from game.utils import meters, nautical_miles
@@ -90,6 +90,32 @@ class IpZoneGeometry:
safe_zones = MultiPolygon([safe_zones])
self.safe_zones = safe_zones
+ # See explanation where this is used in _unsafe_ip.
+ # https://github.com/dcs-liberation/dcs_liberation/issues/2754
+ preferred_threatened_zone_wiggle_room = nautical_miles(5)
+ threat_buffer_distance = self.permissible_zone.distance(
+ self.threat_zone.boundary
+ )
+ preferred_threatened_zone_mask = self.threat_zone.buffer(
+ -threat_buffer_distance - preferred_threatened_zone_wiggle_room.meters
+ )
+ preferred_threatened_zones = self.threat_zone.difference(
+ preferred_threatened_zone_mask
+ )
+
+ if not isinstance(preferred_threatened_zones, MultiPolygon):
+ preferred_threatened_zones = MultiPolygon([preferred_threatened_zones])
+ self.preferred_threatened_zones = preferred_threatened_zones
+
+ tolerable_threatened_lines = self.preferred_threatened_zones.intersection(
+ self.permissible_zone.boundary
+ )
+ if tolerable_threatened_lines.is_empty:
+ tolerable_threatened_lines = MultiLineString([])
+ elif not isinstance(tolerable_threatened_lines, MultiLineString):
+ tolerable_threatened_lines = MultiLineString([tolerable_threatened_lines])
+ self.tolerable_threatened_lines = tolerable_threatened_lines
+
def _unsafe_ip(self) -> ShapelyPoint:
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
if unthreatened_home_zone.is_empty:
@@ -100,8 +126,28 @@ class IpZoneGeometry:
self.permissible_zone, self.threat_zone.boundary
)[0]
- # No safe point in the IP zone, but the home zone is safe. Pick the max-
- # distance IP that's closest to the untreatened home zone.
+ # No safe point in the IP zone, but the home zone is safe. Pick an IP within
+ # both the permissible zone and preferred threatened zone that's as close to the
+ # unthreatened home zone as possible. This should get us a max-range IP that
+ # is roughly as safe as possible without unjustifiably long routes.
+ #
+ # If we do the obvious thing and pick the IP that minimizes threatened travel
+ # time (the IP closest to the threat boundary) and the objective is near the
+ # center of the threat zone (common when there is an airbase covered only by air
+ # defenses with shorter range than the BARCAP zone, and the target is a TGO near
+ # the CP), the IP could be placed such that the flight would fly all the way
+ # around the threat zone just to avoid a few more threatened miles of travel. To
+ # avoid that, we generate a set of preferred threatened areas that offer a
+ # trade-off between travel time and safety.
+ #
+ # https://github.com/dcs-liberation/dcs_liberation/issues/2754
+ if not self.tolerable_threatened_lines.is_empty:
+ return shapely.ops.nearest_points(
+ self.tolerable_threatened_lines, self.home
+ )[0]
+
+ # But if no part of the permissible zone is tolerably threatened, fall back to
+ # the old safety maximizing approach.
return shapely.ops.nearest_points(
self.permissible_zone, unthreatened_home_zone
)[0]
diff --git a/game/server/debuggeometries/models.py b/game/server/debuggeometries/models.py
index 8dd271cf..93ecd166 100644
--- a/game/server/debuggeometries/models.py
+++ b/game/server/debuggeometries/models.py
@@ -64,14 +64,16 @@ class IpZonesJs(BaseModel):
ipBubble: LeafletPoly = Field(alias="ipBubble")
permissibleZone: LeafletPoly = Field(alias="permissibleZone")
safeZones: list[LeafletPoly] = Field(alias="safeZones")
+ preferred_threatened_zones: list[LeafletPoly] = Field(
+ alias="preferredThreatenedZones"
+ )
+ tolerable_threatened_lines: list[LeafletLine] = Field(
+ alias="tolerableThreatenedLines"
+ )
class Config:
title = "IpZones"
- @classmethod
- def empty(cls) -> IpZonesJs:
- return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[])
-
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
target = flight.package.target
@@ -84,6 +86,12 @@ class IpZonesJs(BaseModel):
geometry.permissible_zone, game.theater
),
safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater),
+ preferredThreatenedZones=ShapelyUtil.polys_to_leaflet(
+ geometry.preferred_threatened_zones, game.theater
+ ),
+ tolerableThreatenedLines=ShapelyUtil.lines_to_leaflet(
+ geometry.tolerable_threatened_lines, game.theater
+ ),
)