diff --git a/game/ato/flightplans/flightplanbuilder.py b/game/ato/flightplans/flightplanbuilder.py index c8028239..37e6171f 100644 --- a/game/ato/flightplans/flightplanbuilder.py +++ b/game/ato/flightplans/flightplanbuilder.py @@ -3,10 +3,6 @@ from __future__ import annotations from typing import Any, TYPE_CHECKING, Type from game.ato import FlightType -from game.ato.closestairfields import ObjectiveDistanceCache -from game.data.doctrine import Doctrine -from game.flightplan import IpZoneGeometry, JoinZoneGeometry -from game.flightplan.refuelzonegeometry import RefuelZoneGeometry from .aewc import AewcFlightPlan from .airassault import AirAssaultFlightPlan from .airlift import AirliftFlightPlan @@ -27,13 +23,12 @@ from .strike import StrikeFlightPlan from .sweep import SweepFlightPlan from .tarcap import TarCapFlightPlan from .theaterrefueling import TheaterRefuelingFlightPlan -from .waypointbuilder import WaypointBuilder +from ..packagewaypoints import PackageWaypoints if TYPE_CHECKING: - from game.ato import Flight, FlightWaypoint, Package + from game.ato import Flight, Package from game.coalition import Coalition - from game.theater import ConflictTheater, ControlPoint, FrontLine - from game.threatzones import ThreatZones + from game.theater import ConflictTheater, FrontLine class FlightPlanBuilder: @@ -57,14 +52,6 @@ class FlightPlanBuilder: def is_player(self) -> bool: return self.coalition.player - @property - def doctrine(self) -> Doctrine: - return self.coalition.doctrine - - @property - def threat_zones(self) -> ThreatZones: - return self.coalition.opponent.threat_zone - def populate_flight_plan(self, flight: Flight) -> None: """Creates a default flight plan for the given mission.""" if flight not in self.package.flights: @@ -74,7 +61,9 @@ class FlightPlanBuilder: try: if self.package.waypoints is None: - self.regenerate_package_waypoints() + self.package.waypoints = PackageWaypoints.create( + self.package, self.coalition + ) flight.flight_plan = self.generate_flight_plan(flight) except NavMeshError as ex: color = "blue" if self.is_player else "red" @@ -121,76 +110,3 @@ class FlightPlanBuilder: ) layout = plan_type.builder_type()(flight, self.theater).build() return plan_type(flight, layout) - - def regenerate_flight_plans(self) -> None: - new_flights: list[Flight] = [] - for old_flight in self.package.flights: - old_flight.flight_plan = self.generate_flight_plan(old_flight) - new_flights.append(old_flight) - self.package.flights = new_flights - - def regenerate_package_waypoints(self) -> None: - from game.ato.packagewaypoints import PackageWaypoints - - package_airfield = self.package_airfield() - - # Start by picking the best IP for the attack. - ingress_point = IpZoneGeometry( - self.package.target.position, - package_airfield.position, - self.coalition, - ).find_best_ip() - - join_point = JoinZoneGeometry( - self.package.target.position, - package_airfield.position, - ingress_point, - self.coalition, - ).find_best_join_point() - - refuel_point = RefuelZoneGeometry( - package_airfield.position, - join_point, - self.coalition, - ).find_best_refuel_point() - - # And the split point based on the best route from the IP. Since that's no - # different than the best route *to* the IP, this is the same as the join point. - # TODO: Estimate attack completion point based on the IP and split from there? - self.package.waypoints = PackageWaypoints( - WaypointBuilder.perturb(join_point), - ingress_point, - WaypointBuilder.perturb(join_point), - refuel_point, - ) - - # TODO: Make a model for the waypoint builder and use that in the UI. - def generate_rtb_waypoint( - self, flight: Flight, arrival: ControlPoint - ) -> FlightWaypoint: - """Generate RTB landing point. - - Args: - flight: The flight to generate the landing waypoint for. - arrival: Arrival airfield or carrier. - """ - builder = WaypointBuilder(flight, self.coalition) - return builder.land(arrival) - - def package_airfield(self) -> ControlPoint: - # We'll always have a package, but if this is being planned via the UI - # it could be the first flight in the package. - if not self.package.flights: - raise PlanningError( - "Cannot determine source airfield for package with no flights" - ) - - # The package airfield is either the flight's airfield (when there is no - # package) or the closest airfield to the objective that is the - # departure airfield for some flight in the package. - cache = ObjectiveDistanceCache.get_closest_airfields(self.package.target) - for airfield in cache.operational_airfields: - for flight in self.package.flights: - if flight.departure == airfield: - return airfield - raise PlanningError("Could not find any airfield assigned to this package") diff --git a/game/ato/package.py b/game/ato/package.py index b36cd37b..4a56526f 100644 --- a/game/ato/package.py +++ b/game/ato/package.py @@ -6,16 +6,17 @@ from dataclasses import dataclass, field from datetime import timedelta from typing import Dict, List, Optional, TYPE_CHECKING -from .flightplans.formation import FormationFlightPlan from game.db import Database from game.utils import Speed +from .closestairfields import ObjectiveDistanceCache from .flight import Flight +from .flightplans.formation import FormationFlightPlan from .flighttype import FlightType from .packagewaypoints import PackageWaypoints from .traveltime import TotEstimator if TYPE_CHECKING: - from game.theater import MissionTarget + from game.theater import ControlPoint, MissionTarget @dataclass @@ -193,6 +194,24 @@ class Package: return "OCA Strike" return str(task) + def departure_closest_to_target(self) -> ControlPoint: + # We'll always have a package, but if this is being planned via the UI + # it could be the first flight in the package. + if not self.flights: + raise RuntimeError( + "Cannot determine source airfield for package with no flights" + ) + + # The package airfield is either the flight's airfield (when there is no + # package) or the closest airfield to the objective that is the + # departure airfield for some flight in the package. + cache = ObjectiveDistanceCache.get_closest_airfields(self.target) + for airfield in cache.operational_airfields: + for flight in self.flights: + if flight.departure == airfield: + return airfield + raise RuntimeError("Could not find any airfield assigned to this package") + def __hash__(self) -> int: # TODO: Far from perfect. Number packages? return hash(self.target.name) diff --git a/game/ato/packagewaypoints.py b/game/ato/packagewaypoints.py index e478b607..62bd4219 100644 --- a/game/ato/packagewaypoints.py +++ b/game/ato/packagewaypoints.py @@ -1,8 +1,18 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Optional +from typing import TYPE_CHECKING from dcs import Point +from game.ato.flightplans.waypointbuilder import WaypointBuilder +from game.flightplan import IpZoneGeometry, JoinZoneGeometry +from game.flightplan.refuelzonegeometry import RefuelZoneGeometry + +if TYPE_CHECKING: + from game.ato import Package + from game.coalition import Coalition + @dataclass(frozen=True) class PackageWaypoints: @@ -10,3 +20,37 @@ class PackageWaypoints: ingress: Point split: Point refuel: Point + + @staticmethod + def create(package: Package, coalition: Coalition) -> PackageWaypoints: + origin = package.departure_closest_to_target() + + # Start by picking the best IP for the attack. + ingress_point = IpZoneGeometry( + package.target.position, + origin.position, + coalition, + ).find_best_ip() + + join_point = JoinZoneGeometry( + package.target.position, + origin.position, + ingress_point, + coalition, + ).find_best_join_point() + + refuel_point = RefuelZoneGeometry( + origin.position, + join_point, + coalition, + ).find_best_refuel_point() + + # And the split point based on the best route from the IP. Since that's no + # different than the best route *to* the IP, this is the same as the join point. + # TODO: Estimate attack completion point based on the IP and split from there? + return PackageWaypoints( + WaypointBuilder.perturb(join_point), + ingress_point, + WaypointBuilder.perturb(join_point), + refuel_point, + ) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index bf79f925..75ab7ed1 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable, List, Optional, Any +from typing import Iterable, List, Optional from PySide2.QtCore import Signal from PySide2.QtWidgets import ( @@ -14,10 +14,10 @@ from PySide2.QtWidgets import ( from game import Game from game.ato.flight import Flight from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout -from game.ato.flightplans.flightplan import FlightPlan from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder from game.ato.flightplans.formationattack import FormationAttackFlightPlan from game.ato.flightplans.planningerror import PlanningError +from game.ato.flightplans.waypointbuilder import WaypointBuilder from game.ato.flighttype import FlightType from game.ato.flightwaypoint import FlightWaypoint from game.ato.loadouts import Loadout @@ -139,7 +139,7 @@ class QFlightWaypointTab(QFrame): self.on_change() def on_rtb_waypoint(self): - rtb = self.planner.generate_rtb_waypoint(self.flight, self.flight.from_cp) + rtb = WaypointBuilder(self.flight, self.coalition).land(self.flight.arrival) self.degrade_to_custom_flight_plan() assert isinstance(self.flight.flight_plan, CustomFlightPlan) self.flight.flight_plan.layout.custom_waypoints.append(rtb)