Add runway bombing missions.

This allows planning the missions and the missions are functional, but
they will have no effect on future turns yet.
This commit is contained in:
Dan Albert 2020-11-23 01:38:27 -08:00
parent 63bdbebcaa
commit 9394ed663a
10 changed files with 237 additions and 87 deletions

View File

@ -401,16 +401,18 @@ class Airfield(ControlPoint):
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from super().mission_types(for_player)
from gen.flights.flight import FlightType
if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
else:
yield from [
# TODO: FlightType.STRIKE
]
FlightType.RUNWAY_ATTACK,
# TODO: FlightType.OCA_STRIKE
]
yield from super().mission_types(for_player)
@property
def total_aircraft_parking(self) -> int:

View File

@ -41,6 +41,7 @@ from dcs.task import (
AntishipStrike,
AttackGroup,
Bombing,
BombingRunway,
CAP,
CAS,
ControlledTask,
@ -55,6 +56,7 @@ from dcs.task import (
OptReactOnThreat,
OptRestrictJettison,
OrbitAction,
RunwayAttack,
SEAD,
StartCommand,
Targets,
@ -1169,6 +1171,18 @@ class AircraftConflictGenerator:
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_runway_attack(
self, group: FlyingGroup, package: Package, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = RunwayAttack.name
self._setup_group(group, RunwayAttack, package, flight,
dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
restrict_jettison=True)
def configure_escort(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
@ -1206,6 +1220,9 @@ class AircraftConflictGenerator:
self.configure_anti_ship(group, package, flight, dynamic_runways)
elif flight_type == FlightType.ESCORT:
self.configure_escort(group, package, flight, dynamic_runways)
elif flight_type == FlightType.RUNWAY_ATTACK:
self.configure_runway_attack(group, package, flight,
dynamic_runways)
else:
self.configure_unknown_task(group, flight)
@ -1345,6 +1362,7 @@ class PydcsWaypointBuilder:
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
FlightWaypointType.INGRESS_RUNWAY_BOMBING: RunwayBombingIngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder,
@ -1480,6 +1498,22 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
return waypoint
class RunwayBombingIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target = self.package.target
if not isinstance(target, Airfield):
logging.error(
"Unexpected target type for runway bombing mission: %s",
target.__class__.__name__)
return waypoint
waypoint.tasks.append(
BombingRunway(airport_id=target.airport.id, group_attack=True))
return waypoint
class SeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()

View File

@ -44,6 +44,8 @@ from gen.flights.ai_flight_planner_db import (
CAP_PREFERRED,
CAS_CAPABLE,
CAS_PREFERRED,
RUNWAY_ATTACK_CAPABLE,
RUNWAY_ATTACK_PREFERRED,
SEAD_CAPABLE,
SEAD_PREFERRED,
STRIKE_CAPABLE,
@ -160,6 +162,8 @@ class AircraftAllocator:
return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED
elif task == FlightType.RUNWAY_ATTACK:
return RUNWAY_ATTACK_PREFERRED
elif task == FlightType.STRIKE:
return STRIKE_PREFERRED
elif task == FlightType.ESCORT:
@ -180,6 +184,8 @@ class AircraftAllocator:
return CAS_CAPABLE
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_CAPABLE
elif task == FlightType.RUNWAY_ATTACK:
return RUNWAY_ATTACK_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:

View File

@ -484,6 +484,13 @@ ANTISHIP_PREFERRED = [
Su_24M,
]
RUNWAY_ATTACK_PREFERRED = [
JF_17,
M_2000C,
]
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
DRONES = [
MQ_9_Reaper,
RQ_1A_Predator,

View File

@ -39,6 +39,7 @@ class FlightType(Enum):
EWAR = 16
SWEEP = 17
RUNWAY_ATTACK = 18
class FlightWaypointType(Enum):
@ -66,6 +67,7 @@ class FlightWaypointType(Enum):
INGRESS_SWEEP = 21
INGRESS_BAI = 22
DIVERT = 23
INGRESS_RUNWAY_BOMBING = 24
class FlightWaypoint:

View File

@ -20,6 +20,7 @@ from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.theater import (
Airfield,
ControlPoint,
FrontLine,
MissionTarget,
@ -650,6 +651,8 @@ class FlightPlanBuilder:
return self.generate_dead(flight, custom_targets)
elif task == FlightType.ESCORT:
return self.generate_escort(flight)
elif task == FlightType.RUNWAY_ATTACK:
return self.generate_runway_attack(flight)
elif task == FlightType.SEAD:
return self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE:
@ -713,7 +716,9 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(building.category, building))
return self.strike_flightplan(flight, location, targets)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_STRIKE,
targets)
def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
"""Generates a BAI flight plan.
@ -731,7 +736,8 @@ class FlightPlanBuilder:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location, targets)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan.
@ -756,7 +762,8 @@ class FlightPlanBuilder:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan(flight, location, targets)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_BAI, targets)
def generate_barcap(self, flight: Flight) -> BarCapFlightPlan:
"""Generate a BARCAP flight at a given location.
@ -942,7 +949,25 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, targets)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_DEAD, targets)
def generate_runway_attack(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a runway attack flight plan at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, Airfield):
logging.exception(
f"Invalid Objective Location for runway bombing flight "
f"{flight=} at {location=}.")
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_RUNWAY_BOMBING)
def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> StrikeFlightPlan:
@ -963,7 +988,8 @@ class FlightPlanBuilder:
for target in custom_targets:
targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan(flight, location, targets)
return self.strike_flightplan(flight, location,
FlightWaypointType.INGRESS_SEAD, targets)
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None
@ -1012,7 +1038,8 @@ class FlightPlanBuilder:
flight=flight,
patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.departure),
patrol_start=builder.ingress_cas(ingress, location),
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
ingress, location),
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
land=builder.land(flight.arrival),
@ -1101,23 +1128,11 @@ class FlightPlanBuilder:
def strike_flightplan(
self, flight: Flight, location: MissionTarget,
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
targets)
if flight.flight_type is FlightType.SEAD:
ingress = builder.ingress_sead(self.package.waypoints.ingress,
location)
elif flight.flight_type is FlightType.DEAD:
ingress = builder.ingress_dead(self.package.waypoints.ingress,
location)
elif flight.flight_type in {FlightType.ANTISHIP, FlightType.BAI}:
ingress = builder.ingress_bai(self.package.waypoints.ingress,
location)
else:
ingress = builder.ingress_strike(self.package.waypoints.ingress,
location)
target_waypoints: List[FlightWaypoint] = []
if targets is not None:
@ -1134,7 +1149,8 @@ class FlightPlanBuilder:
takeoff=builder.takeoff(flight.departure),
hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
ingress=builder.ingress(ingress_type,
self.package.waypoints.ingress, location),
targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split),

