Support for convoy "spawn-route" in campaign design

Resolves #91

'Assault On Damascus' was revamped using this new technique
This commit is contained in:
Raffson 2023-06-15 01:47:08 +02:00
parent 057ea58d8e
commit 45b8757593
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
7 changed files with 97 additions and 15 deletions

View File

@ -25,6 +25,7 @@
* **[Squadrons]** Warning messages when opening up a squadron through the air wing dialog, indicating squadrons that potentially won't fit w.r.t. parking space. * **[Squadrons]** Warning messages when opening up a squadron through the air wing dialog, indicating squadrons that potentially won't fit w.r.t. parking space.
* **[Squadrons Transfers]** Determine number of available parking slots more accurately w.r.t. squadron transfers, taking aircraft dimensions into account which should prevent forced air-starts. * **[Squadrons Transfers]** Determine number of available parking slots more accurately w.r.t. squadron transfers, taking aircraft dimensions into account which should prevent forced air-starts.
* **[UX]** Allow usage of CTRL/SHIFT modifiers in ground unit transfer window. * **[UX]** Allow usage of CTRL/SHIFT modifiers in ground unit transfer window.
* **[Campaign Design]** Ability to define "spawn-routes" for convoys, allowing them to start from the road without having to edit the mission
## Fixes ## Fixes
* **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul). * **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul).

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import itertools import itertools
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import Iterator, List, TYPE_CHECKING, Tuple from typing import Iterator, List, TYPE_CHECKING, Tuple, Optional
from uuid import UUID from uuid import UUID
from dcs import Mission from dcs import Mission
@ -28,7 +28,7 @@ from game.theater.controlpoint import (
OffMapSpawn, OffMapSpawn,
) )
from game.theater.presetlocation import PresetLocation from game.theater.presetlocation import PresetLocation
from game.utils import Distance, meters from game.utils import Distance, meters, feet
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater.conflicttheater import ConflictTheater from game.theater.conflicttheater import ConflictTheater
@ -45,6 +45,7 @@ class MizCampaignLoader:
LHA_UNIT_TYPE = LHA_Tarawa.id LHA_UNIT_TYPE = LHA_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.M_113.id FRONT_LINE_UNIT_TYPE = Armor.M_113.id
SHIPPING_LANE_UNIT_TYPE = HandyWind.id SHIPPING_LANE_UNIT_TYPE = HandyWind.id
CP_CONVOY_SPAWN_TYPE = Armor.M1043_HMMWV_Armament.id
FOB_UNIT_TYPE = Unarmed.SKP_11.id FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"] FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"]
@ -317,6 +318,50 @@ class MizCampaignLoader:
if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE: if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE:
yield group yield group
@property
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group:
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
yield group
def _construct_cp_spawnpoints(self, start: Point) -> Tuple[Point, ...]:
closest = self._find_closest_cp_spawn(start)
if closest:
return self._interpolate_points(closest, start)
return tuple()
@staticmethod
def _interpolate_points(closest: VehicleGroup, start: Point) -> Tuple[Point, ...]:
points = [start]
waypoints = points + [wpt.position for wpt in closest.points]
last = waypoints[0]
meters100ft = feet(100).meters
residual = 0.0
for wpt in waypoints[1:]:
dist = wpt.distance_to_point(last)
fraction = meters100ft / dist # 100ft separation
interpol_count = int((dist + residual) / meters100ft - 1)
offset = (meters100ft - residual) / dist
if offset <= 1:
points.append(last.lerp(wpt, offset))
if offset + fraction <= 1:
for i in range(1, interpol_count + 1):
points.append(last.lerp(wpt, i * fraction + offset))
residual = (residual + dist - meters100ft * interpol_count) % meters100ft
last = wpt
return tuple(points)
def _find_closest_cp_spawn(self, start: Point) -> Optional[VehicleGroup]:
closest: Optional[VehicleGroup] = None
for spawn in self.cp_convoy_spawns:
if closest is None:
closest = spawn
continue
dist = start.distance_to_point(closest.position)
if start.distance_to_point(spawn.position) < dist:
closest = spawn
return closest
def add_supply_routes(self) -> None: def add_supply_routes(self) -> None:
for group in self.front_line_path_groups: for group in self.front_line_path_groups:
# The unit will have its first waypoint at the source CP and the final # The unit will have its first waypoint at the source CP and the final
@ -334,9 +379,14 @@ class MizCampaignLoader:
f"No control point near the final waypoint of {group.name}" f"No control point near the final waypoint of {group.name}"
) )
self.control_points[origin.id].create_convoy_route(destination, waypoints) o_spawns = self._construct_cp_spawnpoints(waypoints[0])
d_spawns = self._construct_cp_spawnpoints(waypoints[-1])
self.control_points[origin.id].create_convoy_route(
destination, waypoints, o_spawns
)
self.control_points[destination.id].create_convoy_route( self.control_points[destination.id].create_convoy_route(
origin, list(reversed(waypoints)) origin, list(reversed(waypoints)), d_spawns
) )
def add_shipping_lanes(self) -> None: def add_shipping_lanes(self) -> None:

