diff --git a/changelog.md b/changelog.md index fdaaf124..5265c6b4 100644 --- a/changelog.md +++ b/changelog.md @@ -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 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. +* **[Campaign Design]** Ability to define "spawn-routes" for convoys, allowing them to start from the road without having to edit the mission ## Fixes * **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul). diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index f8baebeb..ca660f64 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -3,7 +3,7 @@ from __future__ import annotations import itertools from functools import cached_property 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 dcs import Mission @@ -28,7 +28,7 @@ from game.theater.controlpoint import ( OffMapSpawn, ) from game.theater.presetlocation import PresetLocation -from game.utils import Distance, meters +from game.utils import Distance, meters, feet if TYPE_CHECKING: from game.theater.conflicttheater import ConflictTheater @@ -45,6 +45,7 @@ class MizCampaignLoader: LHA_UNIT_TYPE = LHA_Tarawa.id FRONT_LINE_UNIT_TYPE = Armor.M_113.id SHIPPING_LANE_UNIT_TYPE = HandyWind.id + CP_CONVOY_SPAWN_TYPE = Armor.M1043_HMMWV_Armament.id FOB_UNIT_TYPE = Unarmed.SKP_11.id FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"] @@ -317,6 +318,50 @@ class MizCampaignLoader: if group.units[0].type in self.POWER_SOURCE_UNIT_TYPE: 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: for group in self.front_line_path_groups: # 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}" ) - 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( - origin, list(reversed(waypoints)) + origin, list(reversed(waypoints)), d_spawns ) def add_shipping_lanes(self) -> None: diff --git a/game/migrator.py b/game/migrator.py index 4853aff2..b941d0ff 100644 --- a/game/migrator.py +++ b/game/migrator.py @@ -75,6 +75,7 @@ class Migrator: try_set_attr(cp, "icls_channel") try_set_attr(cp, "icls_name") try_set_attr(cp, "link4") + try_set_attr(cp, "convoy_spawns", {}) def _update_flights(self) -> None: for f in self.game.db.flights.objects.values(): diff --git a/game/missiongenerator/convoygenerator.py b/game/missiongenerator/convoygenerator.py index 1c40a85a..e1bad3ce 100644 --- a/game/missiongenerator/convoygenerator.py +++ b/game/missiongenerator/convoygenerator.py @@ -1,7 +1,8 @@ from __future__ import annotations import itertools -from typing import TYPE_CHECKING +import logging +from typing import TYPE_CHECKING, List from dcs import Mission from dcs.mapping import Point @@ -40,26 +41,49 @@ class ConvoyGenerator: convoy.units, 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: - end_point = convoy.route_end + wpts.extend(route) else: # 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 # ground vehicles between control points, since the CPU load for pathfinding # 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 # 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. - end_point = route[1] + wpts.append(route[1]) - group.add_waypoint( - end_point, - speed=kph(40).kph, - move_formation=PointAction.OnRoad, - ) + for wpt in wpts: + group.add_waypoint( + wpt, + speed=kph(40).kph, + move_formation=PointAction.OnRoad, + ) self.make_drivable(group) self.unit_map.add_convoy_units(group, convoy) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index dc442602..54321302 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -347,6 +347,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): # TODO: Should be Airbase specific. self.connected_points: List[ControlPoint] = [] self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {} + self.convoy_spawns: Dict[ControlPoint, Tuple[Point, ...]] = {} self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.base: Base = Base() self.cptype = cptype @@ -604,10 +605,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): def convoy_route_to(self, destination: ControlPoint) -> Sequence[Point]: 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.stances[to.id] = CombatStance.DEFENSIVE self.convoy_routes[to] = tuple(waypoints) + self.convoy_spawns[to] = tuple(spawns) def create_shipping_lane( self, to: ControlPoint, waypoints: Iterable[Point] diff --git a/game/version.py b/game/version.py index ee28fb0b..e168d9fd 100644 --- a/game/version.py +++ b/game/version.py @@ -182,5 +182,7 @@ VERSION = _build_version_string() #: #: Version 10.7 #: * 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) diff --git a/resources/campaigns/WRL_AssaultonDamascus.miz b/resources/campaigns/WRL_AssaultonDamascus.miz index dfb8f1ef..93d82c6c 100644 Binary files a/resources/campaigns/WRL_AssaultonDamascus.miz and b/resources/campaigns/WRL_AssaultonDamascus.miz differ