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]** 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

View File

@ -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,

View File

@ -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 = (

View File

@ -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:

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()