mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Support for convoy "spawn-route" in campaign design
Resolves #91 'Assault On Damascus' was revamped using this new technique
This commit is contained in:
parent
057ea58d8e
commit
45b8757593
@ -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).
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user