diff --git a/game/threatzones.py b/game/threatzones.py index b8ea85ac..17f4df96 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import singledispatchmethod -from typing import TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union from dcs.mapping import Point as DcsPoint from shapely.geometry import ( @@ -13,7 +13,9 @@ from shapely.geometry import ( from shapely.geometry.base import BaseGeometry from shapely.ops import nearest_points, unary_union -from game.utils import nautical_miles +from game.theater import ControlPoint +from game.utils import Distance, meters, nautical_miles +from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import Flight if TYPE_CHECKING: @@ -78,6 +80,39 @@ class ThreatZones: self.dcs_to_shapely_point(p.position) for p in flight.points ))) + @classmethod + def closest_enemy_airbase(cls, location: ControlPoint, + max_distance: Distance) -> Optional[ControlPoint]: + airfields = ObjectiveDistanceCache.get_closest_airfields(location) + for airfield in airfields.airfields_within(max_distance): + if airfield.captured != location.captured: + return airfield + return None + + @classmethod + def barcap_threat_range(cls, game: Game, + control_point: ControlPoint) -> Distance: + doctrine = game.faction_for(control_point.captured).doctrine + cap_threat_range = (doctrine.cap_max_distance_from_cp + + doctrine.cap_engagement_range) + opposing_airfield = cls.closest_enemy_airbase(control_point, + cap_threat_range * 2) + if opposing_airfield is None: + return cap_threat_range + + airfield_distance = meters( + opposing_airfield.position.distance_to_point(control_point.position) + ) + + # BARCAPs should not commit further than halfway to the closest enemy + # airfield (with some breathing room) to avoid those missions becoming + # offensive. For dissimilar doctrines we could weight this so that, as + # an example, modern US goes no closer than 70% of the way to the WW2 + # German base, and the Germans go no closer than 30% of the way to the + # US base, but for now equal weighting is fine. + max_distance = airfield_distance * 0.45 + return min(cap_threat_range, max_distance) + @classmethod def for_faction(cls, game: Game, player: bool) -> ThreatZones: """Generates the threat zones projected by the given coalition. @@ -92,8 +127,6 @@ class ThreatZones: zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ - doctrine = game.faction_for(player).doctrine - airbases = [] air_defenses = [] for control_point in game.theater.controlpoints: @@ -102,8 +135,7 @@ class ThreatZones: if control_point.runway_is_operational(): point = ShapelyPoint(control_point.position.x, control_point.position.y) - cap_threat_range = (doctrine.cap_max_distance_from_cp + - doctrine.cap_engagement_range) + cap_threat_range = cls.barcap_threat_range(game, control_point) airbases.append(point.buffer(cap_threat_range.meters)) for tgo in control_point.ground_objects: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 4d5f563e..28dd4cbd 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -17,6 +17,7 @@ from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from dcs.mapping import Point from dcs.unit import Unit +from shapely.geometry import Point as ShapelyPoint from game.data.doctrine import Doctrine from game.theater import ( @@ -976,7 +977,7 @@ class FlightPlanBuilder: if isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - start, end = self.racetrack_for_objective(location) + start, end = self.racetrack_for_objective(location, barcap=True) patrol_alt = meters(random.randint( int(self.doctrine.min_patrol_altitude.meters), int(self.doctrine.max_patrol_altitude.meters) @@ -1037,8 +1038,8 @@ class FlightPlanBuilder: divert=builder.divert(flight.divert) ) - def racetrack_for_objective(self, - location: MissionTarget) -> Tuple[Point, Point]: + def racetrack_for_objective(self, location: MissionTarget, + barcap: bool) -> Tuple[Point, Point]: closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) for airfield in closest_cache.closest_airfields: # If the mission is a BARCAP of an enemy airfield, find the *next* @@ -1055,12 +1056,28 @@ class FlightPlanBuilder: closest_airfield.position ) - min_distance_from_enemy = nautical_miles(20) - distance_to_airfield = meters( - closest_airfield.position.distance_to_point( - self.package.target.position - )) - distance_to_no_fly = distance_to_airfield - min_distance_from_enemy + position = ShapelyPoint(self.package.target.position.x, + self.package.target.position.y) + + if barcap: + # BARCAPs should remain far enough back from the enemy that their + # commit range does not enter the enemy's threat zone. Include a 5nm + # buffer. + distance_to_no_fly = meters( + position.distance(self.threat_zones.all) + ) - self.doctrine.cap_engagement_range - nautical_miles(5) + else: + # Other race tracks (TARCAPs, currently) just try to keep some + # distance from the nearest enemy airbase, but since they are by + # definition in enemy territory they can't avoid the threat zone + # without being useless. + min_distance_from_enemy = nautical_miles(20) + distance_to_airfield = meters( + closest_airfield.position.distance_to_point( + self.package.target.position + )) + distance_to_no_fly = distance_to_airfield - min_distance_from_enemy + min_cap_distance = min(self.doctrine.cap_min_distance_from_cp, distance_to_no_fly) max_cap_distance = min(self.doctrine.cap_max_distance_from_cp, @@ -1125,7 +1142,8 @@ class FlightPlanBuilder: orbit0p, orbit1p = self.racetrack_for_frontline( flight.departure.position, location) else: - orbit0p, orbit1p = self.racetrack_for_objective(location) + orbit0p, orbit1p = self.racetrack_for_objective(location, + barcap=False) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) return TarCapFlightPlan(