Reorganize flight planning.

Previously we were trying to make every potential flight plan look
just like a strike mission's flight plan. This led to a lot of special
case behavior in several places that was causing us to misplan TOTs.

I've reorganized this such that there's now an explicit `FlightPlan`
class, and any specialized behavior is handled by the subclasses.

I've also taken the opportunity to alter the behavior of CAS and
front-line CAP missions. These no longer involve the usual formation
waypoints. Instead the CAP will aim to be on station at the time that
the CAS mission reaches its ingress point, and leave at its egress
time. Both flights fly directly to the point with a start time
configured for a rendezvous.

It might be worth adding hold points back to every flight plan just to
ensure that non-formation flights don't end up with a very low speed
enroute to the target if they perform ground ops quicker than
expected.
This commit is contained in:
Dan Albert
2020-10-30 15:54:20 -07:00
parent d94c57afd6
commit 88b9ed29ba
17 changed files with 1033 additions and 669 deletions

View File

@@ -162,7 +162,7 @@ class PackageModel(QAbstractListModel):
"""Returns the flight located at the given index."""
return self.package.flights[index.row()]
def update_tot(self, tot: int) -> None:
def update_tot(self, tot: datetime.timedelta) -> None:
self.package.time_over_target = tot
self.layoutChanged.emit()

View File

@@ -126,7 +126,7 @@ class QTopPanel(QFrame):
continue
estimator = TotEstimator(package)
for flight in package.flights:
if estimator.mission_start_time(flight) < 0:
if estimator.mission_start_time(flight).total_seconds() < 0:
packages.append(package)
break
return packages

View File

@@ -64,7 +64,8 @@ class FlightDelegate(QStyledItemDelegate):
count = flight.count
name = db.unit_type_name(flight.unit_type)
estimator = TotEstimator(self.package)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
delay = datetime.timedelta(
seconds=int(estimator.mission_start_time(flight).total_seconds()))
return f"[{task}] {count} x {name} in {delay}"
def second_row_text(self, index: QModelIndex) -> str:
@@ -328,10 +329,9 @@ class PackageDelegate(QStyledItemDelegate):
def right_text(self, index: QModelIndex) -> str:
package = self.package(index)
if package.time_over_target is None:
return ""
tot = datetime.timedelta(seconds=package.time_over_target)
return f"TOT T+{tot}"
delay = datetime.timedelta(
seconds=int(package.time_over_target.total_seconds()))
return f"TOT T+{delay}"
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
index: QModelIndex) -> None:

View File

@@ -1,9 +1,8 @@
from PySide2.QtCore import QSortFilterProxyModel, Qt, QModelIndex
from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QComboBox, QCompleter
from game import Game
from gen import Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint, PredefinedWaypointCategory
from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
from theater import ControlPointType
@@ -66,8 +65,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.description = "Frontline"
wpt.data = [cp, ecp]
wpt.category = PredefinedWaypointCategory.FRONTLINE
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_targets:
@@ -86,13 +83,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)
wpt.data = ground_object
if cp.captured:
wpt.description = "Friendly Building"
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
else:
wpt.description = "Enemy Building"
wpt.category = PredefinedWaypointCategory.ENEMY_BUILDING
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_units:
@@ -112,15 +106,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
wpt.pretty_name = wpt.name
wpt.targets.append(u)
wpt.data = u
wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly unit : " + u.type
wpt.category = PredefinedWaypointCategory.ALLY_UNIT
else:
wpt.description = "Enemy unit : " + u.type
wpt.category = PredefinedWaypointCategory.ENEMY_UNIT
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:
@@ -134,13 +125,10 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
)
wpt.alt_type = "RADIO"
wpt.name = cp.name
wpt.data = cp
if cp.captured:
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
wpt.category = PredefinedWaypointCategory.ALLY_CP
else:
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
wpt.category = PredefinedWaypointCategory.ENEMY_CP
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"

View File

