mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Allow user to add navigation waypoints where possible
This commit is contained in:
parent
819bd92d9a
commit
66d741d0b3
@ -15,6 +15,7 @@
|
||||
* **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa
|
||||
* **[Mission Planning]** Allow attack helicopters to escort other helicopters
|
||||
* **[UI]** Allow changing waypoint names in FlightEdit's waypoints tab
|
||||
* **[Waypoints]** Allow user to add navigation waypoints where possible without degrading to a custom flight-plan
|
||||
|
||||
## Fixes
|
||||
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task
|
||||
|
||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from typing import TYPE_CHECKING, Type, Optional
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
@ -12,6 +12,7 @@ from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .standard import StandardFlightPlan, StandardLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -21,7 +22,6 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class AirliftLayout(StandardLayout):
|
||||
nav_to_pickup: list[FlightWaypoint]
|
||||
# There will not be a pickup waypoint when the pickup airfield is the departure
|
||||
# airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
|
||||
# up the cargo near the airfield.
|
||||
@ -36,25 +36,30 @@ class AirliftLayout(StandardLayout):
|
||||
drop_off: FlightWaypoint | None
|
||||
# drop_off_zone will be used for player flights to create the CTLD stuff
|
||||
ctld_drop_off_zone: FlightWaypoint | None
|
||||
nav_to_home: list[FlightWaypoint]
|
||||
|
||||
def add_waypoint(
|
||||
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> bool:
|
||||
new_wpt = self.get_midpoint(wpt, next_wpt)
|
||||
if wpt.waypoint_type in [
|
||||
FlightWaypointType.PICKUP_ZONE,
|
||||
FlightWaypointType.CARGO_STOP,
|
||||
]:
|
||||
self.nav_to_drop_off.insert(0, new_wpt)
|
||||
return True
|
||||
return super().add_waypoint(wpt, next_wpt)
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if super().delete_waypoint(waypoint):
|
||||
return True
|
||||
if waypoint in self.nav_to_pickup:
|
||||
self.nav_to_pickup.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_to_drop_off:
|
||||
if waypoint in self.nav_to_drop_off:
|
||||
self.nav_to_drop_off.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_to_home:
|
||||
self.nav_to_home.remove(waypoint)
|
||||
elif super().delete_waypoint(waypoint):
|
||||
return True
|
||||
return False
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_pickup
|
||||
yield from self.nav_to
|
||||
if self.pickup is not None:
|
||||
yield self.pickup
|
||||
if self.ctld_pickup_zone is not None:
|
||||
@ -64,7 +69,7 @@ class AirliftLayout(StandardLayout):
|
||||
yield self.drop_off
|
||||
if self.ctld_drop_off_zone is not None:
|
||||
yield self.ctld_drop_off_zone
|
||||
yield from self.nav_to_home
|
||||
yield from self.nav_from
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
@ -141,7 +146,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
|
||||
return AirliftLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_pickup=nav_to_pickup,
|
||||
nav_to=nav_to_pickup,
|
||||
pickup=pickup,
|
||||
ctld_pickup_zone=pickup_zone,
|
||||
nav_to_drop_off=builder.nav_path(
|
||||
@ -152,7 +157,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
),
|
||||
drop_off=drop_off,
|
||||
ctld_drop_off_zone=drop_off_zone,
|
||||
nav_to_home=builder.nav_path(
|
||||
nav_from=builder.nav_path(
|
||||
cargo.origin.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
|
||||
@ -29,6 +29,10 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def is_custom(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
target_types = (
|
||||
|
||||
@ -17,19 +17,9 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class FerryLayout(StandardLayout):
|
||||
nav_to_destination: list[FlightWaypoint]
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if super().delete_waypoint(waypoint):
|
||||
return True
|
||||
if waypoint in self.nav_to_destination:
|
||||
self.nav_to_destination.remove(waypoint)
|
||||
return True
|
||||
return False
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to_destination
|
||||
yield from self.nav_to
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
@ -76,7 +66,7 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
return FerryLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to_destination=builder.nav_path(
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
@ -85,6 +75,7 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
)
|
||||
|
||||
def build(self) -> FerryFlightPlan:
|
||||
|
||||
@ -317,6 +317,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def is_airassault(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_custom(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
|
||||
@ -18,24 +18,16 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class FormationLayout(LoiterLayout, ABC):
|
||||
nav_to: list[FlightWaypoint]
|
||||
join: Optional[FlightWaypoint]
|
||||
split: FlightWaypoint
|
||||
refuel: Optional[FlightWaypoint]
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if super().delete_waypoint(waypoint):
|
||||
return True
|
||||
if waypoint in self.nav_to:
|
||||
self.nav_to.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_from:
|
||||
self.nav_from.remove(waypoint)
|
||||
return True
|
||||
elif waypoint == self.refuel:
|
||||
if waypoint == self.refuel:
|
||||
self.refuel = None
|
||||
return True
|
||||
elif super().delete_waypoint(waypoint):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@ -18,21 +18,8 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class PatrollingLayout(StandardLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
patrol_start: FlightWaypoint
|
||||
patrol_end: FlightWaypoint
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if super().delete_waypoint(waypoint):
|
||||
return True
|
||||
if waypoint in self.nav_to:
|
||||
self.nav_to.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_from:
|
||||
self.nav_from.remove(waypoint)
|
||||
return True
|
||||
return False
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
|
||||
@ -18,12 +18,11 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class RtbLayout(StandardLayout):
|
||||
abort_location: FlightWaypoint
|
||||
nav_to_destination: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield self.abort_location
|
||||
yield from self.nav_to_destination
|
||||
yield from self.nav_to
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
@ -78,7 +77,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
return RtbLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
abort_location=abort_point,
|
||||
nav_to_destination=builder.nav_path(
|
||||
nav_to=builder.nav_path(
|
||||
current_position,
|
||||
self.flight.arrival.position,
|
||||
altitude,
|
||||
@ -87,6 +86,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
nav_from=[],
|
||||
)
|
||||
|
||||
def build(self) -> RtbFlightPlan:
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
|
||||
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ...utils import feet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
@ -15,11 +19,57 @@ class StandardLayout(Layout, ABC):
|
||||
arrival: FlightWaypoint
|
||||
divert: FlightWaypoint | None
|
||||
bullseye: FlightWaypoint
|
||||
nav_to: list[FlightWaypoint]
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def add_waypoint(
|
||||
self, wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> bool:
|
||||
new_wpt = self.get_midpoint(wpt, next_wpt)
|
||||
if wpt.waypoint_type in [FlightWaypointType.TAKEOFF, FlightWaypointType.LOITER]:
|
||||
self.nav_to.insert(0, new_wpt)
|
||||
return True
|
||||
elif wpt.waypoint_type in [
|
||||
FlightWaypointType.SPLIT,
|
||||
FlightWaypointType.REFUEL,
|
||||
FlightWaypointType.PATROL,
|
||||
FlightWaypointType.EGRESS,
|
||||
]:
|
||||
self.nav_from.insert(0, new_wpt)
|
||||
return True
|
||||
elif wpt.waypoint_type is FlightWaypointType.NAV:
|
||||
if wpt in self.nav_to:
|
||||
index = self.nav_to.index(wpt) + 1
|
||||
self.nav_to.insert(index, new_wpt)
|
||||
return True
|
||||
elif wpt in self.nav_from:
|
||||
index = self.nav_from.index(wpt) + 1
|
||||
self.nav_from.insert(index, new_wpt)
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_midpoint(
|
||||
wpt: FlightWaypoint, next_wpt: Optional[FlightWaypoint]
|
||||
) -> FlightWaypoint:
|
||||
new_pos = deepcopy(wpt.position)
|
||||
next_alt = feet(20000)
|
||||
if next_wpt:
|
||||
new_pos = wpt.position.lerp(next_wpt.position, 0.5)
|
||||
next_alt = next_wpt.alt
|
||||
new_wpt = WaypointBuilder.nav(new_pos, max(wpt.alt, next_alt))
|
||||
return new_wpt
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint is self.divert:
|
||||
self.divert = None
|
||||
return True
|
||||
elif waypoint in self.nav_to:
|
||||
self.nav_to.remove(waypoint)
|
||||
return True
|
||||
elif waypoint in self.nav_from:
|
||||
self.nav_from.remove(waypoint)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@ -32,6 +32,14 @@ class TarCapLayout(PatrollingLayout):
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
|
||||
if waypoint == self.refuel:
|
||||
self.refuel = None
|
||||
return True
|
||||
elif super().delete_waypoint(waypoint):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
@property
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from PySide2.QtGui import QStandardItem
|
||||
from PySide2.QtGui import QStandardItem, Qt
|
||||
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
|
||||
@ -6,6 +6,7 @@ from game.ato.flightwaypoint import FlightWaypoint
|
||||
class QWaypointItem(QStandardItem):
|
||||
def __init__(self, point: FlightWaypoint, number):
|
||||
super(QWaypointItem, self).__init__()
|
||||
self.setData(point, Qt.UserRole)
|
||||
self.number = number
|
||||
self.setText("{:<16}".format(point.pretty_name))
|
||||
self.setEditable(True)
|
||||
|
||||
@ -78,6 +78,7 @@ class QFlightWaypointList(QTableView):
|
||||
finally:
|
||||
# stop ignoring signals
|
||||
self.model.blockSignals(False)
|
||||
self.update()
|
||||
|
||||
def _add_waypoint_row(
|
||||
self, row: int, flight: Flight, waypoint: FlightWaypoint
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtCore import Signal, Qt, QModelIndex
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
@ -9,6 +9,7 @@ from PySide2.QtWidgets import (
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
@ -42,6 +43,7 @@ class QFlightWaypointTab(QFrame):
|
||||
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
|
||||
self.rtb_waypoint: Optional[QPushButton] = None
|
||||
self.delete_selected: Optional[QPushButton] = None
|
||||
self.add_nav_waypoint: Optional[QPushButton] = None
|
||||
self.open_fast_waypoint_button: Optional[QPushButton] = None
|
||||
self.recreate_buttons: List[QPushButton] = []
|
||||
self.init_ui()
|
||||
@ -77,6 +79,10 @@ class QFlightWaypointTab(QFrame):
|
||||
rlayout.addWidget(button)
|
||||
self.recreate_buttons.append(button)
|
||||
|
||||
self.add_nav_waypoint = QPushButton("Insert NAV point")
|
||||
self.add_nav_waypoint.clicked.connect(self.on_add_nav)
|
||||
rlayout.addWidget(self.add_nav_waypoint)
|
||||
|
||||
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
|
||||
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
|
||||
|
||||
@ -94,6 +100,31 @@ class QFlightWaypointTab(QFrame):
|
||||
rlayout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def on_add_nav(self):
|
||||
selected = self.flight_waypoint_list.selectedIndexes()
|
||||
if not selected:
|
||||
return
|
||||
index: QModelIndex = selected[0]
|
||||
self.flight_waypoint_list.setCurrentIndex(index)
|
||||
wpt: FlightWaypoint = self.flight_waypoint_list.model.data(index, Qt.UserRole)
|
||||
next_wpt: Optional[FlightWaypoint] = None
|
||||
if index.row() + 1 < self.flight_waypoint_list.model.rowCount():
|
||||
next_wpt = self.flight_waypoint_list.model.data(
|
||||
index.siblingAtRow(index.row() + 1), Qt.UserRole
|
||||
)
|
||||
if not self.flight.flight_plan.layout.add_waypoint(wpt, next_wpt):
|
||||
QMessageBox.critical(
|
||||
QWidget(),
|
||||
"Failed to add NAV waypoint",
|
||||
"Could not insert a new waypoint given the currently selected waypoint.\n"
|
||||
"Please select a different waypoint to insert the new NAV waypoint.",
|
||||
)
|
||||
else:
|
||||
self.flight_waypoint_list.model.insertRow(
|
||||
self.flight_waypoint_list.model.rowCount()
|
||||
)
|
||||
self.on_change()
|
||||
|
||||
def on_delete_waypoint(self):
|
||||
waypoints = []
|
||||
selection = self.flight_waypoint_list.selectionModel()
|
||||
@ -102,7 +133,6 @@ class QFlightWaypointTab(QFrame):
|
||||
waypoints.append(self.flight.flight_plan.waypoints[selected_row.row()])
|
||||
for waypoint in waypoints:
|
||||
self.delete_waypoint(waypoint)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def delete_waypoint(self, waypoint: FlightWaypoint) -> None:
|
||||
@ -116,10 +146,21 @@ class QFlightWaypointTab(QFrame):
|
||||
if is_target and count > 1:
|
||||
fp.target_area_waypoint.targets.remove(waypoint)
|
||||
return
|
||||
|
||||
model = self.flight_waypoint_list.model
|
||||
if fp.layout.delete_waypoint(waypoint):
|
||||
model.removeRow(model.rowCount() - 1)
|
||||
return
|
||||
|
||||
if not self.flight.flight_plan.is_custom:
|
||||
confirmed = self.confirm_degrade()
|
||||
if not confirmed:
|
||||
return
|
||||
model.removeRow(model.rowCount() - 1)
|
||||
self.degrade_to_custom_flight_plan()
|
||||
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
|
||||
self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)
|
||||
|
||||
def confirm_degrade(self) -> bool:
|
||||
result = QMessageBox.warning(
|
||||
self,
|
||||
"Degrade flight-plan?",
|
||||
@ -129,11 +170,7 @@ class QFlightWaypointTab(QFrame):
|
||||
QMessageBox.Yes,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if result == QMessageBox.No:
|
||||
return
|
||||
self.degrade_to_custom_flight_plan()
|
||||
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
|
||||
self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)
|
||||
return result == QMessageBox.Yes
|
||||
|
||||
def on_fast_waypoint(self):
|
||||
self.subwindow = QPredefinedWaypointSelectionWindow(
|
||||
@ -145,10 +182,15 @@ class QFlightWaypointTab(QFrame):
|
||||
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
|
||||
if not waypoints:
|
||||
return
|
||||
if not self.flight.flight_plan.is_custom:
|
||||
confirmed = self.confirm_degrade()
|
||||
if not confirmed:
|
||||
return
|
||||
self.degrade_to_custom_flight_plan()
|
||||
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
|
||||
self.flight.flight_plan.layout.custom_waypoints.extend(waypoints)
|
||||
self.flight_waypoint_list.update_list()
|
||||
rc = self.flight_waypoint_list.model.rowCount()
|
||||
self.flight_waypoint_list.model.insertRows(rc, len(list(waypoints)))
|
||||
self.on_change()
|
||||
|
||||
def on_rtb_waypoint(self):
|
||||
@ -156,7 +198,6 @@ class QFlightWaypointTab(QFrame):
|
||||
self.degrade_to_custom_flight_plan()
|
||||
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
|
||||
self.flight.flight_plan.layout.custom_waypoints.append(rtb)
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def degrade_to_custom_flight_plan(self) -> None:
|
||||
@ -188,8 +229,9 @@ class QFlightWaypointTab(QFrame):
|
||||
if not self.flight.loadout.is_custom:
|
||||
self.flight.loadout = Loadout.default_for(self.flight)
|
||||
self.loadout_changed.emit()
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
def on_change(self):
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.flight_waypoint_list.on_changed()
|
||||
self.update()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user