Allow deletion of certain types of waypoints

Resolves #60
NAV/REFUEL/DIVERT waypoints should have no effect on the timings.
This commit is contained in:
Raffson 2023-05-29 00:11:27 +02:00
parent 8363a7e8fa
commit 198ff7d8a3
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
17 changed files with 96 additions and 24 deletions

View File

@ -16,6 +16,7 @@
* **[Modding]** Support for Iron Dome v1.2 by IDF Mods Project * **[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. * **[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 <ins>__human pilots only__</ins>. Please note that the autoplanner won't generate flights for harriers at FOBs/FARPs, which means you need to plan your missions manually. * **[Campaign Management]** Ability to operate harriers from FOBs/FARPs for <ins>__human pilots only__</ins>. 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 ## 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).

View File

@ -4,11 +4,11 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type 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.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.theater.controlpoint import ControlPointType from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters from game.utils import Distance, feet, meters
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay from .uizonedisplay import UiZone, UiZoneDisplay
@ -21,7 +21,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class AirAssaultLayout(StandardLayout): class AirAssaultLayout(StandardLayout):
# The pickup point is optional because we don't always need to load the cargo. When # 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. # departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.

View File

@ -5,9 +5,9 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from ._common_ctld import generate_random_ctld_point
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.utils import feet from game.utils import feet
from ._common_ctld import generate_random_ctld_point
from .ibuilder import IBuilder from .ibuilder import IBuilder
from .planningerror import PlanningError from .planningerror import PlanningError
from .standard import StandardFlightPlan, StandardLayout from .standard import StandardFlightPlan, StandardLayout
@ -19,7 +19,7 @@ if TYPE_CHECKING:
from dcs import Point from dcs import Point
@dataclass(frozen=True) @dataclass
class AirliftLayout(StandardLayout): class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint] 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
@ -38,6 +38,20 @@ class AirliftLayout(StandardLayout):
ctld_drop_off_zone: FlightWaypoint | None ctld_drop_off_zone: FlightWaypoint | None
nav_to_home: list[FlightWaypoint] 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]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.nav_to_pickup yield from self.nav_to_pickup

View File

@ -18,7 +18,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class CasLayout(PatrollingLayout): class CasLayout(PatrollingLayout):
target: FlightWaypoint target: FlightWaypoint

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class CustomLayout(Layout): class CustomLayout(Layout):
custom_waypoints: list[FlightWaypoint] custom_waypoints: list[FlightWaypoint]

View File

@ -15,10 +15,18 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class FerryLayout(StandardLayout): class FerryLayout(StandardLayout):
nav_to_destination: list[FlightWaypoint] 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_destination

View File

@ -41,7 +41,7 @@ INGRESS_TYPES = {
} }
@dataclass(frozen=True) @dataclass
class Layout(ABC): class Layout(ABC):
departure: FlightWaypoint departure: FlightWaypoint
@ -50,6 +50,9 @@ class Layout(ABC):
"""A list of all waypoints in the flight plan, in order.""" """A list of all waypoints in the flight plan, in order."""
return list(self.iter_waypoints()) return list(self.iter_waypoints())
def delete_waypoint(self, waypoint: FlightWaypoint) -> bool:
return False
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
"""Iterates over all waypoints in the flight plan, in order.""" """Iterates over all waypoints in the flight plan, in order."""
raise NotImplementedError raise NotImplementedError

View File

@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property 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.typeguard import self_type_guard
from game.utils import Speed from game.utils import Speed
@ -16,14 +16,28 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class FormationLayout(LoiterLayout, ABC): class FormationLayout(LoiterLayout, ABC):
nav_to: list[FlightWaypoint] nav_to: list[FlightWaypoint]
join: FlightWaypoint join: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
refuel: FlightWaypoint refuel: Optional[FlightWaypoint]
nav_from: list[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): class FormationFlightPlan(LoiterFlightPlan, ABC):
@property @property

View File

