Move mission type compatibility to the target.

This was also needed in other parts of the UI and is easier to implement
in the target class anyway.

Note that DEAD is now properly restricted to air defense targets.

Also added error boxes to the UI for when planning fails on an invalid
target.
This commit is contained in:
Dan Albert 2020-11-16 19:57:26 -08:00
parent f3553ced78
commit 8bd00bf450
9 changed files with 119 additions and 138 deletions

View File

@ -20,7 +20,13 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
from theater import (
ControlPoint,
FrontLine,
MissionTarget,
SamGroundObject,
TheaterGroundObject,
)
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime
@ -616,13 +622,7 @@ class FlightPlanBuilder:
raise RuntimeError("Flight must be a part of the package")
if self.package.waypoints is None:
self.regenerate_package_waypoints()
try:
flight_plan = self.generate_flight_plan(flight, custom_targets)
except PlanningError:
logging.exception(f"Could not create flight plan")
return
flight.flight_plan = flight_plan
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
def generate_flight_plan(
self, flight: Flight,
@ -872,7 +872,7 @@ class FlightPlanBuilder:
"""
location = self.package.target
if not isinstance(location, TheaterGroundObject):
if not isinstance(location, SamGroundObject):
logging.exception(f"Invalid Objective Location for DEAD flight {flight=} at {location=}")
raise InvalidObjectiveLocation(flight.flight_type, location)
@ -897,9 +897,6 @@ class FlightPlanBuilder:
"""
location = self.package.target
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
# TODO: Unify these.
# There doesn't seem to be any reason to treat the UI fragged missions
# different from the automatic missions.
@ -1066,7 +1063,7 @@ class FlightPlanBuilder:
return builder.land(arrival)
def strike_flightplan(
self, flight: Flight, location: TheaterGroundObject,
self, flight: Flight, location: MissionTarget,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
@ -1116,8 +1113,8 @@ class FlightPlanBuilder:
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading(heading,
-self.doctrine.join_distance)
return attack_transition.point_from_heading(
heading, -self.doctrine.join_distance)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point(

View File

@ -1,107 +1,16 @@
"""Combo box for selecting a flight's task type."""
import logging
from typing import Iterator
from PySide2.QtWidgets import QComboBox
from gen.flights.flight import FlightType
from theater import (
ConflictTheater,
ControlPoint,
FrontLine,
MissionTarget,
TheaterGroundObject,
)
from theater import ConflictTheater, MissionTarget
class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type."""
COMMON_ENEMY_MISSIONS = [
FlightType.TARCAP,
FlightType.ESCORT,
FlightType.SEAD,
FlightType.DEAD,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,
# TODO: FlightType.RECON,
]
COMMON_FRIENDLY_MISSIONS = [
FlightType.BARCAP,
]
FRIENDLY_AIRBASE_MISSIONS = [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
] + COMMON_FRIENDLY_MISSIONS
FRIENDLY_CARRIER_MISSIONS = [
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
] + COMMON_FRIENDLY_MISSIONS
ENEMY_CARRIER_MISSIONS = [
FlightType.ESCORT,
FlightType.BARCAP,
# TODO: FlightType.ANTISHIP
]
ENEMY_AIRBASE_MISSIONS = [
# TODO: FlightType.STRIKE
] + COMMON_ENEMY_MISSIONS
FRIENDLY_GROUND_OBJECT_MISSIONS = [
# TODO: FlightType.LOGISTICS
# TODO: FlightType.TROOP_TRANSPORT
] + COMMON_FRIENDLY_MISSIONS
ENEMY_GROUND_OBJECT_MISSIONS = [
FlightType.STRIKE,
] + COMMON_ENEMY_MISSIONS
FRONT_LINE_MISSIONS = [
FlightType.CAS,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
] + COMMON_ENEMY_MISSIONS
# TODO: Add BAI missions after we have useful BAI targets.
def __init__(self, theater: ConflictTheater, target: MissionTarget) -> None:
super().__init__()
self.theater = theater
self.target = target
for mission_type in self.mission_types_for_target():
for mission_type in self.target.mission_types(for_player=True):
self.addItem(mission_type.name, userData=mission_type)
def mission_types_for_target(self) -> Iterator[FlightType]:
if isinstance(self.target, ControlPoint):
friendly = self.target.captured
fleet = self.target.is_fleet
if friendly:
if fleet:
yield from self.FRIENDLY_CARRIER_MISSIONS
else:
yield from self.FRIENDLY_AIRBASE_MISSIONS
else:
if fleet:
yield from self.ENEMY_CARRIER_MISSIONS
else:
yield from self.ENEMY_AIRBASE_MISSIONS
elif isinstance(self.target, TheaterGroundObject):
# TODO: Filter more based on the category.
friendly = self.target.control_point.captured
if friendly:
yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
else:
yield from self.ENEMY_GROUND_OBJECT_MISSIONS
elif isinstance(self.target, FrontLine):
yield from self.FRONT_LINE_MISSIONS
else:
logging.error(
f"Unhandled target type: {self.target.__class__.__name__}"
)

View File

@ -8,6 +8,7 @@ from PySide2.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QTimeEdit,
QVBoxLayout,
@ -16,7 +17,7 @@ from PySide2.QtWidgets import (
from game.game import Game
from gen.ato import Package
from gen.flights.flight import Flight
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.flightplan import FlightPlanBuilder, PlanningError
from gen.flights.traveltime import TotEstimator
from qt_ui.models import AtoModel, GameModel, PackageModel
from qt_ui.uiconstants import EVENT_ICONS
@ -167,7 +168,15 @@ class QPackageDialog(QDialog):
self.package_model.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package_model.package,
is_player=True)
try:
planner.populate_flight_plan(flight)
except PlanningError as ex:
self.game.aircraft_inventory.return_from_flight(flight)
self.package_model.delete_flight(flight)
logging.exception("Could not create flight")
QMessageBox.critical(
self, "Could not create flight", str(ex), QMessageBox.Ok
)
# noinspection PyUnresolvedReferences
self.package_changed.emit()

View File

@ -1,3 +1,4 @@
import logging
from typing import List, Optional
from PySide2.QtCore import Signal
@ -13,13 +14,15 @@ from PySide2.QtWidgets import (
from game import Game
from gen.ato import Package
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.flightplan import (
FlightPlanBuilder,
PlanningError,
)
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
QFlightWaypointList
from qt_ui.windows.mission.flight.waypoints \
.QPredefinedWaypointSelectionWindow import \
QPredefinedWaypointSelectionWindow
from theater import FrontLine
class QFlightWaypointTab(QFrame):
@ -55,17 +58,8 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("<strong>Generator :</strong>"))
rlayout.addWidget(QLabel("<small>AI compatible</small>"))
# TODO: Filter by objective type.
self.recreate_buttons.clear()
recreate_types = [
FlightType.CAS,
FlightType.CAP,
FlightType.DEAD,
FlightType.ESCORT,
FlightType.SEAD,
FlightType.STRIKE
]
for task in recreate_types:
for task in self.package.target.mission_types(for_player=True):
def make_closure(arg):
def closure():
return self.confirm_recreate(arg)
@ -142,19 +136,17 @@ class QFlightWaypointTab(QFrame):
QMessageBox.No,
QMessageBox.Yes
)
original_task = self.flight.flight_type
if result == QMessageBox.Yes:
# TODO: Should be buttons for both BARCAP and TARCAP.
# BARCAP and TARCAP behave differently. TARCAP arrives a few minutes
# ahead of the rest of the package and stays until the package
# departs, whereas BARCAP usually isn't part of a strike package and
# has a fixed mission time.
if task == FlightType.CAP:
if self.package.target.is_friendly(to_player=True):
task = FlightType.BARCAP
else:
task = FlightType.TARCAP
self.flight.flight_type = task
try:
self.planner.populate_flight_plan(self.flight)
except PlanningError as ex:
self.flight.flight_type = original_task
logging.exception("Could not recreate flight")
QMessageBox.critical(
self, "Could not recreate flight", str(ex), QMessageBox.Ok
)
self.flight_waypoint_list.update_list()
self.on_change()

View File

@ -2,3 +2,4 @@ from .base import *
from .conflicttheater import *
from .controlpoint import *
from .missiontarget import MissionTarget
from .theatergroundobject import SamGroundObject

View File

@ -18,6 +18,7 @@ from dcs.terrain import (
)
from dcs.terrain.terrain import Terrain
from gen.flights.flight import FlightType
from .controlpoint import ControlPoint, MissionTarget
from .landmap import Landmap, load_landmap, poly_contains
@ -354,6 +355,14 @@ class FrontLine(MissionTarget):
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]
yield from super().mission_types(for_player)
@property
def position(self):
"""

View File

@ -2,8 +2,8 @@ from __future__ import annotations
import itertools
import re
from typing import Dict, List, TYPE_CHECKING
from enum import Enum
from typing import Dict, Iterator, List, TYPE_CHECKING
from dcs.mapping import Point
from dcs.ships import (
@ -20,12 +20,12 @@ from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
BaseDefenseGroundObject,
SamGroundObject,
TheaterGroundObject,
)
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
class ControlPointType(Enum):
@ -237,3 +237,28 @@ class ControlPoint(MissionTarget):
from .start_generator import BaseDefenseGenerator
self.base_defenses = []
BaseDefenseGenerator(game, self).generate()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
if self.is_fleet:
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: Buddy tanking for the A-4?
# TODO: Rescue chopper?
# TODO: Inter-ship logistics?
]
else:
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
else:
if self.is_fleet:
yield from [
# TODO: FlightType.ANTISHIP
]
else:
yield from [
# TODO: FlightType.STRIKE
]

View File

@ -1,7 +1,12 @@
from __future__ import annotations
from typing import Iterator, TYPE_CHECKING
from dcs.mapping import Point
if TYPE_CHECKING:
from gen.flights.flight import FlightType
class MissionTarget:
def __init__(self, name: str, position: Point) -> None:
@ -21,3 +26,18 @@ class MissionTarget:
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
raise NotImplementedError
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield FlightType.BARCAP
else:
yield from [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.SEAD,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,
# TODO: FlightType.RECON,
]

View File

@ -1,7 +1,7 @@
from __future__ import annotations
import itertools
from typing import List, TYPE_CHECKING
from typing import Iterator, List, TYPE_CHECKING
from dcs.mapping import Point
from dcs.unit import Unit
@ -9,6 +9,8 @@ from dcs.unitgroup import Group
if TYPE_CHECKING:
from .controlpoint import ControlPoint
from gen.flights.flight import FlightType
from .missiontarget import MissionTarget
NAME_BY_CATEGORY = {
@ -114,7 +116,18 @@ class TheaterGroundObject(MissionTarget):
return "BLUE" if self.control_point.captured else "RED"
def is_friendly(self, to_player: bool) -> bool:
return not self.control_point.is_friendly(to_player)
return self.control_point.is_friendly(to_player)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.LOGISTICS
# TODO: FlightType.TROOP_TRANSPORT
]
else:
yield FlightType.STRIKE
yield from super().mission_types(for_player)
class BuildingGroundObject(TheaterGroundObject):
@ -240,6 +253,12 @@ class SamGroundObject(BaseDefenseGroundObject):
else:
return super().group_name
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield from super().mission_types(for_player)
class EwrGroundObject(BaseDefenseGroundObject):
def __init__(self, name: str, group_id: int, position: Point,