mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Implement ferry flights for squadron transfers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1145
This commit is contained in:
parent
f9f0b429b6
commit
1a4be911c0
@ -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]** 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]** 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]** 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]** 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]** 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.
|
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
||||||
|
|||||||
@ -11,17 +11,19 @@ from typing import (
|
|||||||
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
from game.settings import AutoAtoBehavior, Settings
|
from game.settings import AutoAtoBehavior, Settings
|
||||||
from game.squadrons.operatingbases import OperatingBases
|
from gen.ato import Package
|
||||||
from game.squadrons.pilot import Pilot, PilotStatus
|
from gen.flights.flight import FlightType, Flight
|
||||||
from game.squadrons.squadrondef import SquadronDef
|
from gen.flights.flightplan import FlightPlanBuilder
|
||||||
|
from .pilot import Pilot, PilotStatus
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
from gen.flights.flight import FlightType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint, ConflictTheater
|
||||||
|
from .operatingbases import OperatingBases
|
||||||
|
from .squadrondef import SquadronDef
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -312,7 +314,9 @@ 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(self, destination: ControlPoint) -> None:
|
def plan_relocation(
|
||||||
|
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 "
|
||||||
@ -331,6 +335,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)
|
||||||
|
|
||||||
def cancel_relocation(self) -> None:
|
def cancel_relocation(self) -> None:
|
||||||
if self.destination is None:
|
if self.destination is None:
|
||||||
@ -343,6 +348,55 @@ class Squadron:
|
|||||||
if self.expected_size_next_turn >= self.location.unclaimed_parking():
|
if self.expected_size_next_turn >= self.location.unclaimed_parking():
|
||||||
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
|
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
|
||||||
self.destination = None
|
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
|
@classmethod
|
||||||
def create_from(
|
def create_from(
|
||||||
|
|||||||
@ -57,6 +57,7 @@ from dcs.task import (
|
|||||||
Transport,
|
Transport,
|
||||||
WeaponType,
|
WeaponType,
|
||||||
TargetType,
|
TargetType,
|
||||||
|
Nothing,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||||
@ -1086,6 +1087,23 @@ class AircraftConflictGenerator:
|
|||||||
restrict_jettison=True,
|
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:
|
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||||
self.configure_behavior(flight, group)
|
self.configure_behavior(flight, group)
|
||||||
@ -1130,6 +1148,8 @@ class AircraftConflictGenerator:
|
|||||||
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
||||||
elif flight_type == FlightType.TRANSPORT:
|
elif flight_type == FlightType.TRANSPORT:
|
||||||
self.configure_transport(group, package, flight, dynamic_runways)
|
self.configure_transport(group, package, flight, dynamic_runways)
|
||||||
|
elif flight_type == FlightType.FERRY:
|
||||||
|
self.configure_ferry(group, package, flight, dynamic_runways)
|
||||||
else:
|
else:
|
||||||
self.configure_unknown_task(group, flight)
|
self.configure_unknown_task(group, flight)
|
||||||
|
|
||||||
|
|||||||
@ -183,6 +183,7 @@ class Package:
|
|||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
FlightType.AEWC,
|
FlightType.AEWC,
|
||||||
|
FlightType.FERRY,
|
||||||
FlightType.REFUELING,
|
FlightType.REFUELING,
|
||||||
FlightType.SWEEP,
|
FlightType.SWEEP,
|
||||||
FlightType.ESCORT,
|
FlightType.ESCORT,
|
||||||
|
|||||||
@ -70,6 +70,7 @@ class FlightType(Enum):
|
|||||||
TRANSPORT = "Transport"
|
TRANSPORT = "Transport"
|
||||||
SEAD_ESCORT = "SEAD Escort"
|
SEAD_ESCORT = "SEAD Escort"
|
||||||
REFUELING = "Refueling"
|
REFUELING = "Refueling"
|
||||||
|
FERRY = "Ferry"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
|
|||||||
@ -37,10 +37,8 @@ from game.theater.theatergroundobject import (
|
|||||||
NavalGroundObject,
|
NavalGroundObject,
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
|
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
|
||||||
|
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
from .traveltime import GroundSpeed, TravelTime
|
from .traveltime import GroundSpeed, TravelTime
|
||||||
@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan):
|
|||||||
return self.package.time_over_target
|
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)
|
@dataclass(frozen=True)
|
||||||
class CustomFlightPlan(FlightPlan):
|
class CustomFlightPlan(FlightPlan):
|
||||||
custom_waypoints: List[FlightWaypoint]
|
custom_waypoints: List[FlightWaypoint]
|
||||||
@ -958,6 +989,8 @@ class FlightPlanBuilder:
|
|||||||
return self.generate_transport(flight)
|
return self.generate_transport(flight)
|
||||||
elif task == FlightType.REFUELING:
|
elif task == FlightType.REFUELING:
|
||||||
return self.generate_refueling_racetrack(flight)
|
return self.generate_refueling_racetrack(flight)
|
||||||
|
elif task == FlightType.FERRY:
|
||||||
|
return self.generate_ferry(flight)
|
||||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||||
|
|
||||||
def regenerate_package_waypoints(self) -> None:
|
def regenerate_package_waypoints(self) -> None:
|
||||||
@ -1244,6 +1277,42 @@ class FlightPlanBuilder:
|
|||||||
bullseye=builder.bullseye(),
|
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(
|
def cap_racetrack_for_objective(
|
||||||
self, location: MissionTarget, barcap: bool
|
self, location: MissionTarget, barcap: bool
|
||||||
) -> Tuple[Point, Point]:
|
) -> Tuple[Point, Point]:
|
||||||
|
|||||||
@ -20,7 +20,7 @@ from game.squadrons import Squadron
|
|||||||
from game.theater import ConflictTheater
|
from game.theater import ConflictTheater
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.delegates import TwoColumnRowDelegate
|
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
|
from qt_ui.windows.SquadronDialog import SquadronDialog
|
||||||
|
|
||||||
|
|
||||||
@ -57,8 +57,14 @@ class SquadronDelegate(TwoColumnRowDelegate):
|
|||||||
class SquadronList(QListView):
|
class SquadronList(QListView):
|
||||||
"""List view for displaying the air wing's squadrons."""
|
"""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__()
|
super().__init__()
|
||||||
|
self.ato_model = ato_model
|
||||||
self.air_wing_model = air_wing_model
|
self.air_wing_model = air_wing_model
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.dialog: Optional[SquadronDialog] = None
|
self.dialog: Optional[SquadronDialog] = None
|
||||||
@ -78,6 +84,7 @@ class SquadronList(QListView):
|
|||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return
|
return
|
||||||
self.dialog = SquadronDialog(
|
self.dialog = SquadronDialog(
|
||||||
|
self.ato_model,
|
||||||
SquadronModel(self.air_wing_model.squadron_at_index(index)),
|
SquadronModel(self.air_wing_model.squadron_at_index(index)),
|
||||||
self.theater,
|
self.theater,
|
||||||
self,
|
self,
|
||||||
@ -199,7 +206,11 @@ class AirWingTabs(QTabWidget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.addTab(
|
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",
|
"Squadrons",
|
||||||
)
|
)
|
||||||
self.addTab(AirInventoryView(game_model), "Inventory")
|
self.addTab(AirInventoryView(game_model), "Inventory")
|
||||||
|
|||||||
@ -24,7 +24,7 @@ from game.theater import ControlPoint, ConflictTheater
|
|||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
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
|
from qt_ui.models import SquadronModel, AtoModel
|
||||||
|
|
||||||
|
|
||||||
class PilotDelegate(TwoColumnRowDelegate):
|
class PilotDelegate(TwoColumnRowDelegate):
|
||||||
@ -135,10 +135,16 @@ class SquadronDialog(QDialog):
|
|||||||
"""Dialog window showing a squadron."""
|
"""Dialog window showing a squadron."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, squadron_model: SquadronModel, theater: ConflictTheater, parent
|
self,
|
||||||
|
ato_model: AtoModel,
|
||||||
|
squadron_model: SquadronModel,
|
||||||
|
theater: ConflictTheater,
|
||||||
|
parent,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
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))
|
||||||
@ -194,7 +200,8 @@ 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.squadron.plan_relocation(destination, self.theater)
|
||||||
|
self.ato_model.replace_from_game(player=True)
|
||||||
|
|
||||||
def check_disabled_button_states(
|
def check_disabled_button_states(
|
||||||
self, button: QPushButton, index: QModelIndex
|
self, button: QPushButton, index: QModelIndex
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user