Redo convoy attack flight plans.

The previous flight plan only makes sense if the convoy will make it a
significant distance from its starting point. At road speeds over the
typical mission duration this is not true, so we can actually plan this
as if it was a strike mission near the origin point and that's close
enough.

There's some cleanup work to do here that I've added todos for.

Fixes https://github.com/Khopa/dcs_liberation/issues/996
This commit is contained in:
Dan Albert
2021-04-20 21:06:58 -07:00
parent 3f16c0378a
commit 50d8e08a34
12 changed files with 216 additions and 301 deletions

View File

@@ -72,7 +72,7 @@ from dcs.task import (
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import FlyingType, UnitType
from game import db
@@ -88,6 +88,7 @@ from game.theater.controlpoint import (
OffMapSpawn,
)
from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import Convoy, RoadTransferOrder
from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles
from gen.ato import AirTaskingOrder, Package
@@ -1691,25 +1692,30 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
# TODO: Add common "UnitGroupTarget" base type.
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
logging.error(
"Could not find group for BAI mission %s", target_group.group_name
)
group_name = target_group.group_name
elif isinstance(target_group, Convoy):
group_name = target_group.transfer.name
else:
logging.error(
"Unexpected target type for BAI mission: %s",
target_group.__class__.__name__,
)
return waypoint
group = self.mission.find_group(group_name)
if group is None:
logging.error("Could not find group for BAI mission %s", group_name)
return waypoint
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint

View File

@@ -12,6 +12,7 @@ from dcs.unittype import VehicleType
from game.transfers import RoadTransferOrder
from game.unitmap import UnitMap
from game.utils import kph
if TYPE_CHECKING:
from game import Game
@@ -26,24 +27,26 @@ class ConvoyGenerator:
def generate(self) -> None:
# Reset the count to make generation deterministic.
self.count = itertools.count()
for transfer in self.game.transfers:
self.generate_convoy_for(transfer)
def generate_convoy_for(self, transfer: RoadTransferOrder) -> None:
def generate_convoy_for(self, transfer: RoadTransferOrder) -> VehicleGroup:
next_hop = transfer.path()[0]
origin = transfer.position.convoy_spawns[next_hop]
destination = next_hop.convoy_spawns[transfer.position]
group = self._create_mixed_unit_group(
f"Convoy {next(self.count)}",
transfer.name,
origin,
transfer.units,
transfer.player,
)
group.add_waypoint(destination, move_formation=PointAction.OnRoad)
group.add_waypoint(
destination, speed=kph(40).kph, move_formation=PointAction.OnRoad
)
self.make_drivable(group)
self.unit_map.add_convoy_units(group, transfer)
return group
def _create_mixed_unit_group(
self,

View File

@@ -30,8 +30,8 @@ from game.theater import (
SamGroundObject,
TheaterGroundObject,
)
from game.theater.supplyroutes import SupplyRouteLink
from game.theater.theatergroundobject import EwrGroundObject
from game.transfers import Convoy
from game.utils import Distance, Speed, feet, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@@ -467,25 +467,6 @@ class CasFlightPlan(PatrollingFlightPlan):
return self.patrol_end
@dataclass(frozen=True)
class ConvoyInterdictionFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield from [
self.patrol_start,
self.patrol_end,
]
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
@dataclass(frozen=True)
class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
@@ -1042,75 +1023,22 @@ class FlightPlanBuilder:
"""
location = self.package.target
if isinstance(location, SupplyRouteLink):
return self.generate_supply_route_bai(flight, location)
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = []
for group in location.groups:
if group.units:
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
if isinstance(location, TheaterGroundObject):
for group in location.groups:
if group.units:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group)
)
elif isinstance(location, Convoy):
targets.append(StrikeTarget(location.name, location))
else:
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets
)
def generate_supply_route_bai(
self, flight: Flight, location: SupplyRouteLink
) -> ConvoyInterdictionFlightPlan:
"""Generates a BAI flight plan for attacking a supply route.
These flight plans are extremely rough because we do not know where the roads
are. For now they're mostly only usable by players. The flight plan includes a
start and end patrol point matching the end points of the convoy's route and a
30 minute time on station. It is up to the player to find the target.
Args:
flight: The flight to generate the flight plan for.
location: The supply route link to attack.
"""
origin = self.package_airfield()
a_dist = origin.distance_to(location.control_point_a)
b_dist = origin.distance_to(location.control_point_b)
if a_dist < b_dist:
near = location.control_point_a
far = location.control_point_b
else:
near = location.control_point_b
far = location.control_point_a
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.convoy_search(near, far, patrol_alt)
return ConvoyInterdictionFlightPlan(
self.package,
flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, near.position, patrol_alt
),
nav_from=builder.nav_path(
far.position, flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
# Not relevant because player only.
engagement_distance=meters(0),
patrol_duration=timedelta(minutes=30),
)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.

View File

@@ -16,6 +16,8 @@ from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup
from game.transfers import Convoy
if TYPE_CHECKING:
from game import Game
@@ -32,7 +34,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group]
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, Convoy]
class WaypointBuilder:
@@ -349,63 +351,6 @@ class WaypointBuilder:
self.race_track_end(end, altitude),
)
@staticmethod
def convoy_search_start(
control_point: ControlPoint, altitude: Distance
) -> FlightWaypoint:
"""Creates a convoy search start waypoint.
Args:
control_point: Control point for the beginning of the search.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_BAI,
control_point.position.x,
control_point.position.y,
altitude,
)
waypoint.name = control_point.name
waypoint.description = "Beginning of convoy search area"
waypoint.pretty_name = "Search start"
return waypoint
@staticmethod
def convoy_search_end(
control_point: ControlPoint, altitude: Distance
) -> FlightWaypoint:
"""Creates a convoy search start waypoint.
Args:
control_point: Control point for the beginning of the search.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
control_point.position.x,
control_point.position.y,
altitude,
)
waypoint.name = control_point.name
waypoint.description = "End of convoy search area"
waypoint.pretty_name = "Search end"
return waypoint
def convoy_search(
self, start: ControlPoint, end: ControlPoint, altitude: Distance
) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a convoy search path.
Args:
start: The beginning convoy search waypoint.
end: The ending convoy search waypoint.
altitude: The convoy search altitude.
"""
return (
self.convoy_search_start(start, altitude),
self.convoy_search_end(end, altitude),
)
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point.
@@ -463,7 +408,7 @@ class WaypointBuilder:
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
return self.sweep_start(start, altitude), self.sweep_end(end, altitude)
def escort(
self, ingress: Point, target: MissionTarget, egress: Point

View File

@@ -250,6 +250,7 @@ class NameGenerator:
number = 0
infantry_number = 0
aircraft_number = 0
convoy_number = 0
ANIMALS = ANIMALS
existing_alphas: List[str] = []
@@ -258,6 +259,7 @@ class NameGenerator:
def reset(cls):
cls.number = 0
cls.infantry_number = 0
cls.convoy_number = 0
cls.ANIMALS = ANIMALS
cls.existing_alphas = []
@@ -266,6 +268,7 @@ class NameGenerator:
cls.number = 0
cls.infantry_number = 0
cls.aircraft_number = 0
cls.convoy_number = 0
@classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
@@ -327,6 +330,11 @@ class NameGenerator:
cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number)
@classmethod
def next_convoy_name(cls) -> str:
cls.convoy_number += 1
return f"Convoy {cls.convoy_number:04}"
@classmethod
def random_objective_name(cls):
if len(cls.ANIMALS) == 0: