diff --git a/game/data/doctrine.py b/game/data/doctrine.py
index 4f944833..21402501 100644
--- a/game/data/doctrine.py
+++ b/game/data/doctrine.py
@@ -47,8 +47,13 @@ class Doctrine:
#: fallback flight plan layout (when the departure airfield is near a threat zone).
join_distance: Distance
- #: The distance between the ingress point (beginning of the attack) and target.
- ingress_distance: Distance
+ #: The maximum distance between the ingress point (beginning of the attack) and
+ #: target.
+ max_ingress_distance: Distance
+
+ #: The minimum distance between the ingress point (beginning of the attack) and
+ #: target.
+ min_ingress_distance: Distance
ingress_altitude: Distance
@@ -87,9 +92,22 @@ class Doctrine:
@has_save_compat_for(5)
def __setstate__(self, state: dict[str, Any]) -> None:
- if "ingress_distance" not in state:
- state["ingress_distance"] = state["ingress_egress_distance"]
- del state["ingress_egress_distance"]
+ if "max_ingress_distance" not in state:
+ try:
+ state["max_ingress_distance"] = state["ingress_distance"]
+ del state["ingress_distance"]
+ except KeyError:
+ state["max_ingress_distance"] = state["ingress_egress_distance"]
+ del state["ingress_egress_distance"]
+
+ max_ip: Distance = state["max_ingress_distance"]
+ if "min_ingress_distance" not in state:
+ if max_ip < nautical_miles(10):
+ min_ip = nautical_miles(5)
+ else:
+ min_ip = nautical_miles(10)
+ state["min_ingress_distance"] = min_ip
+
self.__dict__.update(state)
@@ -103,7 +121,8 @@ MODERN_DOCTRINE = Doctrine(
hold_distance=nautical_miles(15),
push_distance=nautical_miles(20),
join_distance=nautical_miles(20),
- ingress_distance=nautical_miles(45),
+ max_ingress_distance=nautical_miles(45),
+ min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000),
@@ -139,7 +158,8 @@ COLDWAR_DOCTRINE = Doctrine(
hold_distance=nautical_miles(10),
push_distance=nautical_miles(10),
join_distance=nautical_miles(10),
- ingress_distance=nautical_miles(30),
+ max_ingress_distance=nautical_miles(30),
+ min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000),
@@ -175,7 +195,8 @@ WWII_DOCTRINE = Doctrine(
push_distance=nautical_miles(5),
join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
- ingress_distance=nautical_miles(7),
+ max_ingress_distance=nautical_miles(7),
+ min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000),
diff --git a/game/flightplan/__init__.py b/game/flightplan/__init__.py
new file mode 100644
index 00000000..51dce4a3
--- /dev/null
+++ b/game/flightplan/__init__.py
@@ -0,0 +1 @@
+from .ipzonegeometry import IpZoneGeometry
diff --git a/game/flightplan/ipzonegeometry.py b/game/flightplan/ipzonegeometry.py
new file mode 100644
index 00000000..92e647c2
--- /dev/null
+++ b/game/flightplan/ipzonegeometry.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import shapely.ops
+from dcs import Point
+from shapely.geometry import Point as ShapelyPoint
+
+from game.utils import nautical_miles, meters
+
+if TYPE_CHECKING:
+ from game.coalition import Coalition
+
+
+class IpZoneGeometry:
+ """Defines the zones used for finding optimal IP placement.
+
+ The zones themselves are stored in the class rather than just the resulting IP so
+ that the zones can be drawn in the map for debugging purposes.
+ """
+
+ def __init__(
+ self,
+ target: Point,
+ home: Point,
+ coalition: Coalition,
+ ) -> None:
+ self.threat_zone = coalition.opponent.threat_zone.all
+ self.home = ShapelyPoint(home.x, home.y)
+
+ max_ip_distance = coalition.doctrine.max_ingress_distance
+ min_ip_distance = coalition.doctrine.min_ingress_distance
+
+ # The minimum distance between the home location and the IP.
+ min_distance_from_home = nautical_miles(5)
+
+ # The distance that is expected to be needed between the beginning of the attack
+ # and weapon release. This buffers the threat zone to give a 5nm window between
+ # the edge of the "safe" zone and the actual threat so that "safe" IPs are less
+ # likely to end up with the attacker entering a threatened area.
+ attack_distance_buffer = nautical_miles(5)
+
+ home_threatened = coalition.opponent.threat_zone.threatened(home)
+
+ shapely_target = ShapelyPoint(target.x, target.y)
+ home_to_target_distance = meters(home.distance_to_point(target))
+
+ self.home_bubble = self.home.buffer(home_to_target_distance.meters).difference(
+ self.home.buffer(min_distance_from_home.meters)
+ )
+
+ # If the home zone is not threatened and home is within LAR, constrain the max
+ # range to the home-to-target distance to prevent excessive backtracking.
+ #
+ # If the home zone *is* threatened, we need to back out of the zone to
+ # rendezvous anyway.
+ if not home_threatened and (
+ min_ip_distance < home_to_target_distance < max_ip_distance
+ ):
+ max_ip_distance = home_to_target_distance
+ max_ip_bubble = shapely_target.buffer(max_ip_distance.meters)
+ min_ip_bubble = shapely_target.buffer(min_ip_distance.meters)
+ self.ip_bubble = max_ip_bubble.difference(min_ip_bubble)
+
+ # The intersection of the home bubble and IP bubble will be all the points that
+ # are within the valid IP range that are not farther from home than the target
+ # is. However, if the origin airfield is threatened but there are safe
+ # placements for the IP, we should not constrain to the home zone. In this case
+ # we'll either end up with a safe zone outside the home zone and pick the
+ # closest point in to to home (minimizing backtracking), or we'll have no safe
+ # IP anywhere within range of the target, and we'll later pick the IP nearest
+ # the edge of the threat zone.
+ if home_threatened:
+ self.permissible_zone = self.ip_bubble
+ else:
+ self.permissible_zone = self.ip_bubble.intersection(self.home_bubble)
+
+ if self.permissible_zone.is_empty:
+ # If home is closer to the target than the min range, there will not be an
+ # IP solution that's close enough to home, in which case we need to ignore
+ # the home bubble.
+ self.permissible_zone = self.ip_bubble
+
+ self.safe_zone = self.permissible_zone.difference(
+ self.threat_zone.buffer(attack_distance_buffer.meters)
+ )
+
+ def _unsafe_ip(self) -> ShapelyPoint:
+ unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
+ if unthreatened_home_zone.is_empty:
+ # Nowhere in our home zone is safe. The package will need to exit the
+ # threatened area to hold and rendezvous. Pick the IP closest to the
+ # edge of the threat zone.
+ return shapely.ops.nearest_points(
+ 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.
+ return shapely.ops.nearest_points(
+ self.permissible_zone, unthreatened_home_zone
+ )[0]
+
+ def _safe_ip(self) -> ShapelyPoint:
+ # We have a zone of possible IPs that are safe, close enough, and in range. Pick
+ # the IP in the zone that's closest to the target.
+ return shapely.ops.nearest_points(self.safe_zone, self.home)[0]
+
+ def find_best_ip(self) -> Point:
+ if self.safe_zone.is_empty:
+ ip = self._unsafe_ip()
+ else:
+ ip = self._safe_ip()
+ return Point(ip.x, ip.y)
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 0927b968..13295d01 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -20,6 +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.theater import (
Airfield,
ControlPoint,
@@ -946,57 +947,35 @@ class FlightPlanBuilder:
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
- # The simple case is where the target is not near the departure airfield. In
- # this case, we can plan the shortest route from the departure airfield to the
- # target, use the nearest non-threatened point *that's farther from the target
- # than the ingress point to avoid backtracking) as the join point.
- #
- # The other case that we need to handle is when the target is close to
- # the origin airfield. In this case we currently fall back to the old planning
- # behavior.
- #
- # A messy (and very unlikely) case that we can't do much about:
- #
- # +--------------+ +---------------+
- # | | | |
- # | IP-+---+-T |
- # | | | |
- # | | | |
- # +--------------+ +---------------+
from gen.ato import PackageWaypoints
- target = self.package.target.position
+ # Start by picking the best IP for the attack.
+ ingress_point = IpZoneGeometry(
+ self.package.target.position,
+ self.package_airfield().position,
+ self.coalition,
+ ).find_best_ip()
- for join_point in self.preferred_join_points():
- join_distance = meters(join_point.distance_to_point(target))
- if join_distance > self.doctrine.ingress_distance:
- break
- else:
+ # 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()
+ self.legacy_package_waypoints_impl(ingress_point)
return
- attack_heading = join_point.heading_between_point(target)
- ingress_point = self._ingress_point(attack_heading)
-
- # The first case described above. The ingress and join points are placed
- # reasonably relative to each other.
+ # 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.
+ # TODO: Estimate attack completion point based on the IP and split from there?
self.package.waypoints = PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
- WaypointBuilder.perturb(
- self.preferred_split_point(ingress_point, join_point)
- ),
+ WaypointBuilder.perturb(join_point),
)
- def retreat_point(self, origin: Point) -> Point:
- return self.threat_zones.closest_boundary(origin)
-
- def legacy_package_waypoints_impl(self) -> None:
+ def legacy_package_waypoints_impl(self, ingress_point: Point) -> None:
from gen.ato import PackageWaypoints
- ingress_point = self._ingress_point(self._target_heading_to_package_airfield())
join_point = self._rendezvous_point(ingress_point)
self.package.waypoints = PackageWaypoints(
WaypointBuilder.perturb(join_point),
@@ -1009,23 +988,15 @@ class FlightPlanBuilder:
if not self.threat_zones.threatened(point):
yield point
- def preferred_join_points(self) -> Iterator[Point]:
+ def preferred_join_point(self, ingress_point: Point) -> Optional[Point]:
# Use non-threatened points along the path to the target as the join point. We
# may need to try more than one in the event that the close non-threatened
# points are closer than the ingress point itself.
- return self.safe_points_between(
- self.package.target.position, self.package_airfield().position
- )
-
- def preferred_split_point(self, ingress_point: Point, join_point: Point) -> Point:
- # Use non-threatened points along the path to the target as the join point. We
- # may need to try more than one in the event that the close non-threatened
- # points are closer than the ingress point itself.
- for point in self.safe_points_between(
+ for join_point in self.safe_points_between(
ingress_point, self.package_airfield().position
):
- return point
- return join_point
+ return join_point
+ return None
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a strike flight plan.
@@ -1847,7 +1818,7 @@ class FlightPlanBuilder:
def _ingress_point(self, heading: float) -> Point:
return self.package.target.position.point_from_heading(
- heading - 180, self.doctrine.ingress_distance.meters
+ heading - 180, self.doctrine.max_ingress_distance.meters
)
def _target_heading_to_package_airfield(self) -> float:
diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py
index 02eadd9f..c2e6706a 100644
--- a/qt_ui/widgets/map/mapmodel.py
+++ b/qt_ui/widgets/map/mapmodel.py
@@ -27,7 +27,12 @@ from game.transfers import MultiGroupTransport, TransportMap
from game.utils import meters, nautical_miles
from gen.ato import AirTaskingOrder
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
-from gen.flights.flightplan import FlightPlan, PatrollingFlightPlan, CasFlightPlan
+from gen.flights.flightplan import (
+ FlightPlan,
+ PatrollingFlightPlan,
+ CasFlightPlan,
+)
+from game.flightplan.ipzonegeometry import IpZoneGeometry
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel, AtoModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -39,6 +44,10 @@ LeafletPoly = list[LeafletLatLon]
MAX_SHIP_DISTANCE = nautical_miles(80)
+# Set to True to enable computing expensive debugging information. At the time of
+# writing this only controls computing the waypoint placement zones.
+ENABLE_EXPENSIVE_DEBUG_TOOLS = False
+
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
#
# https://bugreports.qt.io/browse/PYSIDE-1426
@@ -512,6 +521,19 @@ class FlightJs(QObject):
selectedChanged = Signal()
commitBoundaryChanged = Signal()
+ originChanged = Signal()
+
+ @Property(list, notify=originChanged)
+ def origin(self) -> LeafletLatLon:
+ return self._waypoints[0].position
+
+ targetChanged = Signal()
+
+ @Property(list, notify=targetChanged)
+ def target(self) -> LeafletLatLon:
+ ll = self.theater.point_to_ll(self.flight.package.target.position)
+ return [ll.latitude, ll.longitude]
+
def __init__(
self,
flight: Flight,
@@ -769,6 +791,56 @@ class UnculledZone(QObject):
)
+class IpZonesJs(QObject):
+ homeBubbleChanged = Signal()
+ ipBubbleChanged = Signal()
+ permissibleZoneChanged = Signal()
+ safeZoneChanged = Signal()
+
+ def __init__(
+ self,
+ home_bubble: list[LeafletPoly],
+ ip_bubble: list[LeafletPoly],
+ permissible_zone: list[LeafletPoly],
+ safe_zone: list[LeafletPoly],
+ ) -> None:
+ super().__init__()
+ self._home_bubble = home_bubble
+ self._ip_bubble = ip_bubble
+ self._permissible_zone = permissible_zone
+ self._safe_zone = safe_zone
+
+ @Property(list, notify=homeBubbleChanged)
+ def homeBubble(self) -> list[LeafletPoly]:
+ return self._home_bubble
+
+ @Property(list, notify=ipBubbleChanged)
+ def ipBubble(self) -> list[LeafletPoly]:
+ return self._ip_bubble
+
+ @Property(list, notify=permissibleZoneChanged)
+ def permissibleZone(self) -> list[LeafletPoly]:
+ return self._permissible_zone
+
+ @Property(list, notify=permissibleZoneChanged)
+ def safeZone(self) -> list[LeafletPoly]:
+ return self._safe_zone
+
+ @classmethod
+ def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs:
+ 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),
+ shapely_to_leaflet_polys(geometry.ip_bubble, game.theater),
+ shapely_to_leaflet_polys(geometry.permissible_zone, game.theater),
+ shapely_to_leaflet_polys(geometry.safe_zone, game.theater),
+ )
+
+
class MapModel(QObject):
cleared = Signal()
@@ -782,6 +854,7 @@ class MapModel(QObject):
navmeshesChanged = Signal()
mapZonesChanged = Signal()
unculledZonesChanged = Signal()
+ ipZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None:
super().__init__()
@@ -798,6 +871,7 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
+ self._ip_zones = IpZonesJs([], [], [], [])
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)
@@ -821,6 +895,7 @@ class MapModel(QObject):
self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
+ self._ip_zones = IpZonesJs([], [], [], [])
self.cleared.emit()
def set_package_selection(self, index: int) -> None:
@@ -896,11 +971,24 @@ class MapModel(QObject):
)
return flights
+ def _get_selected_flight(self) -> Optional[Flight]:
+ for p_idx, package in enumerate(self.game.blue.ato.packages):
+ for f_idx, flight in enumerate(package.flights):
+ if (p_idx, f_idx) == self._selected_flight_index:
+ return flight
+ return None
+
def reset_atos(self) -> None:
self._flights = self._flights_in_ato(
self.game.blue.ato, blue=True
) + 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)
+ else:
+ self._ip_zones = IpZonesJs([], [], [], [])
+ self.ipZonesChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> List[FlightJs]:
@@ -1029,6 +1117,10 @@ class MapModel(QObject):
def unculledZones(self) -> list[UnculledZone]:
return self._unculled_zones
+ @Property(IpZonesJs, notify=ipZonesChanged)
+ def ipZones(self) -> IpZonesJs:
+ return self._ip_zones
+
@property
def game(self) -> Game:
if self.game_model.game is None:
diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js
index 6cae9ae2..e56a9fc0 100644
--- a/resources/ui/map/map.js
+++ b/resources/ui/map/map.js
@@ -1,3 +1,7 @@
+// Won't actually enable anything unless the same property is set in
+// mapmodel.py.
+const ENABLE_EXPENSIVE_DEBUG_TOOLS = false;
+
const Colors = Object.freeze({
Blue: "#0084ff",
Red: "#c85050",
@@ -124,26 +128,26 @@ const map = L.map("map", {
L.control.scale({ maxWidth: 200 }).addTo(map);
const rulerOptions = {
- position: 'topleft',
+ position: "topleft",
circleMarker: {
color: Colors.Highlight,
- radius: 2
+ radius: 2,
},
lineStyle: {
color: Colors.Highlight,
- dashArray: '1,6'
+ dashArray: "1,6",
},
lengthUnit: {
display: "nm",
decimal: "2",
factor: 0.539956803,
- label: "Distance:"
+ label: "Distance:",
},
angleUnit: {
- display: '°',
+ display: "°",
decimal: 0,
- label: "Bearing:"
- }
+ label: "Bearing:",
+ },
};
L.control.ruler(rulerOptions).addTo(map);
@@ -194,6 +198,48 @@ 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 debugControlGroups = {
+ "Blue Threat Zones": {
+ Hide: L.layerGroup().addTo(map),
+ Full: blueFullThreatZones,
+ Aircraft: blueAircraftThreatZones,
+ "Air Defenses": blueAirDefenseThreatZones,
+ "Radar SAMs": blueRadarSamThreatZones,
+ },
+ "Red Threat Zones": {
+ Hide: L.layerGroup().addTo(map),
+ Full: redFullThreatZones,
+ Aircraft: redAircraftThreatZones,
+ "Air Defenses": redAirDefenseThreatZones,
+ "Radar SAMs": redRadarSamThreatZones,
+ },
+ Navmeshes: {
+ Hide: L.layerGroup().addTo(map),
+ Blue: blueNavmesh,
+ Red: redNavmesh,
+ },
+ "Map Zones": {
+ "Inclusion zones": inclusionZones,
+ "Exclusion zones": exclusionZones,
+ "Sea zones": seaZones,
+ "Culling exclusion zones": unculledZones,
+ },
+};
+
+if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
+ debugControlGroups["IP Zones"] = {
+ "Home bubble": homeBubble,
+ "IP bubble": ipBubble,
+ "Permissible zone": permissibleZone,
+ "Safe zone": safeZone,
+ };
+}
+
// Main map controls. These are the ones that we expect users to interact with.
// These are always open, which unfortunately means that the scroll bar will not
// appear if the menu doesn't fit. This fits in the smallest window size we
@@ -239,41 +285,11 @@ L.control
// Debug map controls. Hover over to open. Not something most users will want or
// need to interact with.
L.control
- .groupedLayers(
- null,
- {
- "Blue Threat Zones": {
- Hide: L.layerGroup().addTo(map),
- Full: blueFullThreatZones,
- Aircraft: blueAircraftThreatZones,
- "Air Defenses": blueAirDefenseThreatZones,
- "Radar SAMs": blueRadarSamThreatZones,
- },
- "Red Threat Zones": {
- Hide: L.layerGroup().addTo(map),
- Full: redFullThreatZones,
- Aircraft: redAircraftThreatZones,
- "Air Defenses": redAirDefenseThreatZones,
- "Radar SAMs": redRadarSamThreatZones,
- },
- Navmeshes: {
- Hide: L.layerGroup().addTo(map),
- Blue: blueNavmesh,
- Red: redNavmesh,
- },
- "Map Zones": {
- "Inclusion zones": inclusionZones,
- "Exclusion zones": exclusionZones,
- "Sea zones": seaZones,
- "Culling exclusion zones": unculledZones,
- },
- },
- {
- position: "topleft",
- exclusiveGroups: ["Blue Threat Zones", "Red Threat Zones", "Navmeshes"],
- groupCheckboxes: true,
- }
- )
+ .groupedLayers(null, debugControlGroups, {
+ position: "topleft",
+ exclusiveGroups: ["Blue Threat Zones", "Red Threat Zones", "Navmeshes"],
+ groupCheckboxes: true,
+ })
.addTo(map);
let game;
@@ -291,6 +307,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.navmeshesChanged.connect(drawNavmeshes);
game.mapZonesChanged.connect(drawMapZones);
game.unculledZonesChanged.connect(drawUnculledZones);
+ game.ipZonesChanged.connect(drawIpZones);
});
function recenterMap(center) {
@@ -570,7 +587,11 @@ class TheaterGroundObject {
}
L.marker(this.tgo.position, { icon: this.icon() })
- .bindTooltip(`${this.tgo.name} (${this.tgo.controlPointName})
${this.tgo.units.join("
")}`)
+ .bindTooltip(
+ `${this.tgo.name} (${
+ this.tgo.controlPointName
+ })
${this.tgo.units.join("
")}`
+ )
.on("click", () => this.tgo.showInfoDialog())
.on("contextmenu", () => this.tgo.showPackageDialog())
.addTo(this.layer());
@@ -970,6 +991,37 @@ function drawUnculledZones() {
}
}
+function drawIpZones() {
+ homeBubble.clearLayers();
+ ipBubble.clearLayers();
+ permissibleZone.clearLayers();
+ safeZone.clearLayers();
+
+ L.polygon(game.ipZones.homeBubble, {
+ color: Colors.Highlight,
+ fillOpacity: 0.1,
+ interactive: false,
+ }).addTo(homeBubble);
+
+ L.polygon(game.ipZones.ipBubble, {
+ color: "#bb89ff",
+ fillOpacity: 0.1,
+ interactive: false,
+ }).addTo(ipBubble);
+
+ L.polygon(game.ipZones.permissibleZone, {
+ color: "#ffffff",
+ fillOpacity: 0.1,
+ interactive: false,
+ }).addTo(permissibleZone);
+
+ L.polygon(game.ipZones.safeZone, {
+ color: Colors.Green,
+ fillOpacity: 0.1,
+ interactive: false,
+ }).addTo(safeZone);
+}
+
function drawInitialMap() {
recenterMap(game.mapCenter);
drawControlPoints();
@@ -981,6 +1033,7 @@ function drawInitialMap() {
drawNavmeshes();
drawMapZones();
drawUnculledZones();
+ drawIpZones();
}
function clearAllLayers() {