View File

@ -75,6 +75,7 @@ class Migrator:
try_set_attr(cp, "icls_channel") try_set_attr(cp, "icls_channel")
try_set_attr(cp, "icls_name") try_set_attr(cp, "icls_name")
try_set_attr(cp, "link4") try_set_attr(cp, "link4")
try_set_attr(cp, "convoy_spawns", {})
def _update_flights(self) -> None: def _update_flights(self) -> None:
for f in self.game.db.flights.objects.values(): for f in self.game.db.flights.objects.values():

View File

@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import TYPE_CHECKING import logging
from typing import TYPE_CHECKING, List
from dcs import Mission from dcs import Mission
from dcs.mapping import Point from dcs.mapping import Point
@ -40,26 +41,49 @@ class ConvoyGenerator:
convoy.units, convoy.units,
convoy.player_owned, convoy.player_owned,
) )
group.manualHeading = True
spawns_tuple = convoy.origin.convoy_spawns.get(convoy.destination)
if spawns_tuple:
spawns = list(spawns_tuple)
last_unit = group.units[0]
spawns.pop(0)
for u in group.units[1:]:
if spawns:
u.position = spawns.pop(0)
u.heading = u.position.heading_between_point(last_unit.position)
if last_unit.heading == 0:
last_unit.heading = u.heading
last_unit = u
else:
logging.warning(
f"Insufficient convoy spawns at {convoy.origin.name} "
f"with destination {convoy.destination.name}, "
"convoy may experience issues at mission start"
)
break
wpts: List[Point] = []
route = convoy.origin.convoy_route_to(convoy.destination)
if self.game.settings.convoys_travel_full_distance: if self.game.settings.convoys_travel_full_distance:
end_point = convoy.route_end wpts.extend(route)
else: else:
# convoys_travel_full_distance is disabled, so have the convoy only move the # convoys_travel_full_distance is disabled, so have the convoy only move the
# first segment on the route. This option aims to remove long routes for # first segment on the route. This option aims to remove long routes for
# ground vehicles between control points, since the CPU load for pathfinding # ground vehicles between control points, since the CPU load for pathfinding
# long routes on DCS can be pretty heavy. # long routes on DCS can be pretty heavy.
route = convoy.origin.convoy_route_to(convoy.destination)
# Select the first route segment from the origin towards the destination so # Select the first route segment from the origin towards the destination so
# the convoy spawns at the origin CP. This allows the convoy to be targeted # the convoy spawns at the origin CP. This allows the convoy to be targeted
# by BAI flights and starts it within the protection umbrella of the CP. # by BAI flights and starts it within the protection umbrella of the CP.
end_point = route[1] wpts.append(route[1])
group.add_waypoint( for wpt in wpts:
end_point, group.add_waypoint(
speed=kph(40).kph, wpt,
move_formation=PointAction.OnRoad, speed=kph(40).kph,
) move_formation=PointAction.OnRoad,
)
self.make_drivable(group) self.make_drivable(group)
self.unit_map.add_convoy_units(group, convoy) self.unit_map.add_convoy_units(group, convoy)

View File

@ -347,6 +347,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.connected_points: List[ControlPoint] = [] self.connected_points: List[ControlPoint] = []
self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.convoy_spawns: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.base: Base = Base() self.base: Base = Base()
self.cptype = cptype self.cptype = cptype
@ -604,10 +605,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def convoy_route_to(self, destination: ControlPoint) -> Sequence[Point]: def convoy_route_to(self, destination: ControlPoint) -> Sequence[Point]:
return self.convoy_routes[destination] return self.convoy_routes[destination]
def create_convoy_route(self, to: ControlPoint, waypoints: Iterable[Point]) -> None: def create_convoy_route(
self, to: ControlPoint, waypoints: Iterable[Point], spawns: Iterable[Point]
) -> None:
self.connected_points.append(to) self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE self.stances[to.id] = CombatStance.DEFENSIVE
self.convoy_routes[to] = tuple(waypoints) self.convoy_routes[to] = tuple(waypoints)
self.convoy_spawns[to] = tuple(spawns)
def create_shipping_lane( def create_shipping_lane(
self, to: ControlPoint, waypoints: Iterable[Point] self, to: ControlPoint, waypoints: Iterable[Point]

View File

@ -182,5 +182,7 @@ VERSION = _build_version_string()
#: #:
#: Version 10.7 #: Version 10.7
#: * Support for defining squadron sizes. #: * Support for defining squadron sizes.
#: * Definition of "spawn-routes" allowing convoys to spawn on the road
#: please note that an insufficiently long route can cause trouble in case of large convoys
CAMPAIGN_FORMAT_VERSION = (10, 7) CAMPAIGN_FORMAT_VERSION = (10, 7)