diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 81929d25..6ec68c40 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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: diff --git a/gen/aircraft.py b/gen/aircraft.py index 2aeaf2a9..8459d5c2 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -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() diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 182e0455..c21ca65d 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -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: diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 42b4937d..812d94d0 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -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, diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 276a6396..99bc7f64 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -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: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 6ad8cec9..1b88e040 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -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), diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 7c2ecbd3..e55672bf 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -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, diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 3740448a..a5a51b8f 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -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("" + self.cp.name + "") 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()) diff --git a/resources/customized_payloads/FA-18C_hornet.lua b/resources/customized_payloads/FA-18C_hornet.lua index bcf90452..085c0fcb 100644 --- a/resources/customized_payloads/FA-18C_hornet.lua +++ b/resources/customized_payloads/FA-18C_hornet.lua @@ -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"] = { }, diff --git a/resources/customized_payloads/M-2000C.lua b/resources/customized_payloads/M-2000C.lua index 52c706a1..03cd98e0 100644 --- a/resources/customized_payloads/M-2000C.lua +++ b/resources/customized_payloads/M-2000C.lua @@ -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"] = { },