[3/3] Rework hold points.

This commit is contained in:
Dan Albert 2021-07-15 22:18:53 -07:00
parent d444d716f5
commit 82cca0a602
8 changed files with 346 additions and 190 deletions

View File

@ -7,6 +7,7 @@ Saves from 3.x are not compatible with 5.0.
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
## Fixes

View File

@ -1,2 +1,3 @@
from .holdzonegeometry import HoldZoneGeometry
from .ipzonegeometry import IpZoneGeometry
from .joinzonegeometry import JoinZoneGeometry

View File

@ -0,0 +1,108 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
from shapely.geometry import Point as ShapelyPoint, Polygon, MultiPolygon
from game.theater import ConflictTheater
from game.utils import nautical_miles
if TYPE_CHECKING:
from game.coalition import Coalition
class HoldZoneGeometry:
"""Defines the zones used for finding optimal hold point placement.
The zones themselves are stored in the class rather than just the resulting hold
point so that the zones can be drawn in the map for debugging purposes.
"""
def __init__(
self,
target: Point,
home: Point,
ip: Point,
join: Point,
coalition: Coalition,
theater: ConflictTheater,
) -> None:
# Hold points are placed one of two ways. Either approach guarantees:
#
# * Safe hold point.
# * Minimum distance to the join point.
# * Not closer to the target than the join point.
#
# 1. As near the join point as possible with a specific distance from the
# departure airfield. This prevents loitering directly above the airfield but
# also keeps the hold point close to the departure airfield.
#
# 2. Alternatively, if the entire home zone is excluded by the above criteria,
# as neat the departure airfield as possible within a minimum distance from
# the join point, with a restricted turn angle at the join point. This
# handles the case where we need to backtrack from the departure airfield and
# the join point to place the hold point, but the turn angle limit restricts
# the maximum distance of the backtrack while maintaining the direction of
# the flight plan.
self.threat_zone = coalition.opponent.threat_zone.all
self.home = ShapelyPoint(home.x, home.y)
self.join = ShapelyPoint(join.x, join.y)
self.join_bubble = self.join.buffer(coalition.doctrine.push_distance.meters)
join_to_target_distance = join.distance_to_point(target)
self.target_bubble = ShapelyPoint(target.x, target.y).buffer(
join_to_target_distance
)
self.home_bubble = self.home.buffer(coalition.doctrine.hold_distance.meters)
excluded_zones = shapely.ops.unary_union(
[self.join_bubble, self.target_bubble, self.threat_zone]
)
if not isinstance(excluded_zones, MultiPolygon):
excluded_zones = MultiPolygon([excluded_zones])
self.excluded_zones = excluded_zones
join_heading = ip.heading_between_point(join)
# 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
join_limit_ccw = join.point_from_heading(
join_heading - turn_limit, large_distance
)
join_limit_cw = join.point_from_heading(
join_heading + turn_limit, large_distance
)
join_direction_limit_wedge = Polygon(
[
(join.x, join.y),
(join_limit_ccw.x, join_limit_ccw.y),
(join_limit_cw.x, join_limit_cw.y),
]
)
permissible_zones = (
coalition.nav_mesh.map_bounds(theater)
.intersection(join_direction_limit_wedge)
.difference(self.excluded_zones)
.difference(self.home_bubble)
)
if not isinstance(permissible_zones, MultiPolygon):
permissible_zones = MultiPolygon([permissible_zones])
self.permissible_zones = permissible_zones
self.preferred_lines = self.home_bubble.boundary.difference(self.excluded_zones)
def find_best_hold_point(self) -> Point:
if self.preferred_lines.is_empty:
hold, _ = shapely.ops.nearest_points(self.permissible_zones, self.home)
else:
hold, _ = shapely.ops.nearest_points(self.preferred_lines, self.join)
return Point(hold.x, hold.y)

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
from shapely.geometry import Point as ShapelyPoint
from shapely.geometry import Point as ShapelyPoint, MultiPolygon
from game.utils import nautical_miles, meters
@ -81,10 +81,14 @@ class IpZoneGeometry:
# the home bubble.
self.permissible_zone = self.ip_bubble
self.safe_zone = self.permissible_zone.difference(
safe_zones = self.permissible_zone.difference(
self.threat_zone.buffer(attack_distance_buffer.meters)
)
if not isinstance(safe_zones, MultiPolygon):
safe_zones = MultiPolygon([safe_zones])
self.safe_zones = safe_zones
def _unsafe_ip(self) -> ShapelyPoint:
unthreatened_home_zone = self.home_bubble.difference(self.threat_zone)
if unthreatened_home_zone.is_empty:
@ -104,10 +108,10 @@ class IpZoneGeometry:
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]
return shapely.ops.nearest_points(self.safe_zones, self.home)[0]
def find_best_ip(self) -> Point:
if self.safe_zone.is_empty:
if self.safe_zones.is_empty:
ip = self._unsafe_ip()
else:
ip = self._safe_ip()

