Implement ferry flights for squadron transfers.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1145
This commit is contained in:
Dan Albert 2021-08-31 22:14:32 -07:00
parent f9f0b429b6
commit 1a4be911c0
8 changed files with 179 additions and 16 deletions

View File

@ -7,7 +7,7 @@ Saves from 4.x are not compatible with 5.0.
* **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region.
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
* **[Campaign]** (WIP) Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/discussions/1550 for details.
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.

View File

@ -11,17 +11,19 @@ from typing import (
from faker import Faker
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior, Settings
from game.squadrons.operatingbases import OperatingBases
from game.squadrons.pilot import Pilot, PilotStatus
from game.squadrons.squadrondef import SquadronDef
from gen.ato import Package
from gen.flights.flight import FlightType, Flight
from gen.flights.flightplan import FlightPlanBuilder
from .pilot import Pilot, PilotStatus
if TYPE_CHECKING:
from game import Game
from game.coalition import Coalition
from gen.flights.flight import FlightType
from game.theater import ControlPoint
from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint, ConflictTheater
from .operatingbases import OperatingBases
from .squadrondef import SquadronDef
@dataclass
@ -312,7 +314,9 @@ class Squadron:
def arrival(self) -> ControlPoint:
return self.location if self.destination is None else self.destination
def plan_relocation(self, destination: ControlPoint) -> None:
def plan_relocation(
self, destination: ControlPoint, theater: ConflictTheater
) -> None:
if destination == self.location:
logging.warning(
f"Attempted to plan relocation of {self} to current location "
@ -331,6 +335,7 @@ class Squadron:
if not destination.can_operate(self.aircraft):
raise RuntimeError(f"{self} cannot operate at {destination}.")
self.destination = destination
self.replan_ferry_flights(theater)
def cancel_relocation(self) -> None:
if self.destination is None:
@ -343,6 +348,55 @@ class Squadron:
if self.expected_size_next_turn >= self.location.unclaimed_parking():
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
self.destination = None
self.cancel_ferry_flights()
def replan_ferry_flights(self, theater: ConflictTheater) -> None:
self.cancel_ferry_flights()
self.plan_ferry_flights(theater)
def cancel_ferry_flights(self) -> None:
for package in self.coalition.ato.packages:
# Copy the list so our iterator remains consistent throughout the removal.
for flight in list(package.flights):
if flight.squadron == self and flight.flight_type is FlightType.FERRY:
package.remove_flight(flight)
flight.return_pilots_and_aircraft()
if not package.flights:
self.coalition.ato.remove_package(package)
def plan_ferry_flights(self, theater: ConflictTheater) -> None:
if self.destination is None:
raise RuntimeError(
f"Cannot plan ferry flights for {self} because there is no destination."
)
package = Package(self.destination)
builder = FlightPlanBuilder(package, self.coalition, theater)
remaining = self.untasked_aircraft
while remaining:
size = min(remaining, self.aircraft.max_group_size)
self.plan_ferry_flight(builder, package, size)
remaining -= size
package.set_tot_asap()
self.coalition.ato.add_package(package)
def plan_ferry_flight(
self, builder: FlightPlanBuilder, package: Package, size: int
) -> None:
start_type = self.location.required_aircraft_start_type
if start_type is None:
start_type = self.settings.default_start_type
flight = Flight(
package,
self.coalition.country_name,
self,
size,
FlightType.FERRY,
start_type,
divert=None,
)
package.add_flight(flight)
builder.populate_flight_plan(flight)
@classmethod
def create_from(

View File

@ -57,6 +57,7 @@ from dcs.task import (
Transport,
WeaponType,
TargetType,
Nothing,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule
@ -1086,6 +1087,23 @@ class AircraftConflictGenerator:
restrict_jettison=True,
)
def configure_ferry(
self,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Nothing.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(flight, group)
@ -1130,6 +1148,8 @@ class AircraftConflictGenerator:
self.configure_oca_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.TRANSPORT:
self.configure_transport(group, package, flight, dynamic_runways)
elif flight_type == FlightType.FERRY:
self.configure_ferry(group, package, flight, dynamic_runways)
else:
self.configure_unknown_task(group, flight)

View File

@ -183,6 +183,7 @@ class Package:
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.FERRY,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.ESCORT,

View File

@ -70,6 +70,7 @@ class FlightType(Enum):
TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling"
FERRY = "Ferry"
def __str__(self) -> str:
return self.value

View File

@ -37,10 +37,8 @@ from game.theater.theatergroundobject import (
NavalGroundObject,
BuildingGroundObject,
)
from game.threatzones import ThreatZones
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan):
return self.package.time_over_target
@dataclass(frozen=True)
class FerryFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_destination
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.land
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for ferries. 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) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True)
class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint]
@ -958,6 +989,8 @@ class FlightPlanBuilder:
return self.generate_transport(flight)
elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight)
elif task == FlightType.FERRY:
return self.generate_ferry(flight)
raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None:
@ -1244,6 +1277,42 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(),
)
def generate_ferry(self, flight: Flight) -> FerryFlightPlan:
"""Generate a ferry flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
if flight.departure == flight.arrival:
raise PlanningError(
f"Cannot plan ferry flight: departure and arrival are both "
f"{flight.departure}"
)
altitude_is_agl = flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(flight, self.coalition)
return FerryFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_destination=builder.nav_path(
flight.departure.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def cap_racetrack_for_objective(
self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]:

View File

@ -20,7 +20,7 @@ from game.squadrons import Squadron
from game.theater import ConflictTheater
from gen.flights.flight import Flight
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel
from qt_ui.models import GameModel, AirWingModel, SquadronModel, AtoModel
from qt_ui.windows.SquadronDialog import SquadronDialog
@ -57,8 +57,14 @@ class SquadronDelegate(TwoColumnRowDelegate):
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel, theater: ConflictTheater) -> None:
def __init__(
self,
ato_model: AtoModel,
air_wing_model: AirWingModel,
theater: ConflictTheater,
) -> None:
super().__init__()
self.ato_model = ato_model
self.air_wing_model = air_wing_model
self.theater = theater
self.dialog: Optional[SquadronDialog] = None
@ -78,6 +84,7 @@ class SquadronList(QListView):
if not index.isValid():
return
self.dialog = SquadronDialog(
self.ato_model,
SquadronModel(self.air_wing_model.squadron_at_index(index)),
self.theater,
self,
@ -199,7 +206,11 @@ class AirWingTabs(QTabWidget):
super().__init__()
self.addTab(
SquadronList(game_model.blue_air_wing_model, game_model.game.theater),
SquadronList(
game_model.ato_model,
game_model.blue_air_wing_model,
game_model.game.theater,
),
"Squadrons",
)
self.addTab(AirInventoryView(game_model), "Inventory")

View File

@ -24,7 +24,7 @@ from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType
from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.errorreporter import report_errors
from qt_ui.models import SquadronModel
from qt_ui.models import SquadronModel, AtoModel
class PilotDelegate(TwoColumnRowDelegate):
@ -135,10 +135,16 @@ class SquadronDialog(QDialog):
"""Dialog window showing a squadron."""
def __init__(
self, squadron_model: SquadronModel, theater: ConflictTheater, parent
self,
ato_model: AtoModel,
squadron_model: SquadronModel,
theater: ConflictTheater,
parent,
) -> None:
super().__init__(parent)
self.ato_model = ato_model
self.squadron_model = squadron_model
self.theater = theater
self.setMinimumSize(1000, 440)
self.setWindowTitle(str(squadron_model.squadron))
@ -194,7 +200,8 @@ class SquadronDialog(QDialog):
if destination is None:
self.squadron.cancel_relocation()
else:
self.squadron.plan_relocation(destination)
self.squadron.plan_relocation(destination, self.theater)
self.ato_model.replace_from_game(player=True)
def check_disabled_button_states(
self, button: QPushButton, index: QModelIndex