diff --git a/changelog.md b/changelog.md
index 0f074013..4b7ffd0e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -16,6 +16,7 @@
* **[Modding]** Support for Iron Dome v1.2 by IDF Mods Project
* **[New Game Wizard]** Re-organized generator options & show the regular settings menu instead of the limited "Difficulty & Automation" page.
* **[Campaign Management]** Ability to operate harriers from FOBs/FARPs for __human pilots only__. Please note that the autoplanner won't generate flights for harriers at FOBs/FARPs, which means you need to plan your missions manually.
+* **[Mission Planning]** Allow NAV/REFUEL/DIVERT waypoints to be deleted without degrading to a custom flight-plan, also warning the user before actually degrading the flight-plan.
## Fixes
* **[New Game Wizard]** Settings would not persist when going back to a previous page (obsolete due to overhaul).
diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py
index f4d373de..2a85b698 100644
--- a/game/ato/flightplans/airassault.py
+++ b/game/ato/flightplans/airassault.py
@@ -4,11 +4,11 @@ from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
-from ._common_ctld import generate_random_ctld_point
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
+from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class AirAssaultLayout(StandardLayout):
# The pickup point is optional because we don't always need to load the cargo. When
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py
index 17516f3b..c4f8d53c 100644
--- a/game/ato/flightplans/airlift.py
+++ b/game/ato/flightplans/airlift.py
@@ -5,9 +5,9 @@ from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Type
-from ._common_ctld import generate_random_ctld_point
from game.theater.missiontarget import MissionTarget
from game.utils import feet
+from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .standard import StandardFlightPlan, StandardLayout
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
from dcs import Point
-@dataclass(frozen=True)
+@dataclass
class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
# There will not be a pickup waypoint when the pickup airfield is the departure
@@ -38,6 +38,20 @@ class AirliftLayout(StandardLayout):
ctld_drop_off_zone: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
+ 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:
+ self.nav_to_drop_off.remove(waypoint)
+ return True
+ elif waypoint in self.nav_to_home:
+ self.nav_to_home.remove(waypoint)
+ return True
+ return False
+
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py
index 1879612d..0423b1ca 100644
--- a/game/ato/flightplans/cas.py
+++ b/game/ato/flightplans/cas.py
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class CasLayout(PatrollingLayout):
target: FlightWaypoint
diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py
index 35caa8e0..c47e93c5 100644
--- a/game/ato/flightplans/custom.py
+++ b/game/ato/flightplans/custom.py
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint]
diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py
index 0ddd82ac..de55cced 100644
--- a/game/ato/flightplans/ferry.py
+++ b/game/ato/flightplans/ferry.py
@@ -15,10 +15,18 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@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
diff --git a/game/ato/flightplans/flightplan.py b/game/ato/flightplans/flightplan.py
index 99eb45ef..76e9c5c5 100644
--- a/game/ato/flightplans/flightplan.py
+++ b/game/ato/flightplans/flightplan.py
@@ -41,7 +41,7 @@ INGRESS_TYPES = {
}
-@dataclass(frozen=True)
+@dataclass
class Layout(ABC):
departure: FlightWaypoint
@@ -50,6 +50,9 @@ class Layout(ABC):
"""A list of all waypoints in the flight plan, in order."""
return list(self.iter_waypoints())
+ def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
+ return False
+
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError
diff --git a/game/ato/flightplans/formation.py b/game/ato/flightplans/formation.py
index 5aa39263..cf5bc1a8 100644
--- a/game/ato/flightplans/formation.py
+++ b/game/ato/flightplans/formation.py
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
-from typing import Any, TYPE_CHECKING, TypeGuard
+from typing import Any, TYPE_CHECKING, TypeGuard, Optional
from game.typeguard import self_type_guard
from game.utils import Speed
@@ -16,14 +16,28 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class FormationLayout(LoiterLayout, ABC):
nav_to: list[FlightWaypoint]
join: FlightWaypoint
split: FlightWaypoint
- refuel: 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:
+ self.refuel = None
+ return True
+ return False
+
class FormationFlightPlan(LoiterFlightPlan, ABC):
@property
diff --git a/game/ato/flightplans/formationattack.py b/game/ato/flightplans/formationattack.py
index 435c0c06..fc930088 100644
--- a/game/ato/flightplans/formationattack.py
+++ b/game/ato/flightplans/formationattack.py
@@ -10,7 +10,7 @@ from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
-from game.utils import Speed, meters, Distance
+from game.utils import Speed, meters
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
@@ -130,7 +130,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return super().tot_for_waypoint(waypoint)
-@dataclass(frozen=True)
+@dataclass
class FormationAttackLayout(FormationLayout):
ingress: FlightWaypoint
targets: list[FlightWaypoint]
diff --git a/game/ato/flightplans/loiter.py b/game/ato/flightplans/loiter.py
index c0e4ce56..c3545dcd 100644
--- a/game/ato/flightplans/loiter.py
+++ b/game/ato/flightplans/loiter.py
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class LoiterLayout(StandardLayout, ABC):
hold: FlightWaypoint
diff --git a/game/ato/flightplans/patrolling.py b/game/ato/flightplans/patrolling.py
index eecc1828..b4815188 100644
--- a/game/ato/flightplans/patrolling.py
+++ b/game/ato/flightplans/patrolling.py
@@ -16,13 +16,24 @@ if TYPE_CHECKING:
from .flightplan import FlightPlan
-@dataclass(frozen=True)
+@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
yield from self.nav_to
diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py
index 8f1cbfa8..b5fd5a11 100644
--- a/game/ato/flightplans/rtb.py
+++ b/game/ato/flightplans/rtb.py
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class RtbLayout(StandardLayout):
abort_location: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
diff --git a/game/ato/flightplans/standard.py b/game/ato/flightplans/standard.py
index 5cc45f95..89c5354c 100644
--- a/game/ato/flightplans/standard.py
+++ b/game/ato/flightplans/standard.py
@@ -10,12 +10,18 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class StandardLayout(Layout, ABC):
arrival: FlightWaypoint
divert: FlightWaypoint | None
bullseye: FlightWaypoint
+ def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
+ if waypoint is self.divert:
+ self.divert = None
+ return True
+ return False
+
LayoutT = TypeVar("LayoutT", bound=StandardLayout)
diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py
index caa26487..6e09b7da 100644
--- a/game/ato/flightplans/sweep.py
+++ b/game/ato/flightplans/sweep.py
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class SweepLayout(LoiterLayout):
nav_to: list[FlightWaypoint]
sweep_start: FlightWaypoint
diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py
index 560ef0d7..1a43ce81 100644
--- a/game/ato/flightplans/tarcap.py
+++ b/game/ato/flightplans/tarcap.py
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
-@dataclass(frozen=True)
+@dataclass
class TarCapLayout(PatrollingLayout):
refuel: FlightWaypoint | None
diff --git a/game/ato/flightplans/waypointbuilder.py b/game/ato/flightplans/waypointbuilder.py
index 93fc9fdd..f1ddd38a 100644
--- a/game/ato/flightplans/waypointbuilder.py
+++ b/game/ato/flightplans/waypointbuilder.py
@@ -437,7 +437,7 @@ class WaypointBuilder:
return FlightWaypoint(
"SEAD Search",
- FlightWaypointType.CUSTOM,
+ FlightWaypointType.INGRESS_SEAD,
hold,
self.doctrine.ingress_altitude,
alt_type="BARO",
diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
index bfae6686..f075ffc0 100644
--- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
+++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py
@@ -109,13 +109,28 @@ class QFlightWaypointTab(QFrame):
# Need to degrade to a custom flight plan and remove the waypoint.
# If the waypoint is a target waypoint and is not the last target
# waypoint, we don't need to degrade.
- if isinstance(self.flight.flight_plan, FormationAttackFlightPlan):
- is_target = waypoint in self.flight.flight_plan.target_area_waypoint.targets
- count = len(self.flight.flight_plan.target_area_waypoint.targets)
+ fp = self.flight.flight_plan
+ if isinstance(fp, FormationAttackFlightPlan):
+ is_target = waypoint in fp.target_area_waypoint.targets
+ count = len(fp.target_area_waypoint.targets)
if is_target and count > 1:
- self.flight.flight_plan.target_area_waypoint.targets.remove(waypoint)
+ fp.target_area_waypoint.targets.remove(waypoint)
return
+ if fp.layout.delete_waypoint(waypoint):
+ return
+
+ result = QMessageBox.warning(
+ self,
+ "Degrade flight-plan?",
+ "Deleting the selected waypoint(s) will require degradation to a custom flight-plan. "
+ "A custom flight-plan will no longer respect the TOTs of the package.
"
+ "Are you sure you wish to continue?",
+ 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)