mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
[1/3] Rework IP placement.
Test cases: 1. Target is not threatened. The IP should be placed on a direct heading from the origin to the target at the max ingress distance, or very near the origin airfield if the airfield is closer to the target than the IP distance. 2. Unthreatened home zone, max IP between origin and target, safe locations available for IP. The IP should be placed in LAR at the closest point to home. 3. Unthreatened home zone, origin within LAR, safe locations available for IP. The IP should be placed near the origin airfield to prevent backtracking more than needed. 4. Unthreatened home zone, origin entirely nearer the target than LAR, safe locations available for IP. The IP should be placed in LAR as close as possible to the origin. 5. Threatened home zone, safe locations available for IP. The IP should be placed in LAR as close as possible to the origin. 6. No safe IP. The IP should be placed in LAR at the point nearest the threat boundary.
This commit is contained in:
parent
2b696144e3
commit
e03d710d53
@ -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),
|
||||
|
||||
1
game/flightplan/__init__.py
Normal file
1
game/flightplan/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .ipzonegeometry import IpZoneGeometry
|
||||
114
game/flightplan/ipzonegeometry.py
Normal file
114
game/flightplan/ipzonegeometry.py
Normal file
@ -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)
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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})<br />${this.tgo.units.join("<br />")}`)
|
||||
.bindTooltip(
|
||||
`${this.tgo.name} (${
|
||||
this.tgo.controlPointName
|
||||
})<br />${this.tgo.units.join("<br />")}`
|
||||
)
|
||||
.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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user