@@ -30,9 +30,9 @@ from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen import Conflict
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.flightplan import FlightPlan
from qt_ui.displayoptions import DisplayOptions
from qt_ui.models import GameModel
from qt_ui.widgets.map.QFrontLine import QFrontLine
@@ -294,11 +294,10 @@ class QLiberationMap(QGraphicsView):
selected = (p_idx, f_idx) == self.selected_flight
if DisplayOptions.flight_paths.only_selected and not selected:
continue
self.draw_flight_plan(scene, package_model.package, flight,
selected)
self.draw_flight_plan(scene, flight, selected)
def draw_flight_plan(self, scene: QGraphicsScene, package: Package,
flight: Flight, selected: bool) -> None:
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
selected: bool) -> None:
is_player = flight.from_cp.captured
pos = self._transform_point(flight.from_cp.position)
@@ -310,7 +309,7 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
for idx, point in enumerate(flight.points):
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected)
@@ -321,8 +320,8 @@ class QLiberationMap(QGraphicsView):
# Don't draw dozens of targets over each other.
continue
drew_target = True
self.draw_waypoint_info(scene, idx + 1, point, new_pos, package,
flight)
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan)
prev_pos = tuple(new_pos)
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
@@ -337,21 +336,21 @@ class QLiberationMap(QGraphicsView):
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
waypoint: FlightWaypoint, position: Tuple[int, int],
package: Package, flight: Flight) -> None:
timing = PackageWaypointTiming.for_package(package)
flight_plan: FlightPlan) -> None:
altitude = meter_to_feet(waypoint.alt)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
prefix = "TOT"
time = timing.tot_for_waypoint(flight, waypoint)
time = flight_plan.tot_for_waypoint(waypoint)
if time is None:
prefix = "Depart"
time = timing.depart_time_for_waypoint(waypoint, flight)
time = flight_plan.depart_time_for_waypoint(waypoint)
if time is None:
tot = ""
else:
tot = f"{prefix} T+{datetime.timedelta(seconds=time)}"
time = datetime.timedelta(seconds=int(time.total_seconds()))
tot = f"{prefix} T+{time}"
pen = QPen(QColor("black"), 0.3)
brush = QColor("white")

View File

@@ -1,5 +1,6 @@
"""Dialogs for creating and editing ATO packages."""
import logging
from datetime import timedelta
from typing import Optional
from PySide2.QtCore import QItemSelection, QTime, Signal
@@ -118,7 +119,7 @@ class QPackageDialog(QDialog):
return self.game_model.game
def tot_qtime(self) -> QTime:
delay = self.package_model.package.time_over_target
delay = int(self.package_model.package.time_over_target.total_seconds())
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
@@ -137,11 +138,11 @@ class QPackageDialog(QDialog):
def save_tot(self) -> None:
time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(seconds)
self.package_model.update_tot(timedelta(seconds=seconds))
def reset_tot(self) -> None:
if not list(self.package_model.flights):
self.package_model.update_tot(0)
self.package_model.update_tot(timedelta())
else:
self.package_model.update_tot(
TotEstimator(self.package_model.package).earliest_tot())

View File

@@ -19,7 +19,8 @@ class QFlightDepartureDisplay(QGroupBox):
layout.addLayout(departure_row)
estimator = TotEstimator(package)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
delay = datetime.timedelta(
seconds=int(estimator.mission_start_time(flight).total_seconds()))
departure_row.addWidget(QLabel(
f"Departing from <b>{flight.from_cp.name}</b>"

View File

@@ -1,4 +1,3 @@
import datetime
import itertools
from PySide2.QtCore import QItemSelectionModel, QPoint
@@ -6,7 +5,6 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QHeaderView, QTableView
from game.utils import meter_to_feet
from gen.aircraft import PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
@@ -43,8 +41,6 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
timing = PackageWaypointTiming.for_package(self.package)
# The first waypoint is set up by pydcs at mission generation time, so
# we need to add that waypoint manually.
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
@@ -55,13 +51,12 @@ class QFlightWaypointList(QTableView):
waypoints = itertools.chain([takeoff], self.flight.points)
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint, timing)
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
QItemSelectionModel.Select)
def add_waypoint_row(self, row: int, flight: Flight,
waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> None:
waypoint: FlightWaypoint) -> None:
self.model.insertRow(self.model.rowCount())
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
@@ -72,18 +67,18 @@ class QFlightWaypointList(QTableView):
altitude_item.setEditable(False)
self.model.setItem(row, 1, altitude_item)
tot = self.tot_text(flight, waypoint, timing)
tot = self.tot_text(flight, waypoint)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> str:
@staticmethod
def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str:
prefix = ""
time = timing.tot_for_waypoint(flight, waypoint)
time = flight.flight_plan.tot_for_waypoint(waypoint)
if time is None:
prefix = "Depart "
time = timing.depart_time_for_waypoint(waypoint, self.flight)
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
if time is None:
return ""
return f"{prefix}T+{datetime.timedelta(seconds=time)}"
return f"{prefix}T+{time}"