mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Dan's massive refactor
Squashing 8 commits by DanAlbert: - Track theater in ControlPoint. Simplifies finding the owning theater of a control point. Not used yet. - Clean some cruft out of FlightPlanBuilder. - Clean up silly some exception handling. - Move FlightPlan instantiation into the builder. I'm working on moving the builder to be owned by the Flight, which will simplify callers that need to create (or recreate) flight plans for a flight. - Simplify IBuilder constructor. We have access to the theater via the flight's departure airbase now. - Move FlightPlan creation into Flight. For now this is just a callsite cleanup. Later, this will make it easier to separate unscheduled and scheduled flights into different classes without complicating the layout/scheduling. - Remove superfluous constructors. - Remove unused Package field.
This commit is contained in:
parent
ede4ce1362
commit
48938fc529
0
doc/design/flight-creation.md
Normal file
0
doc/design/flight-creation.md
Normal file
@ -7,10 +7,12 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
|||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||||
|
|
||||||
|
from .flightplans.planningerror import PlanningError
|
||||||
from .flightroster import FlightRoster
|
from .flightroster import FlightRoster
|
||||||
from .flightstate import FlightState, Navigating, Uninitialized
|
from .flightstate import FlightState, Navigating, Uninitialized
|
||||||
from .flightstate.killed import Killed
|
from .flightstate.killed import Killed
|
||||||
from .loadouts import Loadout
|
from .loadouts import Loadout
|
||||||
|
from .packagewaypoints import PackageWaypoints
|
||||||
from ..sidc import (
|
from ..sidc import (
|
||||||
Entity,
|
Entity,
|
||||||
SidcDescribable,
|
SidcDescribable,
|
||||||
@ -81,9 +83,9 @@ class Flight(SidcDescribable):
|
|||||||
# Used for simulating the travel to first contact.
|
# Used for simulating the travel to first contact.
|
||||||
self.state: FlightState = Uninitialized(self, squadron.settings)
|
self.state: FlightState = Uninitialized(self, squadron.settings)
|
||||||
|
|
||||||
# Will be replaced with a more appropriate FlightPlan by
|
# Will be replaced with a more appropriate FlightPlan later, but start with a
|
||||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
# cheaply constructed one since adding more flights to the package may affect
|
||||||
# empty flight plan.
|
# the optimal layout.
|
||||||
from .flightplans.custom import CustomFlightPlan, CustomLayout
|
from .flightplans.custom import CustomFlightPlan, CustomLayout
|
||||||
|
|
||||||
self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
|
self.flight_plan: FlightPlan[Any] = CustomFlightPlan(
|
||||||
@ -194,8 +196,7 @@ class Flight(SidcDescribable):
|
|||||||
def abort(self) -> None:
|
def abort(self) -> None:
|
||||||
from .flightplans.rtb import RtbFlightPlan
|
from .flightplans.rtb import RtbFlightPlan
|
||||||
|
|
||||||
layout = RtbFlightPlan.builder_type()(self, self.coalition.game.theater).build()
|
self.flight_plan = RtbFlightPlan.builder_type()(self).build()
|
||||||
self.flight_plan = RtbFlightPlan(self, layout)
|
|
||||||
|
|
||||||
self.set_state(
|
self.set_state(
|
||||||
Navigating(
|
Navigating(
|
||||||
@ -244,3 +245,24 @@ class Flight(SidcDescribable):
|
|||||||
for pilot in self.roster.pilots:
|
for pilot in self.roster.pilots:
|
||||||
if pilot is not None:
|
if pilot is not None:
|
||||||
results.kill_pilot(self, pilot)
|
results.kill_pilot(self, pilot)
|
||||||
|
|
||||||
|
def recreate_flight_plan(self) -> None:
|
||||||
|
self.flight_plan = self._make_flight_plan()
|
||||||
|
|
||||||
|
def _make_flight_plan(self) -> FlightPlan[Any]:
|
||||||
|
from game.navmesh import NavMeshError
|
||||||
|
from .flightplans.flightplanbuildertypes import FlightPlanBuilderTypes
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.package.waypoints is None:
|
||||||
|
self.package.waypoints = PackageWaypoints.create(
|
||||||
|
self.package, self.coalition
|
||||||
|
)
|
||||||
|
builder = FlightPlanBuilderTypes.for_flight(self)(self)
|
||||||
|
return builder.build()
|
||||||
|
except NavMeshError as ex:
|
||||||
|
color = "blue" if self.squadron.player else "red"
|
||||||
|
raise PlanningError(
|
||||||
|
f"Could not plan {color} {self.flight_type.value} from "
|
||||||
|
f"{self.departure} to {self.package.target}"
|
||||||
|
) from ex
|
||||||
|
|||||||
@ -9,8 +9,31 @@ from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
|||||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||||
def build(self) -> PatrollingLayout:
|
@property
|
||||||
|
def patrol_duration(self) -> timedelta:
|
||||||
|
return timedelta(hours=4)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_speed(self) -> Speed:
|
||||||
|
altitude = self.layout.patrol_start.alt
|
||||||
|
if self.flight.unit_type.preferred_patrol_speed(altitude) is not None:
|
||||||
|
return self.flight.unit_type.preferred_patrol_speed(altitude)
|
||||||
|
return knots(390)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engagement_distance(self) -> Distance:
|
||||||
|
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||||
|
# No harm in setting this, but we ought to clean up a bit.
|
||||||
|
return meters(0)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
||||||
|
def layout(self) -> PatrollingLayout:
|
||||||
racetrack_half_distance = nautical_miles(30).meters
|
racetrack_half_distance = nautical_miles(30).meters
|
||||||
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
@ -67,25 +90,5 @@ class Builder(IBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> AewcFlightPlan:
|
||||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
return AewcFlightPlan(self.flight, self.layout())
|
||||||
@property
|
|
||||||
def patrol_duration(self) -> timedelta:
|
|
||||||
return timedelta(hours=4)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_speed(self) -> Speed:
|
|
||||||
altitude = self.layout.patrol_start.alt
|
|
||||||
if self.flight.unit_type.preferred_patrol_speed(altitude) is not None:
|
|
||||||
return self.flight.unit_type.preferred_patrol_speed(altitude)
|
|
||||||
return knots(390)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def engagement_distance(self) -> Distance:
|
|
||||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
|
||||||
# No harm in setting this, but we ought to clean up a bit.
|
|
||||||
return meters(0)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[IBuilder]:
|
|
||||||
return Builder
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING, Iterator, Type
|
from typing import Iterator, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.ato.flightplans.airlift import AirliftLayout
|
from game.ato.flightplans.airlift import AirliftLayout
|
||||||
from game.ato.flightplans.standard import StandardFlightPlan
|
from game.ato.flightplans.standard import StandardFlightPlan
|
||||||
from game.theater.controlpoint import ControlPointType
|
from game.theater.controlpoint import ControlPointType
|
||||||
@ -12,12 +13,57 @@ from .ibuilder import IBuilder
|
|||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..flight import Flight
|
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
@dataclass(frozen=True)
|
||||||
def build(self) -> AirAssaultLayout:
|
class AirAssaultLayout(AirliftLayout):
|
||||||
|
target: FlightWaypoint
|
||||||
|
|
||||||
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
|
yield self.departure
|
||||||
|
yield from self.nav_to_pickup
|
||||||
|
if self.pickup:
|
||||||
|
yield self.pickup
|
||||||
|
yield from self.nav_to_drop_off
|
||||||
|
yield self.drop_off
|
||||||
|
yield self.target
|
||||||
|
yield from self.nav_to_home
|
||||||
|
yield self.arrival
|
||||||
|
if self.divert is not None:
|
||||||
|
yield self.divert
|
||||||
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
|
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.drop_off
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||||
|
if waypoint == self.tot_waypoint:
|
||||||
|
return self.tot
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engagement_distance(self) -> Distance:
|
||||||
|
# The radius of the WaypointZone created at the target location
|
||||||
|
return meters(2500)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||||
|
def layout(self) -> AirAssaultLayout:
|
||||||
|
|
||||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||||
altitude_is_agl = self.flight.is_helo
|
altitude_is_agl = self.flight.is_helo
|
||||||
@ -78,51 +124,5 @@ class Builder(IBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> AirAssaultFlightPlan:
|
||||||
@dataclass(frozen=True)
|
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||||
class AirAssaultLayout(AirliftLayout):
|
|
||||||
target: FlightWaypoint
|
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
|
||||||
yield self.departure
|
|
||||||
yield from self.nav_to_pickup
|
|
||||||
if self.pickup:
|
|
||||||
yield self.pickup
|
|
||||||
yield from self.nav_to_drop_off
|
|
||||||
yield self.drop_off
|
|
||||||
yield self.target
|
|
||||||
yield from self.nav_to_home
|
|
||||||
yield self.arrival
|
|
||||||
if self.divert is not None:
|
|
||||||
yield self.divert
|
|
||||||
yield self.bullseye
|
|
||||||
|
|
||||||
|
|
||||||
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
|
|
||||||
def __init__(self, flight: Flight, layout: AirAssaultLayout) -> None:
|
|
||||||
super().__init__(flight, layout)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[Builder]:
|
|
||||||
return Builder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
|
||||||
return self.layout.drop_off
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
|
||||||
if waypoint == self.tot_waypoint:
|
|
||||||
return self.tot
|
|
||||||
return None
|
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def engagement_distance(self) -> Distance:
|
|
||||||
# The radius of the WaypointZone created at the target location
|
|
||||||
return meters(2500)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mission_departure_time(self) -> timedelta:
|
|
||||||
return self.package.time_over_target
|
|
||||||
|
|||||||
@ -4,8 +4,8 @@ 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
|
||||||
from game.theater.missiontarget import MissionTarget
|
|
||||||
|
|
||||||
|
from game.theater.missiontarget import MissionTarget
|
||||||
from game.utils import feet
|
from game.utils import feet
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
from .planningerror import PlanningError
|
from .planningerror import PlanningError
|
||||||
@ -13,12 +13,58 @@ from .standard import StandardFlightPlan, StandardLayout
|
|||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..flight import Flight
|
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
@dataclass(frozen=True)
|
||||||
def build(self) -> AirliftLayout:
|
class AirliftLayout(StandardLayout):
|
||||||
|
nav_to_pickup: list[FlightWaypoint]
|
||||||
|
pickup: FlightWaypoint | None
|
||||||
|
nav_to_drop_off: list[FlightWaypoint]
|
||||||
|
drop_off: FlightWaypoint
|
||||||
|
stopover: FlightWaypoint | None
|
||||||
|
nav_to_home: list[FlightWaypoint]
|
||||||
|
|
||||||
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
|
yield self.departure
|
||||||
|
yield from self.nav_to_pickup
|
||||||
|
if self.pickup is not None:
|
||||||
|
yield self.pickup
|
||||||
|
yield from self.nav_to_drop_off
|
||||||
|
yield self.drop_off
|
||||||
|
if self.stopover is not None:
|
||||||
|
yield self.stopover
|
||||||
|
yield from self.nav_to_home
|
||||||
|
yield self.arrival
|
||||||
|
if self.divert is not None:
|
||||||
|
yield self.divert
|
||||||
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
|
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_waypoint(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.drop_off
|
||||||
|
|
||||||
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||||
|
# TOT planning isn't really useful for transports. They're behind the front
|
||||||
|
# lines so no need to wait for escorts or for other missions to complete.
|
||||||
|
return None
|
||||||
|
|
||||||
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mission_departure_time(self) -> timedelta:
|
||||||
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||||
|
def layout(self) -> AirliftLayout:
|
||||||
cargo = self.flight.cargo
|
cargo = self.flight.cargo
|
||||||
if cargo is None:
|
if cargo is None:
|
||||||
raise PlanningError(
|
raise PlanningError(
|
||||||
@ -97,52 +143,5 @@ class Builder(IBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> AirliftFlightPlan:
|
||||||
@dataclass(frozen=True)
|
return AirliftFlightPlan(self.flight, self.layout())
|
||||||
class AirliftLayout(StandardLayout):
|
|
||||||
nav_to_pickup: list[FlightWaypoint]
|
|
||||||
pickup: FlightWaypoint | None
|
|
||||||
nav_to_drop_off: list[FlightWaypoint]
|
|
||||||
drop_off: FlightWaypoint
|
|
||||||
stopover: FlightWaypoint | None
|
|
||||||
nav_to_home: list[FlightWaypoint]
|
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
|
||||||
yield self.departure
|
|
||||||
yield from self.nav_to_pickup
|
|
||||||
if self.pickup is not None:
|
|
||||||
yield self.pickup
|
|
||||||
yield from self.nav_to_drop_off
|
|
||||||
yield self.drop_off
|
|
||||||
if self.stopover is not None:
|
|
||||||
yield self.stopover
|
|
||||||
yield from self.nav_to_home
|
|
||||||
yield self.arrival
|
|
||||||
if self.divert is not None:
|
|
||||||
yield self.divert
|
|
||||||
yield self.bullseye
|
|
||||||
|
|
||||||
|
|
||||||
class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
|
||||||
def __init__(self, flight: Flight, layout: AirliftLayout) -> None:
|
|
||||||
super().__init__(flight, layout)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[Builder]:
|
|
||||||
return Builder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tot_waypoint(self) -> FlightWaypoint | None:
|
|
||||||
return self.layout.drop_off
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
|
||||||
# TOT planning isn't really useful for transports. They're behind the front
|
|
||||||
# lines so no need to wait for escorts or for other missions to complete.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mission_departure_time(self) -> timedelta:
|
|
||||||
return self.package.time_over_target
|
|
||||||
|
|||||||
@ -20,8 +20,8 @@ class AntiShipFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
from game.transfers import CargoShip
|
from game.transfers import CargoShip
|
||||||
@ -40,3 +40,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
||||||
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||||
|
|
||||||
|
def build(self) -> AntiShipFlightPlan:
|
||||||
|
return AntiShipFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -19,8 +19,8 @@ class BaiFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
from game.transfers import Convoy
|
from game.transfers import Convoy
|
||||||
@ -38,3 +38,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||||
|
|
||||||
|
def build(self) -> BaiFlightPlan:
|
||||||
|
return BaiFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -12,8 +12,28 @@ from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
|||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
|
||||||
|
|
||||||
class Builder(CapBuilder):
|
class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||||
def build(self) -> PatrollingLayout:
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_duration(self) -> timedelta:
|
||||||
|
return self.flight.coalition.doctrine.cap_duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_speed(self) -> Speed:
|
||||||
|
return self.flight.unit_type.preferred_patrol_speed(
|
||||||
|
self.layout.patrol_start.alt
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engagement_distance(self) -> Distance:
|
||||||
|
return self.flight.coalition.doctrine.cap_engagement_range
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||||
|
def layout(self) -> PatrollingLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if isinstance(location, FrontLine):
|
if isinstance(location, FrontLine):
|
||||||
@ -46,22 +66,5 @@ class Builder(CapBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> BarCapFlightPlan:
|
||||||
class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
return BarCapFlightPlan(self.flight, self.layout())
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[Builder]:
|
|
||||||
return Builder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_duration(self) -> timedelta:
|
|
||||||
return self.flight.coalition.doctrine.cap_duration
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_speed(self) -> Speed:
|
|
||||||
return self.flight.unit_type.preferred_patrol_speed(
|
|
||||||
self.layout.patrol_start.alt
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def engagement_distance(self) -> Distance:
|
|
||||||
return self.flight.coalition.doctrine.cap_engagement_range
|
|
||||||
|
|||||||
@ -2,12 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import TYPE_CHECKING
|
from typing import Any, TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from shapely.geometry import Point as ShapelyPoint
|
from shapely.geometry import Point as ShapelyPoint
|
||||||
|
|
||||||
from game.utils import Heading, meters, nautical_miles
|
from game.utils import Heading, meters, nautical_miles
|
||||||
|
from .flightplan import FlightPlan
|
||||||
|
from .patrolling import PatrollingLayout
|
||||||
from ..closestairfields import ObjectiveDistanceCache
|
from ..closestairfields import ObjectiveDistanceCache
|
||||||
from ..flightplans.ibuilder import IBuilder
|
from ..flightplans.ibuilder import IBuilder
|
||||||
from ..flightplans.planningerror import PlanningError
|
from ..flightplans.planningerror import PlanningError
|
||||||
@ -15,8 +17,11 @@ from ..flightplans.planningerror import PlanningError
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.theater import MissionTarget
|
from game.theater import MissionTarget
|
||||||
|
|
||||||
|
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
|
||||||
|
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
|
||||||
|
|
||||||
class CapBuilder(IBuilder, ABC):
|
|
||||||
|
class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||||
def cap_racetrack_for_objective(
|
def cap_racetrack_for_objective(
|
||||||
self, location: MissionTarget, barcap: bool
|
self, location: MissionTarget, barcap: bool
|
||||||
) -> tuple[Point, Point]:
|
) -> tuple[Point, Point]:
|
||||||
|
|||||||
@ -17,8 +17,58 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
@dataclass(frozen=True)
|
||||||
def build(self) -> CasLayout:
|
class CasLayout(PatrollingLayout):
|
||||||
|
target: FlightWaypoint
|
||||||
|
|
||||||
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
|
yield self.departure
|
||||||
|
yield from self.nav_to
|
||||||
|
yield self.patrol_start
|
||||||
|
yield self.target
|
||||||
|
yield self.patrol_end
|
||||||
|
yield from self.nav_from
|
||||||
|
yield self.departure
|
||||||
|
if self.divert is not None:
|
||||||
|
yield self.divert
|
||||||
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
|
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
||||||
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_duration(self) -> timedelta:
|
||||||
|
return self.flight.coalition.doctrine.cas_duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_speed(self) -> Speed:
|
||||||
|
# 2021-08-02: patrol_speed will currently have no effect because
|
||||||
|
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
|
||||||
|
# to have patrol_speed
|
||||||
|
return kph(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engagement_distance(self) -> Distance:
|
||||||
|
from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH
|
||||||
|
|
||||||
|
return meters(FRONTLINE_LENGTH) / 2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
|
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
|
||||||
|
|
||||||
|
def request_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.patrol_start
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.patrol_end
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||||
|
def layout(self) -> CasLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, FrontLine):
|
if not isinstance(location, FrontLine):
|
||||||
@ -71,52 +121,5 @@ class Builder(IBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> CasFlightPlan:
|
||||||
@dataclass(frozen=True)
|
return CasFlightPlan(self.flight, self.layout())
|
||||||
class CasLayout(PatrollingLayout):
|
|
||||||
target: FlightWaypoint
|
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
|
||||||
yield self.departure
|
|
||||||
yield from self.nav_to
|
|
||||||
yield self.patrol_start
|
|
||||||
yield self.target
|
|
||||||
yield self.patrol_end
|
|
||||||
yield from self.nav_from
|
|
||||||
yield self.departure
|
|
||||||
if self.divert is not None:
|
|
||||||
yield self.divert
|
|
||||||
yield self.bullseye
|
|
||||||
|
|
||||||
|
|
||||||
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
|
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[Builder]:
|
|
||||||
return Builder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_duration(self) -> timedelta:
|
|
||||||
return self.flight.coalition.doctrine.cas_duration
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_speed(self) -> Speed:
|
|
||||||
# 2021-08-02: patrol_speed will currently have no effect because
|
|
||||||
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
|
|
||||||
# to have patrol_speed
|
|
||||||
return kph(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def engagement_distance(self) -> Distance:
|
|
||||||
from game.missiongenerator.frontlineconflictdescription import FRONTLINE_LENGTH
|
|
||||||
|
|
||||||
return meters(FRONTLINE_LENGTH) / 2
|
|
||||||
|
|
||||||
@property
|
|
||||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
|
||||||
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
|
|
||||||
|
|
||||||
def request_escort_at(self) -> FlightWaypoint | None:
|
|
||||||
return self.layout.patrol_start
|
|
||||||
|
|
||||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
|
||||||
return self.layout.patrol_end
|
|
||||||
|
|||||||
@ -13,11 +13,6 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
|
||||||
def build(self) -> CustomLayout:
|
|
||||||
return CustomLayout([])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CustomLayout(Layout):
|
class CustomLayout(Layout):
|
||||||
custom_waypoints: list[FlightWaypoint]
|
custom_waypoints: list[FlightWaypoint]
|
||||||
@ -55,3 +50,11 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
|||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> timedelta:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||||
|
def layout(self) -> CustomLayout:
|
||||||
|
return CustomLayout([])
|
||||||
|
|
||||||
|
def build(self) -> CustomFlightPlan:
|
||||||
|
return CustomFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -22,8 +22,8 @@ class DeadFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
is_ewr = isinstance(location, EwrGroundObject)
|
is_ewr = isinstance(location, EwrGroundObject)
|
||||||
@ -36,3 +36,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_DEAD)
|
return self._build(FlightWaypointType.INGRESS_DEAD)
|
||||||
|
|
||||||
|
def build(self) -> DeadFlightPlan:
|
||||||
|
return DeadFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -16,8 +16,8 @@ class EscortFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
assert self.package.waypoints is not None
|
assert self.package.waypoints is not None
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
@ -51,3 +51,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> EscortFlightPlan:
|
||||||
|
return EscortFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -15,36 +15,6 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
|
||||||
def build(self) -> FerryLayout:
|
|
||||||
if self.flight.departure == self.flight.arrival:
|
|
||||||
raise PlanningError(
|
|
||||||
f"Cannot plan ferry self.flight: departure and arrival are both "
|
|
||||||
f"{self.flight.departure}"
|
|
||||||
)
|
|
||||||
|
|
||||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
|
||||||
altitude = (
|
|
||||||
feet(1500)
|
|
||||||
if altitude_is_agl
|
|
||||||
else self.flight.unit_type.preferred_patrol_altitude
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
|
||||||
return FerryLayout(
|
|
||||||
departure=builder.takeoff(self.flight.departure),
|
|
||||||
nav_to_destination=builder.nav_path(
|
|
||||||
self.flight.departure.position,
|
|
||||||
self.flight.arrival.position,
|
|
||||||
altitude,
|
|
||||||
altitude_is_agl,
|
|
||||||
),
|
|
||||||
arrival=builder.land(self.flight.arrival),
|
|
||||||
divert=builder.divert(self.flight.divert),
|
|
||||||
bullseye=builder.bullseye(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FerryLayout(StandardLayout):
|
class FerryLayout(StandardLayout):
|
||||||
nav_to_destination: list[FlightWaypoint]
|
nav_to_destination: list[FlightWaypoint]
|
||||||
@ -78,3 +48,36 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
|||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> timedelta:
|
||||||
return self.package.time_over_target
|
return self.package.time_over_target
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
||||||
|
def layout(self) -> FerryLayout:
|
||||||
|
if self.flight.departure == self.flight.arrival:
|
||||||
|
raise PlanningError(
|
||||||
|
f"Cannot plan ferry self.flight: departure and arrival are both "
|
||||||
|
f"{self.flight.departure}"
|
||||||
|
)
|
||||||
|
|
||||||
|
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||||
|
altitude = (
|
||||||
|
feet(1500)
|
||||||
|
if altitude_is_agl
|
||||||
|
else self.flight.unit_type.preferred_patrol_altitude
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
return FerryLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
nav_to_destination=builder.nav_path(
|
||||||
|
self.flight.departure.position,
|
||||||
|
self.flight.arrival.position,
|
||||||
|
altitude,
|
||||||
|
altitude_is_agl,
|
||||||
|
),
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self) -> FerryFlightPlan:
|
||||||
|
return FerryFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -8,15 +8,14 @@ generating the waypoints for the mission.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, Generic, TYPE_CHECKING, Type, TypeGuard, TypeVar
|
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||||
|
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
from game.utils import Distance, Speed, meters
|
from game.utils import Distance, Speed, meters
|
||||||
from .ibuilder import IBuilder
|
|
||||||
from .planningerror import PlanningError
|
from .planningerror import PlanningError
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
from ..starttype import StartType
|
from ..starttype import StartType
|
||||||
@ -64,11 +63,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
def package(self) -> Package:
|
def package(self) -> Package:
|
||||||
return self.flight.package
|
return self.flight.package
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abstractmethod
|
|
||||||
def builder_type() -> Type[IBuilder]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def waypoints(self) -> list[FlightWaypoint]:
|
def waypoints(self) -> list[FlightWaypoint]:
|
||||||
"""A list of all waypoints in the flight plan, in order."""
|
"""A list of all waypoints in the flight plan, in order."""
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
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
|
|
||||||
from .antiship import AntiShipFlightPlan
|
|
||||||
from .bai import BaiFlightPlan
|
|
||||||
from .barcap import BarCapFlightPlan
|
|
||||||
from .cas import CasFlightPlan
|
|
||||||
from .dead import DeadFlightPlan
|
|
||||||
from .escort import EscortFlightPlan
|
|
||||||
from .ferry import FerryFlightPlan
|
|
||||||
from .flightplan import FlightPlan
|
|
||||||
from .ocaaircraft import OcaAircraftFlightPlan
|
|
||||||
from .ocarunway import OcaRunwayFlightPlan
|
|
||||||
from .packagerefueling import PackageRefuelingFlightPlan
|
|
||||||
from .planningerror import PlanningError
|
|
||||||
from .sead import SeadFlightPlan
|
|
||||||
from .strike import StrikeFlightPlan
|
|
||||||
from .sweep import SweepFlightPlan
|
|
||||||
from .tarcap import TarCapFlightPlan
|
|
||||||
from .theaterrefueling import TheaterRefuelingFlightPlan
|
|
||||||
from .waypointbuilder import WaypointBuilder
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.ato import Flight, FlightWaypoint, Package
|
|
||||||
from game.coalition import Coalition
|
|
||||||
from game.theater import ConflictTheater, ControlPoint, FrontLine
|
|
||||||
from game.threatzones import ThreatZones
|
|
||||||
|
|
||||||
|
|
||||||
class FlightPlanBuilder:
|
|
||||||
"""Generates flight plans for flights."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, package: Package, coalition: Coalition, theater: ConflictTheater
|
|
||||||
) -> None:
|
|
||||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
|
||||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
|
||||||
# between the join and split points, so we don't need speeds for each
|
|
||||||
# leg individually since they should all be fairly similar. This doesn't
|
|
||||||
# hold too well right now since nothing is stopping each waypoint from
|
|
||||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
|
||||||
# should be avoiding anyway.
|
|
||||||
self.package = package
|
|
||||||
self.coalition = coalition
|
|
||||||
self.theater = theater
|
|
||||||
|
|
||||||
@property
|
|
||||||
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:
|
|
||||||
raise RuntimeError("Flight must be a part of the package")
|
|
||||||
|
|
||||||
from game.navmesh import NavMeshError
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.package.waypoints is None:
|
|
||||||
self.regenerate_package_waypoints()
|
|
||||||
flight.flight_plan = self.generate_flight_plan(flight)
|
|
||||||
except NavMeshError as ex:
|
|
||||||
color = "blue" if self.is_player else "red"
|
|
||||||
raise PlanningError(
|
|
||||||
f"Could not plan {color} {flight.flight_type.value} from "
|
|
||||||
f"{flight.departure} to {flight.package.target}"
|
|
||||||
) from ex
|
|
||||||
|
|
||||||
def plan_type(self, task: FlightType) -> Type[FlightPlan[Any]] | None:
|
|
||||||
plan_type: Type[FlightPlan[Any]]
|
|
||||||
if task == FlightType.REFUELING:
|
|
||||||
if self.package.target.is_friendly(self.is_player) or isinstance(
|
|
||||||
self.package.target, FrontLine
|
|
||||||
):
|
|
||||||
return TheaterRefuelingFlightPlan
|
|
||||||
return PackageRefuelingFlightPlan
|
|
||||||
|
|
||||||
plan_dict: dict[FlightType, Type[FlightPlan[Any]]] = {
|
|
||||||
FlightType.ANTISHIP: AntiShipFlightPlan,
|
|
||||||
FlightType.BAI: BaiFlightPlan,
|
|
||||||
FlightType.BARCAP: BarCapFlightPlan,
|
|
||||||
FlightType.CAS: CasFlightPlan,
|
|
||||||
FlightType.DEAD: DeadFlightPlan,
|
|
||||||
FlightType.ESCORT: EscortFlightPlan,
|
|
||||||
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan,
|
|
||||||
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan,
|
|
||||||
FlightType.SEAD: SeadFlightPlan,
|
|
||||||
FlightType.SEAD_ESCORT: EscortFlightPlan,
|
|
||||||
FlightType.STRIKE: StrikeFlightPlan,
|
|
||||||
FlightType.SWEEP: SweepFlightPlan,
|
|
||||||
FlightType.TARCAP: TarCapFlightPlan,
|
|
||||||
FlightType.AEWC: AewcFlightPlan,
|
|
||||||
FlightType.TRANSPORT: AirliftFlightPlan,
|
|
||||||
FlightType.FERRY: FerryFlightPlan,
|
|
||||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan,
|
|
||||||
}
|
|
||||||
return plan_dict.get(task)
|
|
||||||
|
|
||||||
def generate_flight_plan(self, flight: Flight) -> FlightPlan[Any]:
|
|
||||||
plan_type = self.plan_type(flight.flight_type)
|
|
||||||
if plan_type is None:
|
|
||||||
raise PlanningError(
|
|
||||||
f"{flight.flight_type} flight plan generation not implemented"
|
|
||||||
)
|
|
||||||
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")
|
|
||||||
66
game/ato/flightplans/flightplanbuildertypes.py
Normal file
66
game/ato/flightplans/flightplanbuildertypes.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from game.ato import FlightType
|
||||||
|
from .aewc import AewcFlightPlan
|
||||||
|
from .airassault import AirAssaultFlightPlan
|
||||||
|
from .airlift import AirliftFlightPlan
|
||||||
|
from .antiship import AntiShipFlightPlan
|
||||||
|
from .bai import BaiFlightPlan
|
||||||
|
from .barcap import BarCapFlightPlan
|
||||||
|
from .cas import CasFlightPlan
|
||||||
|
from .dead import DeadFlightPlan
|
||||||
|
from .escort import EscortFlightPlan
|
||||||
|
from .ferry import FerryFlightPlan
|
||||||
|
from .ibuilder import IBuilder
|
||||||
|
from .ocaaircraft import OcaAircraftFlightPlan
|
||||||
|
from .ocarunway import OcaRunwayFlightPlan
|
||||||
|
from .packagerefueling import PackageRefuelingFlightPlan
|
||||||
|
from .planningerror import PlanningError
|
||||||
|
from .sead import SeadFlightPlan
|
||||||
|
from .strike import StrikeFlightPlan
|
||||||
|
from .sweep import SweepFlightPlan
|
||||||
|
from .tarcap import TarCapFlightPlan
|
||||||
|
from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.ato import Flight
|
||||||
|
from game.theater import FrontLine
|
||||||
|
|
||||||
|
|
||||||
|
class FlightPlanBuilderTypes:
|
||||||
|
@staticmethod
|
||||||
|
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||||
|
if flight.flight_type is FlightType.REFUELING:
|
||||||
|
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
|
||||||
|
flight.package.target, FrontLine
|
||||||
|
):
|
||||||
|
return TheaterRefuelingFlightPlan.builder_type()
|
||||||
|
return PackageRefuelingFlightPlan.builder_type()
|
||||||
|
|
||||||
|
builder_dict: dict[FlightType, Type[IBuilder[Any, Any]]] = {
|
||||||
|
FlightType.ANTISHIP: AntiShipFlightPlan.builder_type(),
|
||||||
|
FlightType.BAI: BaiFlightPlan.builder_type(),
|
||||||
|
FlightType.BARCAP: BarCapFlightPlan.builder_type(),
|
||||||
|
FlightType.CAS: CasFlightPlan.builder_type(),
|
||||||
|
FlightType.DEAD: DeadFlightPlan.builder_type(),
|
||||||
|
FlightType.ESCORT: EscortFlightPlan.builder_type(),
|
||||||
|
FlightType.OCA_AIRCRAFT: OcaAircraftFlightPlan.builder_type(),
|
||||||
|
FlightType.OCA_RUNWAY: OcaRunwayFlightPlan.builder_type(),
|
||||||
|
FlightType.SEAD: SeadFlightPlan.builder_type(),
|
||||||
|
FlightType.SEAD_ESCORT: EscortFlightPlan.builder_type(),
|
||||||
|
FlightType.STRIKE: StrikeFlightPlan.builder_type(),
|
||||||
|
FlightType.SWEEP: SweepFlightPlan.builder_type(),
|
||||||
|
FlightType.TARCAP: TarCapFlightPlan.builder_type(),
|
||||||
|
FlightType.AEWC: AewcFlightPlan.builder_type(),
|
||||||
|
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
||||||
|
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||||
|
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return builder_dict[flight.flight_type]
|
||||||
|
except KeyError as ex:
|
||||||
|
raise PlanningError(
|
||||||
|
f"{flight.flight_type} flight plan generation not implemented"
|
||||||
|
) from ex
|
||||||
@ -11,6 +11,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
|
from game.utils import Speed, meters
|
||||||
|
from .flightplan import FlightPlan
|
||||||
from .formation import FormationFlightPlan, FormationLayout
|
from .formation import FormationFlightPlan, FormationLayout
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
from .planningerror import PlanningError
|
from .planningerror import PlanningError
|
||||||
@ -151,10 +152,11 @@ class FormationAttackLayout(FormationLayout):
|
|||||||
yield self.bullseye
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
|
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
||||||
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
||||||
|
|
||||||
|
|
||||||
class FormationAttackBuilder(IBuilder, ABC):
|
class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
||||||
def _build(
|
def _build(
|
||||||
self,
|
self,
|
||||||
ingress_type: FlightWaypointType,
|
ingress_type: FlightWaypointType,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING
|
from typing import Any, Generic, TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
|
from .flightplan import FlightPlan, Layout
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
@ -10,16 +12,26 @@ if TYPE_CHECKING:
|
|||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from ..flight import Flight
|
from ..flight import Flight
|
||||||
from ..package import Package
|
from ..package import Package
|
||||||
from .flightplan import Layout
|
|
||||||
|
|
||||||
|
|
||||||
class IBuilder(ABC):
|
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[Any])
|
||||||
def __init__(self, flight: Flight, theater: ConflictTheater) -> None:
|
LayoutT = TypeVar("LayoutT", bound=Layout)
|
||||||
|
|
||||||
|
|
||||||
|
class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
||||||
|
def __init__(self, flight: Flight) -> None:
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.theater = theater
|
|
||||||
|
@property
|
||||||
|
def theater(self) -> ConflictTheater:
|
||||||
|
return self.flight.departure.theater
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build(self) -> Layout:
|
def layout(self) -> LayoutT:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build(self) -> FlightPlanT:
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -19,8 +19,8 @@ class OcaAircraftFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, Airfield):
|
if not isinstance(location, Airfield):
|
||||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
||||||
|
|
||||||
|
def build(self) -> OcaAircraftFlightPlan:
|
||||||
|
return OcaAircraftFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -19,8 +19,8 @@ class OcaRunwayFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, Airfield):
|
if not isinstance(location, Airfield):
|
||||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
||||||
|
|
||||||
|
def build(self) -> OcaRunwayFlightPlan:
|
||||||
|
return OcaRunwayFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -6,65 +6,15 @@ from typing import Type
|
|||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.utils import Distance, Heading, feet, meters
|
from game.utils import Distance, Heading, feet, meters
|
||||||
|
from .ibuilder import IBuilder
|
||||||
from .patrolling import PatrollingLayout
|
from .patrolling import PatrollingLayout
|
||||||
from .theaterrefueling import (
|
from .refuelingflightplan import RefuelingFlightPlan
|
||||||
Builder as TheaterRefuelingBuilder,
|
|
||||||
TheaterRefuelingFlightPlan,
|
|
||||||
)
|
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
|
|
||||||
|
|
||||||
class Builder(TheaterRefuelingBuilder):
|
class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||||
def build(self) -> PatrollingLayout:
|
|
||||||
package_waypoints = self.package.waypoints
|
|
||||||
assert package_waypoints is not None
|
|
||||||
|
|
||||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
|
||||||
|
|
||||||
racetrack_center = package_waypoints.refuel
|
|
||||||
|
|
||||||
split_heading = Heading.from_degrees(
|
|
||||||
racetrack_center.heading_between_point(package_waypoints.split)
|
|
||||||
)
|
|
||||||
home_heading = split_heading.opposite
|
|
||||||
|
|
||||||
racetrack_start = racetrack_center.point_from_heading(
|
|
||||||
split_heading.degrees, racetrack_half_distance
|
|
||||||
)
|
|
||||||
|
|
||||||
racetrack_end = racetrack_center.point_from_heading(
|
|
||||||
home_heading.degrees, racetrack_half_distance
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
|
||||||
|
|
||||||
tanker_type = self.flight.unit_type
|
|
||||||
if tanker_type.patrol_altitude is not None:
|
|
||||||
altitude = tanker_type.patrol_altitude
|
|
||||||
else:
|
|
||||||
altitude = feet(21000)
|
|
||||||
|
|
||||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
|
||||||
|
|
||||||
return PatrollingLayout(
|
|
||||||
departure=builder.takeoff(self.flight.departure),
|
|
||||||
nav_to=builder.nav_path(
|
|
||||||
self.flight.departure.position, racetrack_start, altitude
|
|
||||||
),
|
|
||||||
nav_from=builder.nav_path(
|
|
||||||
racetrack_end, self.flight.arrival.position, altitude
|
|
||||||
),
|
|
||||||
patrol_start=racetrack[0],
|
|
||||||
patrol_end=racetrack[1],
|
|
||||||
arrival=builder.land(self.flight.arrival),
|
|
||||||
divert=builder.divert(self.flight.divert),
|
|
||||||
bullseye=builder.bullseye(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
@ -122,3 +72,54 @@ class PackageRefuelingFlightPlan(TheaterRefuelingFlightPlan):
|
|||||||
+ delay_split_to_refuel
|
+ delay_split_to_refuel
|
||||||
- timedelta(minutes=1.5)
|
- timedelta(minutes=1.5)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
||||||
|
def layout(self) -> PatrollingLayout:
|
||||||
|
package_waypoints = self.package.waypoints
|
||||||
|
assert package_waypoints is not None
|
||||||
|
|
||||||
|
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||||
|
|
||||||
|
racetrack_center = package_waypoints.refuel
|
||||||
|
|
||||||
|
split_heading = Heading.from_degrees(
|
||||||
|
racetrack_center.heading_between_point(package_waypoints.split)
|
||||||
|
)
|
||||||
|
home_heading = split_heading.opposite
|
||||||
|
|
||||||
|
racetrack_start = racetrack_center.point_from_heading(
|
||||||
|
split_heading.degrees, racetrack_half_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
racetrack_end = racetrack_center.point_from_heading(
|
||||||
|
home_heading.degrees, racetrack_half_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|
||||||
|
tanker_type = self.flight.unit_type
|
||||||
|
if tanker_type.patrol_altitude is not None:
|
||||||
|
altitude = tanker_type.patrol_altitude
|
||||||
|
else:
|
||||||
|
altitude = feet(21000)
|
||||||
|
|
||||||
|
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||||
|
|
||||||
|
return PatrollingLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
self.flight.departure.position, racetrack_start, altitude
|
||||||
|
),
|
||||||
|
nav_from=builder.nav_path(
|
||||||
|
racetrack_end, self.flight.arrival.position, altitude
|
||||||
|
),
|
||||||
|
patrol_start=racetrack[0],
|
||||||
|
patrol_end=racetrack[1],
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self) -> PackageRefuelingFlightPlan:
|
||||||
|
return PackageRefuelingFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
20
game/ato/flightplans/refuelingflightplan.py
Normal file
20
game/ato/flightplans/refuelingflightplan.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
from game.utils import Distance, Speed, knots, meters
|
||||||
|
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||||
|
|
||||||
|
|
||||||
|
class RefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout], ABC):
|
||||||
|
@property
|
||||||
|
def patrol_speed(self) -> Speed:
|
||||||
|
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
||||||
|
if self.flight.unit_type.patrol_speed is not None:
|
||||||
|
return self.flight.unit_type.patrol_speed
|
||||||
|
# ~280 knots IAS at 21000.
|
||||||
|
return knots(400)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def engagement_distance(self) -> Distance:
|
||||||
|
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||||
|
# No harm in setting this, but we ought to clean up a bit.
|
||||||
|
return meters(0)
|
||||||
@ -15,42 +15,6 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
|
||||||
def build(self) -> RtbLayout:
|
|
||||||
if not isinstance(self.flight.state, InFlight):
|
|
||||||
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
|
|
||||||
|
|
||||||
current_position = self.flight.state.estimate_position()
|
|
||||||
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
|
||||||
|
|
||||||
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
|
||||||
altitude = (
|
|
||||||
feet(1500)
|
|
||||||
if altitude_is_agl
|
|
||||||
else self.flight.unit_type.preferred_patrol_altitude
|
|
||||||
)
|
|
||||||
builder = WaypointBuilder(self.flight, self.flight.coalition)
|
|
||||||
abort_point = builder.nav(
|
|
||||||
current_position, current_altitude, altitude_reference == "RADIO"
|
|
||||||
)
|
|
||||||
abort_point.name = "ABORT AND RTB"
|
|
||||||
abort_point.pretty_name = "Abort and RTB"
|
|
||||||
abort_point.description = "Abort mission and return to base"
|
|
||||||
return RtbLayout(
|
|
||||||
departure=builder.takeoff(self.flight.departure),
|
|
||||||
abort_location=abort_point,
|
|
||||||
nav_to_destination=builder.nav_path(
|
|
||||||
current_position,
|
|
||||||
self.flight.arrival.position,
|
|
||||||
altitude,
|
|
||||||
altitude_is_agl,
|
|
||||||
),
|
|
||||||
arrival=builder.land(self.flight.arrival),
|
|
||||||
divert=builder.divert(self.flight.divert),
|
|
||||||
bullseye=builder.bullseye(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class RtbLayout(StandardLayout):
|
class RtbLayout(StandardLayout):
|
||||||
abort_location: FlightWaypoint
|
abort_location: FlightWaypoint
|
||||||
@ -88,3 +52,42 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
|||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> timedelta:
|
||||||
return timedelta()
|
return timedelta()
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||||
|
def layout(self) -> RtbLayout:
|
||||||
|
if not isinstance(self.flight.state, InFlight):
|
||||||
|
raise RuntimeError(f"Cannot abort {self} because it is not in flight")
|
||||||
|
|
||||||
|
current_position = self.flight.state.estimate_position()
|
||||||
|
current_altitude, altitude_reference = self.flight.state.estimate_altitude()
|
||||||
|
|
||||||
|
altitude_is_agl = self.flight.unit_type.dcs_unit_type.helicopter
|
||||||
|
altitude = (
|
||||||
|
feet(1500)
|
||||||
|
if altitude_is_agl
|
||||||
|
else self.flight.unit_type.preferred_patrol_altitude
|
||||||
|
)
|
||||||
|
builder = WaypointBuilder(self.flight, self.flight.coalition)
|
||||||
|
abort_point = builder.nav(
|
||||||
|
current_position, current_altitude, altitude_reference == "RADIO"
|
||||||
|
)
|
||||||
|
abort_point.name = "ABORT AND RTB"
|
||||||
|
abort_point.pretty_name = "Abort and RTB"
|
||||||
|
abort_point.description = "Abort mission and return to base"
|
||||||
|
return RtbLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
abort_location=abort_point,
|
||||||
|
nav_to_destination=builder.nav_path(
|
||||||
|
current_position,
|
||||||
|
self.flight.arrival.position,
|
||||||
|
altitude,
|
||||||
|
altitude_is_agl,
|
||||||
|
),
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self) -> RtbFlightPlan:
|
||||||
|
return RtbFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -8,14 +8,10 @@ from .formationattack import (
|
|||||||
FormationAttackFlightPlan,
|
FormationAttackFlightPlan,
|
||||||
FormationAttackLayout,
|
FormationAttackLayout,
|
||||||
)
|
)
|
||||||
from .. import Flight
|
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
|
|
||||||
|
|
||||||
class SeadFlightPlan(FormationAttackFlightPlan):
|
class SeadFlightPlan(FormationAttackFlightPlan):
|
||||||
def __init__(self, flight: Flight, layout: FormationAttackLayout) -> None:
|
|
||||||
super().__init__(flight, layout)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
@ -25,6 +21,9 @@ class SeadFlightPlan(FormationAttackFlightPlan):
|
|||||||
return timedelta(minutes=1)
|
return timedelta(minutes=1)
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
return self._build(FlightWaypointType.INGRESS_SEAD)
|
return self._build(FlightWaypointType.INGRESS_SEAD)
|
||||||
|
|
||||||
|
def build(self) -> SeadFlightPlan:
|
||||||
|
return SeadFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -19,8 +19,8 @@ class StrikeFlightPlan(FormationAttackFlightPlan):
|
|||||||
return Builder
|
return Builder
|
||||||
|
|
||||||
|
|
||||||
class Builder(FormationAttackBuilder):
|
class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
|
||||||
def build(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, TheaterGroundObject):
|
if not isinstance(location, TheaterGroundObject):
|
||||||
@ -31,3 +31,6 @@ class Builder(FormationAttackBuilder):
|
|||||||
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
|
targets.append(StrikeTarget(f"{unit.type.id} #{idx}", unit))
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
||||||
|
|
||||||
|
def build(self) -> StrikeFlightPlan:
|
||||||
|
return StrikeFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -17,57 +17,6 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
|
||||||
def build(self) -> SweepLayout:
|
|
||||||
assert self.package.waypoints is not None
|
|
||||||
target = self.package.target.position
|
|
||||||
heading = Heading.from_degrees(
|
|
||||||
self.package.waypoints.join.heading_between_point(target)
|
|
||||||
)
|
|
||||||
start_pos = target.point_from_heading(
|
|
||||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
|
||||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
|
||||||
|
|
||||||
hold = builder.hold(self._hold_point())
|
|
||||||
|
|
||||||
refuel = None
|
|
||||||
|
|
||||||
if self.package.waypoints is not None:
|
|
||||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
|
||||||
|
|
||||||
return SweepLayout(
|
|
||||||
departure=builder.takeoff(self.flight.departure),
|
|
||||||
hold=hold,
|
|
||||||
nav_to=builder.nav_path(
|
|
||||||
hold.position, start.position, self.doctrine.ingress_altitude
|
|
||||||
),
|
|
||||||
nav_from=builder.nav_path(
|
|
||||||
end.position,
|
|
||||||
self.flight.arrival.position,
|
|
||||||
self.doctrine.ingress_altitude,
|
|
||||||
),
|
|
||||||
sweep_start=start,
|
|
||||||
sweep_end=end,
|
|
||||||
refuel=refuel,
|
|
||||||
arrival=builder.land(self.flight.arrival),
|
|
||||||
divert=builder.divert(self.flight.divert),
|
|
||||||
bullseye=builder.bullseye(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _hold_point(self) -> Point:
|
|
||||||
assert self.package.waypoints is not None
|
|
||||||
origin = self.flight.departure.position
|
|
||||||
target = self.package.target.position
|
|
||||||
join = self.package.waypoints.join
|
|
||||||
ip = self.package.waypoints.ingress
|
|
||||||
return HoldZoneGeometry(
|
|
||||||
target, origin, ip, join, self.coalition, self.theater
|
|
||||||
).find_best_hold_point()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SweepLayout(LoiterLayout):
|
class SweepLayout(LoiterLayout):
|
||||||
nav_to: list[FlightWaypoint]
|
nav_to: list[FlightWaypoint]
|
||||||
@ -145,3 +94,57 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
|
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> timedelta:
|
||||||
return self.sweep_end_time
|
return self.sweep_end_time
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||||
|
def layout(self) -> SweepLayout:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
target = self.package.target.position
|
||||||
|
heading = Heading.from_degrees(
|
||||||
|
self.package.waypoints.join.heading_between_point(target)
|
||||||
|
)
|
||||||
|
start_pos = target.point_from_heading(
|
||||||
|
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||||
|
|
||||||
|
hold = builder.hold(self._hold_point())
|
||||||
|
|
||||||
|
refuel = None
|
||||||
|
|
||||||
|
if self.package.waypoints is not None:
|
||||||
|
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||||
|
|
||||||
|
return SweepLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
hold=hold,
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
hold.position, start.position, self.doctrine.ingress_altitude
|
||||||
|
),
|
||||||
|
nav_from=builder.nav_path(
|
||||||
|
end.position,
|
||||||
|
self.flight.arrival.position,
|
||||||
|
self.doctrine.ingress_altitude,
|
||||||
|
),
|
||||||
|
sweep_start=start,
|
||||||
|
sweep_end=end,
|
||||||
|
refuel=refuel,
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _hold_point(self) -> Point:
|
||||||
|
assert self.package.waypoints is not None
|
||||||
|
origin = self.flight.departure.position
|
||||||
|
target = self.package.target.position
|
||||||
|
join = self.package.waypoints.join
|
||||||
|
ip = self.package.waypoints.ingress
|
||||||
|
return HoldZoneGeometry(
|
||||||
|
target, origin, ip, join, self.coalition, self.theater
|
||||||
|
).find_best_hold_point()
|
||||||
|
|
||||||
|
def build(self) -> SweepFlightPlan:
|
||||||
|
return SweepFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -15,44 +15,6 @@ if TYPE_CHECKING:
|
|||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class Builder(CapBuilder):
|
|
||||||
def build(self) -> TarCapLayout:
|
|
||||||
location = self.package.target
|
|
||||||
|
|
||||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
|
||||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
|
||||||
patrol_alt = max(
|
|
||||||
self.doctrine.min_patrol_altitude,
|
|
||||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
|
||||||
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
|
||||||
|
|
||||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
|
||||||
|
|
||||||
refuel = None
|
|
||||||
|
|
||||||
if self.package.waypoints is not None:
|
|
||||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
|
||||||
|
|
||||||
return TarCapLayout(
|
|
||||||
departure=builder.takeoff(self.flight.departure),
|
|
||||||
nav_to=builder.nav_path(
|
|
||||||
self.flight.departure.position, orbit0p, patrol_alt
|
|
||||||
),
|
|
||||||
nav_from=builder.nav_path(
|
|
||||||
orbit1p, self.flight.arrival.position, patrol_alt
|
|
||||||
),
|
|
||||||
patrol_start=start,
|
|
||||||
patrol_end=end,
|
|
||||||
refuel=refuel,
|
|
||||||
arrival=builder.land(self.flight.arrival),
|
|
||||||
divert=builder.divert(self.flight.divert),
|
|
||||||
bullseye=builder.bullseye(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TarCapLayout(PatrollingLayout):
|
class TarCapLayout(PatrollingLayout):
|
||||||
refuel: FlightWaypoint | None
|
refuel: FlightWaypoint | None
|
||||||
@ -124,3 +86,44 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
if end is not None:
|
if end is not None:
|
||||||
return end
|
return end
|
||||||
return super().patrol_end_time
|
return super().patrol_end_time
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
||||||
|
def layout(self) -> TarCapLayout:
|
||||||
|
location = self.package.target
|
||||||
|
|
||||||
|
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||||
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
|
patrol_alt = max(
|
||||||
|
self.doctrine.min_patrol_altitude,
|
||||||
|
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
orbit0p, orbit1p = self.cap_racetrack_for_objective(location, barcap=False)
|
||||||
|
|
||||||
|
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||||
|
|
||||||
|
refuel = None
|
||||||
|
|
||||||
|
if self.package.waypoints is not None:
|
||||||
|
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||||
|
|
||||||
|
return TarCapLayout(
|
||||||
|
departure=builder.takeoff(self.flight.departure),
|
||||||
|
nav_to=builder.nav_path(
|
||||||
|
self.flight.departure.position, orbit0p, patrol_alt
|
||||||
|
),
|
||||||
|
nav_from=builder.nav_path(
|
||||||
|
orbit1p, self.flight.arrival.position, patrol_alt
|
||||||
|
),
|
||||||
|
patrol_start=start,
|
||||||
|
patrol_end=end,
|
||||||
|
refuel=refuel,
|
||||||
|
arrival=builder.land(self.flight.arrival),
|
||||||
|
divert=builder.divert(self.flight.divert),
|
||||||
|
bullseye=builder.bullseye(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self) -> TarCapFlightPlan:
|
||||||
|
return TarCapFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@ -3,14 +3,25 @@ from __future__ import annotations
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
from game.utils import Heading, feet, meters, nautical_miles
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
from .patrolling import PatrollingLayout
|
||||||
|
from .refuelingflightplan import RefuelingFlightPlan
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder):
|
class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
||||||
def build(self) -> PatrollingLayout:
|
@staticmethod
|
||||||
|
def builder_type() -> Type[Builder]:
|
||||||
|
return Builder
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patrol_duration(self) -> timedelta:
|
||||||
|
return timedelta(hours=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||||
|
def layout(self) -> PatrollingLayout:
|
||||||
racetrack_half_distance = nautical_miles(20).meters
|
racetrack_half_distance = nautical_miles(20).meters
|
||||||
|
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
@ -68,26 +79,5 @@ class Builder(IBuilder):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def build(self) -> TheaterRefuelingFlightPlan:
|
||||||
class TheaterRefuelingFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
return TheaterRefuelingFlightPlan(self.flight, self.layout())
|
||||||
@staticmethod
|
|
||||||
def builder_type() -> Type[Builder]:
|
|
||||||
return Builder
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_duration(self) -> timedelta:
|
|
||||||
return timedelta(hours=1)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def patrol_speed(self) -> Speed:
|
|
||||||
# TODO: Could use self.flight.unit_type.preferred_patrol_speed(altitude).
|
|
||||||
if self.flight.unit_type.patrol_speed is not None:
|
|
||||||
return self.flight.unit_type.patrol_speed
|
|
||||||
# ~280 knots IAS at 21000.
|
|
||||||
return knots(400)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def engagement_distance(self) -> Distance:
|
|
||||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
|
||||||
# No harm in setting this, but we ought to clean up a bit.
|
|
||||||
return meters(0)
|
|
||||||
|
|||||||
@ -6,16 +6,17 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from .flightplans.formation import FormationFlightPlan
|
|
||||||
from game.db import Database
|
from game.db import Database
|
||||||
from game.utils import Speed
|
from game.utils import Speed
|
||||||
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight
|
from .flight import Flight
|
||||||
|
from .flightplans.formation import FormationFlightPlan
|
||||||
from .flighttype import FlightType
|
from .flighttype import FlightType
|
||||||
from .packagewaypoints import PackageWaypoints
|
from .packagewaypoints import PackageWaypoints
|
||||||
from .traveltime import TotEstimator
|
from .traveltime import TotEstimator
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.theater import MissionTarget
|
from game.theater import ControlPoint, MissionTarget
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -31,8 +32,6 @@ class Package:
|
|||||||
#: The set of flights in the package.
|
#: The set of flights in the package.
|
||||||
flights: List[Flight] = field(default_factory=list)
|
flights: List[Flight] = field(default_factory=list)
|
||||||
|
|
||||||
delay: int = field(default=0)
|
|
||||||
|
|
||||||
#: True if the package ToT should be reset to ASAP whenever the player makes
|
#: True if the package ToT should be reset to ASAP whenever the player makes
|
||||||
#: a change. This is really a UI property rather than a game property, but
|
#: a change. This is really a UI property rather than a game property, but
|
||||||
#: we want it to persist in the save.
|
#: we want it to persist in the save.
|
||||||
@ -193,6 +192,24 @@ class Package:
|
|||||||
return "OCA Strike"
|
return "OCA Strike"
|
||||||
return str(task)
|
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:
|
def __hash__(self) -> int:
|
||||||
# TODO: Far from perfect. Number packages?
|
# TODO: Far from perfect. Number packages?
|
||||||
return hash(self.target.name)
|
return hash(self.target.name)
|
||||||
|
|||||||
@ -1,8 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
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)
|
@dataclass(frozen=True)
|
||||||
class PackageWaypoints:
|
class PackageWaypoints:
|
||||||
@ -10,3 +20,37 @@ class PackageWaypoints:
|
|||||||
ingress: Point
|
ingress: Point
|
||||||
split: Point
|
split: Point
|
||||||
refuel: 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,
|
||||||
|
)
|
||||||
|
|||||||
@ -107,9 +107,8 @@ class MizCampaignLoader:
|
|||||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||||
|
|
||||||
@staticmethod
|
def control_point_from_airport(self, airport: Airport) -> ControlPoint:
|
||||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())
|
||||||
cp = Airfield(airport, starts_blue=airport.is_blue())
|
|
||||||
|
|
||||||
# Use the unlimited aircraft option to determine if an airfield should
|
# Use the unlimited aircraft option to determine if an airfield should
|
||||||
# be owned by the player when the campaign is "inverted".
|
# be owned by the player when the campaign is "inverted".
|
||||||
@ -252,20 +251,26 @@ class MizCampaignLoader:
|
|||||||
for blue in (False, True):
|
for blue in (False, True):
|
||||||
for group in self.off_map_spawns(blue):
|
for group in self.off_map_spawns(blue):
|
||||||
control_point = OffMapSpawn(
|
control_point = OffMapSpawn(
|
||||||
str(group.name), group.position, starts_blue=blue
|
str(group.name), group.position, self.theater, starts_blue=blue
|
||||||
)
|
)
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for ship in self.carriers(blue):
|
for ship in self.carriers(blue):
|
||||||
control_point = Carrier(ship.name, ship.position, starts_blue=blue)
|
control_point = Carrier(
|
||||||
|
ship.name, ship.position, self.theater, starts_blue=blue
|
||||||
|
)
|
||||||
control_point.captured_invert = ship.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for ship in self.lhas(blue):
|
for ship in self.lhas(blue):
|
||||||
control_point = Lha(ship.name, ship.position, starts_blue=blue)
|
control_point = Lha(
|
||||||
|
ship.name, ship.position, self.theater, starts_blue=blue
|
||||||
|
)
|
||||||
control_point.captured_invert = ship.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for fob in self.fobs(blue):
|
for fob in self.fobs(blue):
|
||||||
control_point = Fob(str(fob.name), fob.position, starts_blue=blue)
|
control_point = Fob(
|
||||||
|
str(fob.name), fob.position, self.theater, starts_blue=blue
|
||||||
|
)
|
||||||
control_point.captured_invert = fob.late_activation
|
control_point.captured_invert = fob.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
|
|||||||
|
|
||||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.ato.package import Package
|
from game.ato.package import Package
|
||||||
from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission
|
from game.commander.missionproposals import EscortType, ProposedFlight, ProposedMission
|
||||||
@ -191,12 +190,9 @@ class PackageFulfiller:
|
|||||||
# flights that will rendezvous with their package will be affected by
|
# flights that will rendezvous with their package will be affected by
|
||||||
# the other flights in the package. Escorts will not be able to
|
# the other flights in the package. Escorts will not be able to
|
||||||
# contribute to this.
|
# contribute to this.
|
||||||
flight_plan_builder = FlightPlanBuilder(
|
|
||||||
builder.package, self.coalition, self.theater
|
|
||||||
)
|
|
||||||
for flight in builder.package.flights:
|
for flight in builder.package.flights:
|
||||||
with tracer.trace("Flight plan population"):
|
with tracer.trace("Flight plan population"):
|
||||||
flight_plan_builder.populate_flight_plan(flight)
|
flight.recreate_flight_plan()
|
||||||
|
|
||||||
needed_escorts = self.check_needed_escorts(builder)
|
needed_escorts = self.check_needed_escorts(builder)
|
||||||
for escort in escorts:
|
for escort in escorts:
|
||||||
@ -222,7 +218,7 @@ class PackageFulfiller:
|
|||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
if not flight.flight_plan.waypoints:
|
if not flight.flight_plan.waypoints:
|
||||||
with tracer.trace("Flight plan population"):
|
with tracer.trace("Flight plan population"):
|
||||||
flight_plan_builder.populate_flight_plan(flight)
|
flight.recreate_flight_plan()
|
||||||
|
|
||||||
if package.has_players and self.player_missions_asap:
|
if package.has_players and self.player_missions_asap:
|
||||||
package.auto_asap = True
|
package.auto_asap = True
|
||||||
|
|||||||
@ -11,7 +11,6 @@ from faker import Faker
|
|||||||
from game.ato import Flight, FlightType, Package
|
from game.ato import Flight, FlightType, Package
|
||||||
from game.settings import AutoAtoBehavior, Settings
|
from game.settings import AutoAtoBehavior, Settings
|
||||||
from .pilot import Pilot, PilotStatus
|
from .pilot import Pilot, PilotStatus
|
||||||
from ..ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
|
||||||
from ..db.database import Database
|
from ..db.database import Database
|
||||||
from ..utils import meters
|
from ..utils import meters
|
||||||
|
|
||||||
@ -19,7 +18,7 @@ if TYPE_CHECKING:
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.theater import ControlPoint, ConflictTheater, MissionTarget
|
from game.theater import ControlPoint, MissionTarget
|
||||||
from .operatingbases import OperatingBases
|
from .operatingbases import OperatingBases
|
||||||
from .squadrondef import SquadronDef
|
from .squadrondef import SquadronDef
|
||||||
|
|
||||||
@ -335,9 +334,7 @@ class Squadron:
|
|||||||
def arrival(self) -> ControlPoint:
|
def arrival(self) -> ControlPoint:
|
||||||
return self.location if self.destination is None else self.destination
|
return self.location if self.destination is None else self.destination
|
||||||
|
|
||||||
def plan_relocation(
|
def plan_relocation(self, destination: ControlPoint) -> None:
|
||||||
self, destination: ControlPoint, theater: ConflictTheater
|
|
||||||
) -> None:
|
|
||||||
if destination == self.location:
|
if destination == self.location:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Attempted to plan relocation of {self} to current location "
|
f"Attempted to plan relocation of {self} to current location "
|
||||||
@ -356,7 +353,7 @@ class Squadron:
|
|||||||
if not destination.can_operate(self.aircraft):
|
if not destination.can_operate(self.aircraft):
|
||||||
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
||||||
self.destination = destination
|
self.destination = destination
|
||||||
self.replan_ferry_flights(theater)
|
self.replan_ferry_flights()
|
||||||
|
|
||||||
def cancel_relocation(self) -> None:
|
def cancel_relocation(self) -> None:
|
||||||
if self.destination is None:
|
if self.destination is None:
|
||||||
@ -371,9 +368,9 @@ class Squadron:
|
|||||||
self.destination = None
|
self.destination = None
|
||||||
self.cancel_ferry_flights()
|
self.cancel_ferry_flights()
|
||||||
|
|
||||||
def replan_ferry_flights(self, theater: ConflictTheater) -> None:
|
def replan_ferry_flights(self) -> None:
|
||||||
self.cancel_ferry_flights()
|
self.cancel_ferry_flights()
|
||||||
self.plan_ferry_flights(theater)
|
self.plan_ferry_flights()
|
||||||
|
|
||||||
def cancel_ferry_flights(self) -> None:
|
def cancel_ferry_flights(self) -> None:
|
||||||
for package in self.coalition.ato.packages:
|
for package in self.coalition.ato.packages:
|
||||||
@ -384,7 +381,7 @@ class Squadron:
|
|||||||
if not package.flights:
|
if not package.flights:
|
||||||
self.coalition.ato.remove_package(package)
|
self.coalition.ato.remove_package(package)
|
||||||
|
|
||||||
def plan_ferry_flights(self, theater: ConflictTheater) -> None:
|
def plan_ferry_flights(self) -> None:
|
||||||
if self.destination is None:
|
if self.destination is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot plan ferry flights for {self} because there is no destination."
|
f"Cannot plan ferry flights for {self} because there is no destination."
|
||||||
@ -394,17 +391,14 @@ class Squadron:
|
|||||||
return
|
return
|
||||||
|
|
||||||
package = Package(self.destination, self.flight_db)
|
package = Package(self.destination, self.flight_db)
|
||||||
builder = FlightPlanBuilder(package, self.coalition, theater)
|
|
||||||
while remaining:
|
while remaining:
|
||||||
size = min(remaining, self.aircraft.max_group_size)
|
size = min(remaining, self.aircraft.max_group_size)
|
||||||
self.plan_ferry_flight(builder, package, size)
|
self.plan_ferry_flight(package, size)
|
||||||
remaining -= size
|
remaining -= size
|
||||||
package.set_tot_asap()
|
package.set_tot_asap()
|
||||||
self.coalition.ato.add_package(package)
|
self.coalition.ato.add_package(package)
|
||||||
|
|
||||||
def plan_ferry_flight(
|
def plan_ferry_flight(self, package: Package, size: int) -> None:
|
||||||
self, builder: FlightPlanBuilder, package: Package, size: int
|
|
||||||
) -> None:
|
|
||||||
start_type = self.location.required_aircraft_start_type
|
start_type = self.location.required_aircraft_start_type
|
||||||
if start_type is None:
|
if start_type is None:
|
||||||
start_type = self.settings.default_start_type
|
start_type = self.settings.default_start_type
|
||||||
@ -419,7 +413,7 @@ class Squadron:
|
|||||||
divert=None,
|
divert=None,
|
||||||
)
|
)
|
||||||
package.add_flight(flight)
|
package.add_flight(flight)
|
||||||
builder.populate_flight_plan(flight)
|
flight.recreate_flight_plan()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_from(
|
def create_from(
|
||||||
|
|||||||
@ -26,21 +26,21 @@ from typing import (
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
|
||||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
|
||||||
from dcs.unittype import ShipType
|
|
||||||
from dcs.ships import (
|
from dcs.ships import (
|
||||||
CVN_71,
|
CVN_71,
|
||||||
CVN_72,
|
CVN_72,
|
||||||
CVN_73,
|
CVN_73,
|
||||||
CVN_75,
|
CVN_75,
|
||||||
CV_1143_5,
|
CV_1143_5,
|
||||||
KUZNECOW,
|
|
||||||
Stennis,
|
|
||||||
Forrestal,
|
Forrestal,
|
||||||
|
KUZNECOW,
|
||||||
LHA_Tarawa,
|
LHA_Tarawa,
|
||||||
|
Stennis,
|
||||||
Type_071,
|
Type_071,
|
||||||
)
|
)
|
||||||
|
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||||
|
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||||
|
from dcs.unittype import ShipType
|
||||||
|
|
||||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||||
from game.ground_forces.combat_stance import CombatStance
|
from game.ground_forces.combat_stance import CombatStance
|
||||||
@ -56,8 +56,8 @@ from game.sidc import (
|
|||||||
Status,
|
Status,
|
||||||
SymbolSet,
|
SymbolSet,
|
||||||
)
|
)
|
||||||
from game.utils import Distance, Heading, meters
|
|
||||||
from game.theater.presetlocation import PresetLocation
|
from game.theater.presetlocation import PresetLocation
|
||||||
|
from game.utils import Distance, Heading, meters
|
||||||
from .base import Base
|
from .base import Base
|
||||||
from .frontline import FrontLine
|
from .frontline import FrontLine
|
||||||
from .missiontarget import MissionTarget
|
from .missiontarget import MissionTarget
|
||||||
@ -320,6 +320,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
|||||||
name: str,
|
name: str,
|
||||||
position: Point,
|
position: Point,
|
||||||
at: StartingPosition,
|
at: StartingPosition,
|
||||||
|
theater: ConflictTheater,
|
||||||
starts_blue: bool,
|
starts_blue: bool,
|
||||||
cptype: ControlPointType = ControlPointType.AIRBASE,
|
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -327,6 +328,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
|||||||
self.id = uuid.uuid4()
|
self.id = uuid.uuid4()
|
||||||
self.full_name = name
|
self.full_name = name
|
||||||
self.at = at
|
self.at = at
|
||||||
|
self.theater = theater
|
||||||
self.starts_blue = starts_blue
|
self.starts_blue = starts_blue
|
||||||
self.connected_objectives: List[TheaterGroundObject] = []
|
self.connected_objectives: List[TheaterGroundObject] = []
|
||||||
self.preset_locations = PresetLocations()
|
self.preset_locations = PresetLocations()
|
||||||
@ -1039,11 +1041,14 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Airfield(ControlPoint):
|
class Airfield(ControlPoint):
|
||||||
def __init__(self, airport: Airport, starts_blue: bool) -> None:
|
def __init__(
|
||||||
|
self, airport: Airport, theater: ConflictTheater, starts_blue: bool
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
airport.name,
|
airport.name,
|
||||||
airport.position,
|
airport.position,
|
||||||
airport,
|
airport,
|
||||||
|
theater,
|
||||||
starts_blue,
|
starts_blue,
|
||||||
cptype=ControlPointType.AIRBASE,
|
cptype=ControlPointType.AIRBASE,
|
||||||
)
|
)
|
||||||
@ -1236,9 +1241,16 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Carrier(NavalControlPoint):
|
class Carrier(NavalControlPoint):
|
||||||
def __init__(self, name: str, at: Point, starts_blue: bool):
|
def __init__(
|
||||||
|
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||||
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name, at, at, starts_blue, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP
|
name,
|
||||||
|
at,
|
||||||
|
at,
|
||||||
|
theater,
|
||||||
|
starts_blue,
|
||||||
|
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1275,8 +1287,12 @@ class Carrier(NavalControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class Lha(NavalControlPoint):
|
class Lha(NavalControlPoint):
|
||||||
def __init__(self, name: str, at: Point, starts_blue: bool):
|
def __init__(
|
||||||
super().__init__(name, at, at, starts_blue, cptype=ControlPointType.LHA_GROUP)
|
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||||
@ -1305,9 +1321,16 @@ class OffMapSpawn(ControlPoint):
|
|||||||
def runway_is_operational(self) -> bool:
|
def runway_is_operational(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __init__(self, name: str, position: Point, starts_blue: bool):
|
def __init__(
|
||||||
|
self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool
|
||||||
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name, position, position, starts_blue, cptype=ControlPointType.OFF_MAP
|
name,
|
||||||
|
position,
|
||||||
|
position,
|
||||||
|
theater,
|
||||||
|
starts_blue,
|
||||||
|
cptype=ControlPointType.OFF_MAP,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1364,8 +1387,12 @@ class OffMapSpawn(ControlPoint):
|
|||||||
|
|
||||||
|
|
||||||
class Fob(ControlPoint):
|
class Fob(ControlPoint):
|
||||||
def __init__(self, name: str, at: Point, starts_blue: bool):
|
def __init__(
|
||||||
super().__init__(name, at, at, starts_blue, cptype=ControlPointType.FOB)
|
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
|
||||||
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -43,7 +43,6 @@ from dcs.mapping import Point
|
|||||||
from game.ato.ai_flight_planner_db import aircraft_for_task
|
from game.ato.ai_flight_planner_db import aircraft_for_task
|
||||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.ato.package import Package
|
from game.ato.package import Package
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
@ -364,10 +363,7 @@ class AirliftPlanner:
|
|||||||
transfer.transport = transport
|
transfer.transport = transport
|
||||||
|
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
planner = FlightPlanBuilder(
|
flight.recreate_flight_plan()
|
||||||
self.package, self.game.coalition_for(self.for_player), self.game.theater
|
|
||||||
)
|
|
||||||
planner.populate_flight_plan(flight)
|
|
||||||
return flight_size
|
return flight_size
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,25 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Callable, Iterator, Optional
|
from typing import Callable, Iterator, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import (
|
from PySide2.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, Qt
|
||||||
QItemSelectionModel,
|
|
||||||
QModelIndex,
|
|
||||||
Qt,
|
|
||||||
QItemSelection,
|
|
||||||
)
|
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QDialog,
|
|
||||||
QListView,
|
|
||||||
QVBoxLayout,
|
|
||||||
QPushButton,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QListView,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.squadrons import Pilot, Squadron
|
|
||||||
from game.theater import ControlPoint, ConflictTheater
|
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
|
from game.squadrons import Pilot, Squadron
|
||||||
|
from game.theater import ConflictTheater, ControlPoint
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
from qt_ui.delegates import TwoColumnRowDelegate
|
||||||
from qt_ui.errorreporter import report_errors
|
from qt_ui.errorreporter import report_errors
|
||||||
from qt_ui.models import SquadronModel, AtoModel
|
from qt_ui.models import AtoModel, SquadronModel
|
||||||
|
|
||||||
|
|
||||||
class PilotDelegate(TwoColumnRowDelegate):
|
class PilotDelegate(TwoColumnRowDelegate):
|
||||||
@ -144,7 +139,6 @@ class SquadronDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.ato_model = ato_model
|
self.ato_model = ato_model
|
||||||
self.squadron_model = squadron_model
|
self.squadron_model = squadron_model
|
||||||
self.theater = theater
|
|
||||||
|
|
||||||
self.setMinimumSize(1000, 440)
|
self.setMinimumSize(1000, 440)
|
||||||
self.setWindowTitle(str(squadron_model.squadron))
|
self.setWindowTitle(str(squadron_model.squadron))
|
||||||
@ -200,7 +194,7 @@ class SquadronDialog(QDialog):
|
|||||||
if destination is None:
|
if destination is None:
|
||||||
self.squadron.cancel_relocation()
|
self.squadron.cancel_relocation()
|
||||||
else:
|
else:
|
||||||
self.squadron.plan_relocation(destination, self.theater)
|
self.squadron.plan_relocation(destination)
|
||||||
self.ato_model.replace_from_game(player=True)
|
self.ato_model.replace_from_game(player=True)
|
||||||
|
|
||||||
def check_disabled_button_states(
|
def check_disabled_button_states(
|
||||||
|
|||||||
@ -16,7 +16,6 @@ from PySide2.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
|
||||||
from game.ato.flightplans.planningerror import PlanningError
|
from game.ato.flightplans.planningerror import PlanningError
|
||||||
from game.ato.package import Package
|
from game.ato.package import Package
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
@ -181,11 +180,8 @@ class QPackageDialog(QDialog):
|
|||||||
def add_flight(self, flight: Flight) -> None:
|
def add_flight(self, flight: Flight) -> None:
|
||||||
"""Adds the new flight to the package."""
|
"""Adds the new flight to the package."""
|
||||||
self.package_model.add_flight(flight)
|
self.package_model.add_flight(flight)
|
||||||
planner = FlightPlanBuilder(
|
|
||||||
self.package_model.package, self.game.blue, self.game.theater
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
planner.populate_flight_plan(flight)
|
flight.recreate_flight_plan()
|
||||||
self.package_model.update_tot()
|
self.package_model.update_tot()
|
||||||
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
|
EventStream.put_nowait(GameUpdateEvents().new_flight(flight))
|
||||||
except PlanningError as ex:
|
except PlanningError as ex:
|
||||||
|
|||||||
@ -4,7 +4,6 @@ from PySide2.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.ato.flightplans.flightplanbuilder import FlightPlanBuilder
|
|
||||||
from game.ato.flightplans.planningerror import PlanningError
|
from game.ato.flightplans.planningerror import PlanningError
|
||||||
from game.ato.traveltime import TotEstimator
|
from game.ato.traveltime import TotEstimator
|
||||||
from qt_ui.models import PackageModel
|
from qt_ui.models import PackageModel
|
||||||
@ -71,16 +70,10 @@ class FlightAirfieldDisplay(QGroupBox):
|
|||||||
|
|
||||||
self.flight.divert = divert
|
self.flight.divert = divert
|
||||||
try:
|
try:
|
||||||
self.update_flight_plan()
|
self.flight.recreate_flight_plan()
|
||||||
except PlanningError as ex:
|
except PlanningError as ex:
|
||||||
self.flight.divert = old_divert
|
self.flight.divert = old_divert
|
||||||
logging.exception("Could not change divert airfield")
|
logging.exception("Could not change divert airfield")
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Could not update flight plan", str(ex), QMessageBox.Ok
|
self, "Could not update flight plan", str(ex), QMessageBox.Ok
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_flight_plan(self) -> None:
|
|
||||||
planner = FlightPlanBuilder(
|
|
||||||
self.package_model.package, self.game.blue, self.game.theater
|
|
||||||
)
|
|
||||||
planner.populate_flight_plan(self.flight)
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Iterable, List, Optional, Any
|
from typing import Iterable, List, Optional
|
||||||
|
|
||||||
from PySide2.QtCore import Signal
|
from PySide2.QtCore import Signal
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
@ -14,10 +14,9 @@ from PySide2.QtWidgets import (
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
|
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.formationattack import FormationAttackFlightPlan
|
||||||
from game.ato.flightplans.planningerror import PlanningError
|
from game.ato.flightplans.planningerror import PlanningError
|
||||||
|
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.ato.flightwaypoint import FlightWaypoint
|
from game.ato.flightwaypoint import FlightWaypoint
|
||||||
from game.ato.loadouts import Loadout
|
from game.ato.loadouts import Loadout
|
||||||
@ -38,7 +37,6 @@ class QFlightWaypointTab(QFrame):
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.package = package
|
self.package = package
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
|
|
||||||
|
|
||||||
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
|
||||||
@ -139,7 +137,7 @@ class QFlightWaypointTab(QFrame):
|
|||||||
self.on_change()
|
self.on_change()
|
||||||
|
|
||||||
def on_rtb_waypoint(self):
|
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()
|
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)
|
||||||
@ -168,7 +166,7 @@ class QFlightWaypointTab(QFrame):
|
|||||||
if result == QMessageBox.Yes:
|
if result == QMessageBox.Yes:
|
||||||
self.flight.flight_type = task
|
self.flight.flight_type = task
|
||||||
try:
|
try:
|
||||||
self.planner.populate_flight_plan(self.flight)
|
self.flight.recreate_flight_plan()
|
||||||
except PlanningError as ex:
|
except PlanningError as ex:
|
||||||
self.flight.flight_type = original_task
|
self.flight.flight_type = original_task
|
||||||
logging.exception("Could not recreate flight")
|
logging.exception("Could not recreate flight")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user