View File

@ -4,7 +4,12 @@ from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
from shapely.geometry import Point as ShapelyPoint, Polygon
from shapely.geometry import (
Point as ShapelyPoint,
Polygon,
MultiPolygon,
MultiLineString,
)
from game.theater import ConflictTheater
from game.utils import nautical_miles
@ -51,10 +56,14 @@ class JoinZoneGeometry:
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]
excluded_zones = shapely.ops.unary_union(
[self.ip_bubble, self.target_bubble, self.threat_zone]
)
if not isinstance(excluded_zones, MultiPolygon):
excluded_zones = MultiPolygon([excluded_zones])
self.excluded_zones = excluded_zones
ip_heading = target.heading_between_point(ip)
# Arbitrarily large since this is later constrained by the map boundary, and
@ -73,12 +82,14 @@ class JoinZoneGeometry:
]
)
self.permissible_line = (
coalition.nav_mesh.map_bounds(theater)
.intersection(ip_direction_limit_wedge)
.intersection(self.excluded_zone.boundary)
)
permissible_lines = ip_direction_limit_wedge.intersection(
self.excluded_zones.boundary
).difference(self.home_bubble)
if not isinstance(permissible_lines, MultiLineString):
permissible_lines = MultiLineString([permissible_lines])
self.permissible_lines = permissible_lines
def find_best_join_point(self) -> Point:
join, _ = shapely.ops.nearest_points(self.permissible_line, self.home)
join, _ = shapely.ops.nearest_points(self.permissible_lines, self.home)
return Point(join.x, join.y)

View File