@ -10,7 +10,7 @@ from dcs import Point
from game.flightplan import HoldZoneGeometry from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget from game.theater import MissionTarget
from game.utils import Speed, meters, Distance from game.utils import Speed, meters
from .flightplan import FlightPlan from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder from .ibuilder import IBuilder
@ -130,7 +130,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return super().tot_for_waypoint(waypoint) return super().tot_for_waypoint(waypoint)
@dataclass(frozen=True) @dataclass
class FormationAttackLayout(FormationLayout): class FormationAttackLayout(FormationLayout):
ingress: FlightWaypoint ingress: FlightWaypoint
targets: list[FlightWaypoint] targets: list[FlightWaypoint]

View File

@ -13,7 +13,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class LoiterLayout(StandardLayout, ABC): class LoiterLayout(StandardLayout, ABC):
hold: FlightWaypoint hold: FlightWaypoint

View File

@ -16,13 +16,24 @@ if TYPE_CHECKING:
from .flightplan import FlightPlan from .flightplan import FlightPlan
@dataclass(frozen=True) @dataclass
class PatrollingLayout(StandardLayout): class PatrollingLayout(StandardLayout):
nav_to: list[FlightWaypoint] nav_to: list[FlightWaypoint]
patrol_start: FlightWaypoint patrol_start: FlightWaypoint
patrol_end: FlightWaypoint patrol_end: FlightWaypoint
nav_from: list[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
yield from self.nav_to yield from self.nav_to

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class RtbLayout(StandardLayout): class RtbLayout(StandardLayout):
abort_location: FlightWaypoint abort_location: FlightWaypoint
nav_to_destination: list[FlightWaypoint] nav_to_destination: list[FlightWaypoint]

View File

@ -10,12 +10,18 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class StandardLayout(Layout, ABC): class StandardLayout(Layout, ABC):
arrival: FlightWaypoint arrival: FlightWaypoint
divert: FlightWaypoint | None divert: FlightWaypoint | None
bullseye: FlightWaypoint 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) LayoutT = TypeVar("LayoutT", bound=StandardLayout)

View File

@ -17,7 +17,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class SweepLayout(LoiterLayout): class SweepLayout(LoiterLayout):
nav_to: list[FlightWaypoint] nav_to: list[FlightWaypoint]
sweep_start: FlightWaypoint sweep_start: FlightWaypoint

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True) @dataclass
class TarCapLayout(PatrollingLayout): class TarCapLayout(PatrollingLayout):
refuel: FlightWaypoint | None refuel: FlightWaypoint | None

View File

@ -437,7 +437,7 @@ class WaypointBuilder:
return FlightWaypoint( return FlightWaypoint(
"SEAD Search", "SEAD Search",
FlightWaypointType.CUSTOM, FlightWaypointType.INGRESS_SEAD,
hold, hold,
self.doctrine.ingress_altitude, self.doctrine.ingress_altitude,
alt_type="BARO", alt_type="BARO",

View File

@ -109,13 +109,28 @@ class QFlightWaypointTab(QFrame):
# Need to degrade to a custom flight plan and remove the waypoint. # 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 # If the waypoint is a target waypoint and is not the last target
# waypoint, we don't need to degrade. # waypoint, we don't need to degrade.
if isinstance(self.flight.flight_plan, FormationAttackFlightPlan): fp = self.flight.flight_plan
is_target = waypoint in self.flight.flight_plan.target_area_waypoint.targets if isinstance(fp, FormationAttackFlightPlan):
count = len(self.flight.flight_plan.target_area_waypoint.targets) is_target = waypoint in fp.target_area_waypoint.targets
count = len(fp.target_area_waypoint.targets)
if is_target and count > 1: if is_target and count > 1:
self.flight.flight_plan.target_area_waypoint.targets.remove(waypoint) fp.target_area_waypoint.targets.remove(waypoint)
return 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.<br><br>"
"<b>Are you sure you wish to continue?</b>",
QMessageBox.Yes,
QMessageBox.No,
)
if result == QMessageBox.No:
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.remove(waypoint) self.flight.flight_plan.layout.custom_waypoints.remove(waypoint)