View File

@ -173,38 +173,8 @@ class WaypointBuilder:
waypoint.name = "SPLIT"
return waypoint
def ingress_cas(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
objective)
def ingress_escort(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
objective)
def ingress_bai(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_BAI, position,
objective)
def ingress_dead(self, position:Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
objective)
def ingress_sead(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
objective)
def ingress_strike(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
def ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint(
ingress_type,
position.x,
@ -415,8 +385,8 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,

View File

@ -1,8 +1,17 @@
from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QPixmap
from PySide2.QtWidgets import QDialog, QGridLayout, QHBoxLayout, QLabel, QWidget
from PySide2.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
from game.theater import ControlPoint, ControlPointType
from gen.flights.flight import FlightType
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@ -20,9 +29,6 @@ class QBaseMenu2(QDialog):
self.game_model = game_model
self.objectName = "menuDialogue"
# Widgets
self.qbase_menu_tab = QBaseMenuTabs(cp, self.game_model)
try:
game = self.game_model.game
self.airport = game.theater.terrain.airport_by_id(self.cp.id)
@ -39,15 +45,11 @@ class QBaseMenu2(QDialog):
self.setMinimumWidth(800)
self.setMaximumWidth(800)
self.setModal(True)
self.initUi()
def initUi(self):
self.setWindowTitle(self.cp.name)
self.topLayoutWidget = QWidget()
self.topLayout = QHBoxLayout()
self.topLayoutWidget = QWidget()
self.topLayout = QHBoxLayout()
base_menu_header = QWidget()
top_layout = QHBoxLayout()
header = QLabel(self)
header.setGeometry(0, 0, 655, 106)
@ -57,26 +59,42 @@ class QBaseMenu2(QDialog):
title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_aircraft, self.cp.base.total_armor,
"Available" if self.cp.has_runway() else "Unavailable"))
self.topLayout.addWidget(title)
self.topLayout.addWidget(unitsPower)
self.topLayout.setAlignment(Qt.AlignTop)
self.topLayoutWidget.setProperty("style", "baseMenuHeader")
self.topLayoutWidget.setLayout(self.topLayout)
aircraft = self.cp.base.total_aircraft
armor = self.cp.base.total_armor
runway_status = "operational" if self.cp.has_runway() else "inoperative"
intel_summary = QLabel("\n".join([
f"{aircraft} aircraft",
f"{armor} ground units",
f"Runway {runway_status}"
]))
top_layout.addWidget(title)
top_layout.addWidget(intel_summary)
top_layout.setAlignment(Qt.AlignTop)
base_menu_header.setProperty("style", "baseMenuHeader")
base_menu_header.setLayout(top_layout)
self.mainLayout = QGridLayout()
self.mainLayout.addWidget(header, 0, 0)
self.mainLayout.addWidget(self.topLayoutWidget, 1, 0)
self.mainLayout.addWidget(self.qbase_menu_tab, 2, 0)
totalBudget = QLabel(
main_layout = QVBoxLayout()
main_layout.addWidget(header)
main_layout.addWidget(base_menu_header)
main_layout.addWidget(QBaseMenuTabs(cp, self.game_model))
bottom_row = QHBoxLayout()
main_layout.addLayout(bottom_row)
if FlightType.RUNWAY_ATTACK in self.cp.mission_types(for_player=True):
runway_attack_button = QPushButton("Attack airfield")
bottom_row.addWidget(runway_attack_button)
runway_attack_button.setProperty("style", "btn-danger")
runway_attack_button.clicked.connect(self.new_package)
budget_display = QLabel(
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
)
totalBudget.setObjectName("budgetField")
totalBudget.setAlignment(Qt.AlignRight | Qt.AlignBottom)
totalBudget.setProperty("style", "budget-label")
self.mainLayout.addWidget(totalBudget)
self.setLayout(self.mainLayout)
budget_display.setObjectName("budgetField")
budget_display.setAlignment(Qt.AlignRight | Qt.AlignBottom)
budget_display.setProperty("style", "budget-label")
bottom_row.addWidget(budget_display)
self.setLayout(main_layout)
def closeEvent(self, closeEvent:QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
@ -88,3 +106,6 @@ class QBaseMenu2(QDialog):
return "./resources/ui/lha.png"
else:
return "./resources/ui/airbase.png"
def new_package(self) -> None:
Dialog.open_new_package_dialog(self.cp, parent=self.window())

View File

@ -265,6 +265,50 @@ local unitPayloads = {
[1] = 11,
},
},
[7] = {
["name"] = "RUNWAY_ATTACK",
["pylons"] = {
[1] = {
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
["num"] = 9,
},
[2] = {
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{BRU33_2X_MK-82}",
["num"] = 5,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 4,
},
[6] = {
["CLSID"] = "{FPU_8A_FUEL_TANK}",
["num"] = 7,
},
[7] = {
["CLSID"] = "{FPU_8A_FUEL_TANK}",
["num"] = 3,
},
[8] = {
["CLSID"] = "{BRU33_2X_MK-82}",
["num"] = 2,
},
[9] = {
["CLSID"] = "{BRU33_2X_MK-82}",
["num"] = 8,
},
},
["tasks"] = {
[1] = 34,
},
},
},
["tasks"] = {
},

View File

@ -189,6 +189,54 @@ local unitPayloads = {
[1] = 11,
},
},
[6] = {
["name"] = "RUNWAY_ATTACK",
["pylons"] = {
[1] = {
["CLSID"] = "{Eclair}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{MMagicII}",
["num"] = 9,
},
[3] = {
["CLSID"] = "{MMagicII}",
["num"] = 1,
},
[4] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 8,
},
[5] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 2,
},
[6] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 7,
},
[7] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 3,
},
[8] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BLG66_BELOUGA_AC}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 34,
},
},
},
["tasks"] = {
},