@ -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, JoinZoneGeometry
from game.flightplan import IpZoneGeometry, JoinZoneGeometry, HoldZoneGeometry
from game.theater import (
Airfield,
ControlPoint,
@ -975,31 +975,6 @@ class FlightPlanBuilder:
WaypointBuilder.perturb(join_point),
)
def legacy_package_waypoints_impl(self, ingress_point: Point) -> None:
from gen.ato import PackageWaypoints
join_point = self._rendezvous_point(ingress_point)
self.package.waypoints = PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
WaypointBuilder.perturb(join_point),
)
def safe_points_between(self, a: Point, b: Point) -> Iterator[Point]:
for point in self.coalition.nav_mesh.shortest_path(a, b)[1:-1]:
if not self.threat_zones.threatened(point):
yield 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.
for join_point in self.safe_points_between(
ingress_point, self.package_airfield().position
):
return join_point
return None
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a strike flight plan.
@ -1675,48 +1650,10 @@ class FlightPlanBuilder:
origin = flight.departure.position
target = self.package.target.position
join = self.package.waypoints.join
origin_to_join = origin.distance_to_point(join)
if meters(origin_to_join) < self.doctrine.push_distance:
# If the origin airfield is closer to the join point, than the minimum push
# distance. Plan the hold point such that it retreats from the origin
# airfield.
return join.point_from_heading(
target.heading_between_point(origin), self.doctrine.push_distance.meters
)
heading_to_join = origin.heading_between_point(join)
hold_point = origin.point_from_heading(
heading_to_join, self.doctrine.push_distance.meters
)
hold_distance = meters(hold_point.distance_to_point(join))
if hold_distance >= self.doctrine.push_distance:
# Hold point is between the origin airfield and the join point and
# spaced sufficiently.
return hold_point
# The hold point is between the origin airfield and the join point, but
# the distance between the hold point and the join point is too short.
# Bend the hold point out to extend the distance while maintaining the
# minimum distance from the origin airfield to keep the AI flying
# properly.
origin_to_join = origin.distance_to_point(join)
cos_theta = (
self.doctrine.hold_distance.meters ** 2
+ origin_to_join ** 2
- self.doctrine.join_distance.meters ** 2
) / (2 * self.doctrine.hold_distance.meters * origin_to_join)
try:
theta = math.acos(cos_theta)
except ValueError:
# No solution that maintains hold and join distances. Extend the
# hold point away from the target.
return origin.point_from_heading(
target.heading_between_point(origin), self.doctrine.hold_distance.meters
)
return origin.point_from_heading(
heading_to_join - theta, self.doctrine.hold_distance.meters
)
ip = self.package.waypoints.ingress
return HoldZoneGeometry(
target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point()
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_rtb_waypoint(
@ -1779,59 +1716,6 @@ class FlightPlanBuilder:
lead_time=lead_time,
)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that retreats from the origin airfield."""
return attack_transition.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position
),
self.doctrine.join_distance.meters,
)
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading(
heading, -self.doctrine.join_distance.meters
)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point(
self.package.target.position
)
origin_target_distance = self._distance_to_package_airfield(
self.package.target.position
)
# If the origin point is closer to the target than the ingress point,
# the rendezvous point should be positioned in a position that retreats
# from the origin airfield.
return origin_target_distance < transition_target_distance
def _rendezvous_point(self, attack_transition: Point) -> Point:
"""Returns the position of the rendezvous point.
Args:
attack_transition: The ingress or target point for this rendezvous.
"""
if self._rendezvous_should_retreat(attack_transition):
return self._retreating_rendezvous_point(attack_transition)
return self._advancing_rendezvous_point(attack_transition)
def _ingress_point(self, heading: float) -> Point:
return self.package.target.position.point_from_heading(
heading - 180, self.doctrine.max_ingress_distance.meters
)
def _target_heading_to_package_airfield(self) -> float:
return self._heading_to_package_airfield(self.package.target.position)
def _heading_to_package_airfield(self, point: Point) -> float:
return self.package_airfield().position.heading_between_point(point)
def _distance_to_package_airfield(self, point: Point) -> float:
return self.package_airfield().position.distance_to_point(point)
def package_airfield(self) -> ControlPoint:
# We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package.

View File

@ -8,11 +8,17 @@ from PySide2.QtCore import Property, QObject, Signal, Slot
from dcs import Point
from dcs.unit import Unit
from dcs.vehicles import vehicle_map
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPolygon
from shapely.geometry import (
LineString,
Point as ShapelyPoint,
Polygon,
MultiPolygon,
MultiLineString,
)
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.flightplan import JoinZoneGeometry
from game.flightplan import JoinZoneGeometry, HoldZoneGeometry
from game.navmesh import NavMesh, NavMeshPoly
from game.profiling import logged_duration
from game.theater import (
@ -89,6 +95,12 @@ def shapely_line_to_leaflet_points(
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
def shapely_lines_to_leaflet_points(
lines: MultiLineString, theater: ConflictTheater
) -> list[list[LeafletLatLon]]:
return [shapely_line_to_leaflet_points(l, theater) for l in lines.geoms]
class ControlPointJs(QObject):
nameChanged = Signal()
blueChanged = Signal()
@ -802,36 +814,36 @@ class IpZonesJs(QObject):
homeBubbleChanged = Signal()
ipBubbleChanged = Signal()
permissibleZoneChanged = Signal()
safeZoneChanged = Signal()
safeZonesChanged = Signal()
def __init__(
self,
home_bubble: list[LeafletPoly],
ip_bubble: list[LeafletPoly],
permissible_zone: list[LeafletPoly],
safe_zone: list[LeafletPoly],
home_bubble: LeafletPoly,
ip_bubble: LeafletPoly,
permissible_zone: LeafletPoly,
safe_zones: 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
self._safe_zones = safe_zones
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> list[LeafletPoly]:
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=ipBubbleChanged)
def ipBubble(self) -> list[LeafletPoly]:
def ipBubble(self) -> LeafletPoly:
return self._ip_bubble
@Property(list, notify=permissibleZoneChanged)
def permissibleZone(self) -> list[LeafletPoly]:
def permissibleZone(self) -> LeafletPoly:
return self._permissible_zone
@Property(list, notify=safeZoneChanged)
def safeZone(self) -> list[LeafletPoly]:
return self._safe_zone
@Property(list, notify=safeZonesChanged)
def safeZones(self) -> list[LeafletPoly]:
return self._safe_zones
@classmethod
def empty(cls) -> IpZonesJs:
@ -845,10 +857,10 @@ class IpZonesJs(QObject):
home = flight.departure
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),
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.permissible_zone, game.theater),
shapely_to_leaflet_polys(geometry.safe_zones, game.theater),
)
@ -856,43 +868,43 @@ class JoinZonesJs(QObject):
homeBubbleChanged = Signal()
targetBubbleChanged = Signal()
ipBubbleChanged = Signal()
excludedZoneChanged = Signal()
permissibleLineChanged = Signal()
excludedZonesChanged = Signal()
permissibleLinesChanged = Signal()
def __init__(
self,
home_bubble: list[LeafletPoly],
target_bubble: list[LeafletPoly],
ip_bubble: list[LeafletPoly],
excluded_zone: list[LeafletPoly],
permissible_line: list[LeafletLatLon],
home_bubble: LeafletPoly,
target_bubble: LeafletPoly,
ip_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly],
permissible_lines: list[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
self._excluded_zones = excluded_zones
self._permissible_lines = permissible_lines
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> list[LeafletPoly]:
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=targetBubbleChanged)
def targetBubble(self) -> list[LeafletPoly]:
def targetBubble(self) -> LeafletPoly:
return self._target_bubble
@Property(list, notify=ipBubbleChanged)
def ipBubble(self) -> list[LeafletPoly]:
def ipBubble(self) -> LeafletPoly:
return self._ip_bubble
@Property(list, notify=excludedZoneChanged)
def excludedZone(self) -> list[LeafletPoly]:
return self._excluded_zone
@Property(list, notify=excludedZonesChanged)
def excludedZones(self) -> list[LeafletPoly]:
return self._excluded_zones
@Property(list, notify=permissibleLineChanged)
def permissibleLine(self) -> list[LeafletLatLon]:
return self._permissible_line
@Property(list, notify=permissibleLinesChanged)
def permissibleLines(self) -> list[list[LeafletLatLon]]:
return self._permissible_lines
@classmethod
def empty(cls) -> JoinZonesJs:
@ -911,11 +923,87 @@ class JoinZonesJs(QObject):
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),
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.ip_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_lines_to_leaflet_points(geometry.permissible_lines, game.theater),
)
class HoldZonesJs(QObject):
homeBubbleChanged = Signal()
targetBubbleChanged = Signal()
joinBubbleChanged = Signal()
excludedZonesChanged = Signal()
permissibleZonesChanged = Signal()
permissibleLinesChanged = Signal()
def __init__(
self,
home_bubble: LeafletPoly,
target_bubble: LeafletPoly,
join_bubble: LeafletPoly,
excluded_zones: list[LeafletPoly],
permissible_zones: list[LeafletPoly],
permissible_lines: list[list[LeafletLatLon]],
) -> None:
super().__init__()
self._home_bubble = home_bubble
self._target_bubble = target_bubble
self._join_bubble = join_bubble
self._excluded_zones = excluded_zones
self._permissible_zones = permissible_zones
self._permissible_lines = permissible_lines
@Property(list, notify=homeBubbleChanged)
def homeBubble(self) -> LeafletPoly:
return self._home_bubble
@Property(list, notify=targetBubbleChanged)
def targetBubble(self) -> LeafletPoly:
return self._target_bubble
@Property(list, notify=joinBubbleChanged)
def joinBubble(self) -> LeafletPoly:
return self._join_bubble
@Property(list, notify=excludedZonesChanged)
def excludedZones(self) -> list[LeafletPoly]:
return self._excluded_zones
@Property(list, notify=permissibleZonesChanged)
def permissibleZones(self) -> list[LeafletPoly]:
return self._permissible_zones
@Property(list, notify=permissibleLinesChanged)
def permissibleLines(self) -> list[list[LeafletLatLon]]:
return self._permissible_lines
@classmethod
def empty(cls) -> HoldZonesJs:
return HoldZonesJs([], [], [], [], [], [])
@classmethod
def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs:
if not ENABLE_EXPENSIVE_DEBUG_TOOLS:
return JoinZonesJs.empty()
target = flight.package.target
home = flight.departure
if flight.package.waypoints is None:
return HoldZonesJs.empty()
ip = flight.package.waypoints.ingress
join = flight.package.waypoints.join
geometry = HoldZoneGeometry(
target.position, home.position, ip, join, game.blue, game.theater
)
return HoldZonesJs(
shapely_poly_to_leaflet_points(geometry.home_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.target_bubble, game.theater),
shapely_poly_to_leaflet_points(geometry.join_bubble, game.theater),
shapely_to_leaflet_polys(geometry.excluded_zones, game.theater),
shapely_to_leaflet_polys(geometry.permissible_zones, game.theater),
[], # shapely_to_leaflet_polys(geometry.permissible_lines, game.theater),
)
@ -934,6 +1022,7 @@ class MapModel(QObject):
unculledZonesChanged = Signal()
ipZonesChanged = Signal()
joinZonesChanged = Signal()
holdZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None:
super().__init__()
@ -952,6 +1041,7 @@ class MapModel(QObject):
self._unculled_zones = []
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.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)
@ -1067,11 +1157,14 @@ class MapModel(QObject):
if selected_flight is None:
self._ip_zones = IpZonesJs.empty()
self._join_zones = JoinZonesJs.empty()
self._hold_zones = HoldZonesJs.empty()
else:
self._ip_zones = IpZonesJs.for_flight(selected_flight, self.game)
self._join_zones = JoinZonesJs.for_flight(selected_flight, self.game)
self._hold_zones = HoldZonesJs.for_flight(selected_flight, self.game)
self.ipZonesChanged.emit()
self.joinZonesChanged.emit()
self.holdZonesChanged.emit()
@Property(list, notify=flightsChanged)
def flights(self) -> List[FlightJs]:
@ -1208,6 +1301,10 @@ class MapModel(QObject):
def joinZones(self) -> JoinZonesJs:
return self._join_zones
@Property(HoldZonesJs, notify=holdZonesChanged)
def holdZones(self) -> HoldZonesJs:
return self._hold_zones
@property
def game(self) -> Game:
if self.game_model.game is None:

View File

@ -198,8 +198,10 @@ const exclusionZones = L.layerGroup();
const seaZones = L.layerGroup();
const unculledZones = L.layerGroup();
const noWaypointZones = L.layerGroup();
const ipZones = L.layerGroup();
const joinZones = L.layerGroup().addTo(map);
const joinZones = L.layerGroup();
const holdZones = L.layerGroup().addTo(map);
const debugControlGroups = {
"Blue Threat Zones": {
@ -231,8 +233,10 @@ const debugControlGroups = {
if (ENABLE_EXPENSIVE_DEBUG_TOOLS) {
debugControlGroups["Waypoint Zones"] = {
None: noWaypointZones,
"IP Zones": ipZones,
"Join Zones": joinZones,
"Hold Zones": holdZones,
};
}
@ -310,6 +314,7 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
game.unculledZonesChanged.connect(drawUnculledZones);
game.ipZonesChanged.connect(drawIpZones);
game.joinZonesChanged.connect(drawJoinZones);
game.holdZonesChanged.connect(drawHoldZones);
});
function recenterMap(center) {
@ -1014,11 +1019,13 @@ function drawIpZones() {
interactive: false,
}).addTo(ipZones);
L.polygon(game.ipZones.safeZone, {
color: Colors.Green,
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
for (const zone of game.ipZones.safeZones) {
L.polygon(zone, {
color: Colors.Green,
fillOpacity: 0.1,
interactive: false,
}).addTo(ipZones);
}
}
function drawJoinZones() {
@ -1042,17 +1049,59 @@ function drawJoinZones() {
interactive: false,
}).addTo(joinZones);
L.polygon(game.joinZones.excludedZone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(joinZones);
for (const zone of game.joinZones.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(joinZones);
}
L.polyline(game.joinZones.permissibleLine, {
color: Colors.Green,
for (const line of game.joinZones.permissibleLines) {
L.polyline(line, {
color: Colors.Green,
interactive: false,
}).addTo(joinZones);
}
}
function drawHoldZones() {
holdZones.clearLayers();
L.polygon(game.holdZones.homeBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(joinZones);
}).addTo(holdZones);
L.polygon(game.holdZones.targetBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
L.polygon(game.holdZones.joinBubble, {
color: Colors.Highlight,
fillOpacity: 0.1,
interactive: false,
}).addTo(holdZones);
for (const zone of game.holdZones.excludedZones) {
L.polygon(zone, {
color: "#ffa500",
fillOpacity: 0.2,
stroke: false,
interactive: false,
}).addTo(holdZones);
}
for (const zone of game.holdZones.permissibleZones) {
L.polygon(zone, {
color: Colors.Green,
interactive: false,
}).addTo(holdZones);
}
}
function drawInitialMap() {
@ -1068,6 +1117,7 @@ function drawInitialMap() {
drawUnculledZones();
drawIpZones();
drawJoinZones();
drawHoldZones();
}
function clearAllLayers() {