Allow user to add navigation waypoints where possible

This commit is contained in:
Raffson 2023-08-27 20:54:12 +02:00
parent 819bd92d9a
commit 66d741d0b3
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
13 changed files with 153 additions and 67 deletions

View File

@ -15,6 +15,7 @@
* **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa * **[Mission Planning]** Avoid helicopters being assigned as escort to planes and vice-versa
* **[Mission Planning]** Allow attack helicopters to escort other helicopters * **[Mission Planning]** Allow attack helicopters to escort other helicopters
* **[UI]** Allow changing waypoint names in FlightEdit's waypoints tab * **[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 ## Fixes
* **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task * **[Mission Generation]** Anti-ship strikes should use "group attack" in their attack-task

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta 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.theater.missiontarget import MissionTarget
from game.utils import feet from game.utils import feet
@ -12,6 +12,7 @@ from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
from .standard import StandardFlightPlan, StandardLayout from .standard import StandardFlightPlan, StandardLayout
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...theater.interfaces.CTLD import CTLD from ...theater.interfaces.CTLD import CTLD
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,7 +22,6 @@ if TYPE_CHECKING:
@dataclass @dataclass
class AirliftLayout(StandardLayout): class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
# There will not be a pickup waypoint when the pickup airfield is the departure # 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 # airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
# up the cargo near the airfield. # up the cargo near the airfield.
@ -36,25 +36,30 @@ class AirliftLayout(StandardLayout):
drop_off: FlightWaypoint | None drop_off: FlightWaypoint | None
# drop_off_zone will be used for player flights to create the CTLD stuff # drop_off_zone will be used for player flights to create the CTLD stuff
ctld_drop_off_zone: FlightWaypoint | None 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: def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint): if waypoint in self.nav_to_drop_off:
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:
self.nav_to_drop_off.remove(waypoint) self.nav_to_drop_off.remove(waypoint)
return True return True
elif waypoint in self.nav_to_home: elif super().delete_waypoint(waypoint):
self.nav_to_home.remove(waypoint)
return True return True
return False return False
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.nav_to_pickup yield from self.nav_to
if self.pickup is not None: if self.pickup is not None:
yield self.pickup yield self.pickup
if self.ctld_pickup_zone is not None: if self.ctld_pickup_zone is not None:
@ -64,7 +69,7 @@ class AirliftLayout(StandardLayout):
yield self.drop_off yield self.drop_off
if self.ctld_drop_off_zone is not None: if self.ctld_drop_off_zone is not None:
yield self.ctld_drop_off_zone yield self.ctld_drop_off_zone
yield from self.nav_to_home yield from self.nav_from
yield self.arrival yield self.arrival
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
@ -141,7 +146,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
return AirliftLayout( return AirliftLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup, nav_to=nav_to_pickup,
pickup=pickup, pickup=pickup,
ctld_pickup_zone=pickup_zone, ctld_pickup_zone=pickup_zone,
nav_to_drop_off=builder.nav_path( nav_to_drop_off=builder.nav_path(
@ -152,7 +157,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
), ),
drop_off=drop_off, drop_off=drop_off,
ctld_drop_off_zone=drop_off_zone, ctld_drop_off_zone=drop_off_zone,
nav_to_home=builder.nav_path( nav_from=builder.nav_path(
cargo.origin.position, cargo.origin.position,
self.flight.arrival.position, self.flight.arrival.position,
altitude, altitude,

View File

@ -29,6 +29,10 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
def builder_type() -> Type[Builder]: def builder_type() -> Type[Builder]:
return Builder return Builder
@property
def is_custom(self) -> bool:
return True
@property @property
def tot_waypoint(self) -> FlightWaypoint: def tot_waypoint(self) -> FlightWaypoint:
target_types = ( target_types = (

View File

@ -17,19 +17,9 @@ if TYPE_CHECKING:
@dataclass @dataclass
class FerryLayout(StandardLayout): 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]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.nav_to_destination yield from self.nav_to
yield self.arrival yield self.arrival
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
@ -76,7 +66,7 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
builder = WaypointBuilder(self.flight, self.coalition) builder = WaypointBuilder(self.flight, self.coalition)
return FerryLayout( return FerryLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
nav_to_destination=builder.nav_path( nav_to=builder.nav_path(
self.flight.departure.position, self.flight.departure.position,
self.flight.arrival.position, self.flight.arrival.position,
altitude, altitude,
@ -85,6 +75,7 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
nav_from=[],
) )
def build(self) -> FerryFlightPlan: def build(self) -> FerryFlightPlan:

View File

@ -317,6 +317,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
def is_airassault(self) -> bool: def is_airassault(self) -> bool:
return False return False
@property
def is_custom(self) -> bool:
return False
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
"""The time that the mission is complete and the flight RTBs.""" """The time that the mission is complete and the flight RTBs."""

View File

@ -18,24 +18,16 @@ if TYPE_CHECKING:
@dataclass @dataclass
class FormationLayout(LoiterLayout, ABC): class FormationLayout(LoiterLayout, ABC):
nav_to: list[FlightWaypoint]
join: Optional[FlightWaypoint] join: Optional[FlightWaypoint]
split: FlightWaypoint split: FlightWaypoint
refuel: Optional[FlightWaypoint] refuel: Optional[FlightWaypoint]
nav_from: list[FlightWaypoint]
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool: def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if super().delete_waypoint(waypoint): if waypoint == self.refuel:
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:
self.refuel = None self.refuel = None
return True return True
elif super().delete_waypoint(waypoint):
return True
return False return False

View File

@ -18,21 +18,8 @@ if TYPE_CHECKING:
@dataclass @dataclass
class PatrollingLayout(StandardLayout): class PatrollingLayout(StandardLayout):
nav_to: list[FlightWaypoint]
patrol_start: FlightWaypoint patrol_start: FlightWaypoint
patrol_end: 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]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure

View File

@ -18,12 +18,11 @@ if TYPE_CHECKING:
@dataclass @dataclass
class RtbLayout(StandardLayout): class RtbLayout(StandardLayout):
abort_location: FlightWaypoint abort_location: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield self.abort_location yield self.abort_location
yield from self.nav_to_destination yield from self.nav_to
yield self.arrival yield self.arrival
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
@ -78,7 +77,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
return RtbLayout( return RtbLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
abort_location=abort_point, abort_location=abort_point,
nav_to_destination=builder.nav_path( nav_to=builder.nav_path(
current_position, current_position,
self.flight.arrival.position, self.flight.arrival.position,
altitude, altitude,
@ -87,6 +86,7 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
nav_from=[],
) )
def build(self) -> RtbFlightPlan: def build(self) -> RtbFlightPlan:

View File

@ -1,10 +1,14 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC from abc import ABC
from copy import deepcopy
from dataclasses import dataclass 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 game.ato.flightplans.flightplan import FlightPlan, Layout
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
from ...utils import feet
if TYPE_CHECKING: if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@ -15,11 +19,57 @@ class StandardLayout(Layout, ABC):
arrival: FlightWaypoint arrival: FlightWaypoint
divert: FlightWaypoint | None divert: FlightWaypoint | None
bullseye: FlightWaypoint 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: def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
if waypoint is self.divert: if waypoint is self.divert:
self.divert = None self.divert = None
return True 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 return False

View File

@ -32,6 +32,14 @@ class TarCapLayout(PatrollingLayout):
yield self.divert yield self.divert
yield self.bullseye 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]): class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@property @property

View File

@ -1,4 +1,4 @@
from PySide2.QtGui import QStandardItem from PySide2.QtGui import QStandardItem, Qt
from game.ato.flightwaypoint import FlightWaypoint from game.ato.flightwaypoint import FlightWaypoint
@ -6,6 +6,7 @@ from game.ato.flightwaypoint import FlightWaypoint
class QWaypointItem(QStandardItem): class QWaypointItem(QStandardItem):
def __init__(self, point: FlightWaypoint, number): def __init__(self, point: FlightWaypoint, number):
super(QWaypointItem, self).__init__() super(QWaypointItem, self).__init__()
self.setData(point, Qt.UserRole)
self.number = number self.number = number
self.setText("{:<16}".format(point.pretty_name)) self.setText("{:<16}".format(point.pretty_name))
self.setEditable(True) self.setEditable(True)

View File

@ -78,6 +78,7 @@ class QFlightWaypointList(QTableView):
finally: finally:
# stop ignoring signals # stop ignoring signals
self.model.blockSignals(False) self.model.blockSignals(False)
self.update()
def _add_waypoint_row( def _add_waypoint_row(
self, row: int, flight: Flight, waypoint: FlightWaypoint self, row: int, flight: Flight, waypoint: FlightWaypoint

View File

@ -1,7 +1,7 @@
import logging import logging
from typing import Iterable, List, Optional from typing import Iterable, List, Optional
from PySide2.QtCore import Signal from PySide2.QtCore import Signal, Qt, QModelIndex
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QFrame, QFrame,
QGridLayout, QGridLayout,
@ -9,6 +9,7 @@ from PySide2.QtWidgets import (
QMessageBox, QMessageBox,
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
QWidget,
) )
from game import Game from game import Game
@ -42,6 +43,7 @@ class QFlightWaypointTab(QFrame):
self.flight_waypoint_list: Optional[QFlightWaypointList] = None self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None self.rtb_waypoint: Optional[QPushButton] = None
self.delete_selected: 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.open_fast_waypoint_button: Optional[QPushButton] = None
self.recreate_buttons: List[QPushButton] = [] self.recreate_buttons: List[QPushButton] = []
self.init_ui() self.init_ui()
@ -77,6 +79,10 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(button) rlayout.addWidget(button)
self.recreate_buttons.append(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("<strong>Advanced : </strong>"))
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>")) rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
@ -94,6 +100,31 @@ class QFlightWaypointTab(QFrame):
rlayout.addStretch() rlayout.addStretch()
self.setLayout(layout) 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): def on_delete_waypoint(self):
waypoints = [] waypoints = []
selection = self.flight_waypoint_list.selectionModel() selection = self.flight_waypoint_list.selectionModel()
@ -102,7 +133,6 @@ class QFlightWaypointTab(QFrame):
waypoints.append(self.flight.flight_plan.waypoints[selected_row.row()]) waypoints.append(self.flight.flight_plan.waypoints[selected_row.row()])
for waypoint in waypoints: for waypoint in waypoints:
self.delete_waypoint(waypoint) self.delete_waypoint(waypoint)
self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def delete_waypoint(self, waypoint: FlightWaypoint) -> None: def delete_waypoint(self, waypoint: FlightWaypoint) -> None:
@ -116,10 +146,21 @@ class QFlightWaypointTab(QFrame):
if is_target and count > 1: if is_target and count > 1:
fp.target_area_waypoint.targets.remove(waypoint) fp.target_area_waypoint.targets.remove(waypoint)
return return
model = self.flight_waypoint_list.model
if fp.layout.delete_waypoint(waypoint): if fp.layout.delete_waypoint(waypoint):
model.removeRow(model.rowCount() - 1)
return 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( result = QMessageBox.warning(
self, self,
"Degrade flight-plan?", "Degrade flight-plan?",
@ -129,11 +170,7 @@ class QFlightWaypointTab(QFrame):
QMessageBox.Yes, QMessageBox.Yes,
QMessageBox.No, QMessageBox.No,
) )
if result == QMessageBox.No: return result == QMessageBox.Yes
return
self.degrade_to_custom_flight_plan()
assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)
def on_fast_waypoint(self): def on_fast_waypoint(self):
self.subwindow = QPredefinedWaypointSelectionWindow( self.subwindow = QPredefinedWaypointSelectionWindow(
@ -145,10 +182,15 @@ class QFlightWaypointTab(QFrame):
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None: def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
if not waypoints: if not waypoints:
return return
if not self.flight.flight_plan.is_custom:
confirmed = self.confirm_degrade()
if not confirmed:
return
self.degrade_to_custom_flight_plan() self.degrade_to_custom_flight_plan()
assert isinstance(self.flight.flight_plan, CustomFlightPlan) assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.extend(waypoints) 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() self.on_change()
def on_rtb_waypoint(self): def on_rtb_waypoint(self):
@ -156,7 +198,6 @@ class QFlightWaypointTab(QFrame):
self.degrade_to_custom_flight_plan() self.degrade_to_custom_flight_plan()
assert isinstance(self.flight.flight_plan, CustomFlightPlan) assert isinstance(self.flight.flight_plan, CustomFlightPlan)
self.flight.flight_plan.layout.custom_waypoints.append(rtb) self.flight.flight_plan.layout.custom_waypoints.append(rtb)
self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def degrade_to_custom_flight_plan(self) -> None: def degrade_to_custom_flight_plan(self) -> None:
@ -188,8 +229,9 @@ class QFlightWaypointTab(QFrame):
if not self.flight.loadout.is_custom: if not self.flight.loadout.is_custom:
self.flight.loadout = Loadout.default_for(self.flight) self.flight.loadout = Loadout.default_for(self.flight)
self.loadout_changed.emit() self.loadout_changed.emit()
self.flight_waypoint_list.update_list()
self.on_change() self.on_change()
def on_change(self): def on_change(self):
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()
self.flight_waypoint_list.on_changed()
self.update()