diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py index 7caa91c3..f6c79c8b 100644 --- a/game/pretense/pretenseaircraftgenerator.py +++ b/game/pretense/pretenseaircraftgenerator.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import random from datetime import datetime from functools import cached_property from typing import Any, Dict, List, TYPE_CHECKING, Tuple @@ -33,18 +34,17 @@ from game.theater.controlpoint import ( Fob, ) from game.unitmap import UnitMap -from .aircraftpainter import AircraftPainter -from .flightdata import FlightData -from .flightgroupconfigurator import FlightGroupConfigurator -from .flightgroupspawner import FlightGroupSpawner -from ...data.weapons import WeaponType +from game.missiongenerator.aircraft.aircraftpainter import AircraftPainter +from game.missiongenerator.aircraft.flightdata import FlightData +from game.missiongenerator.aircraft.flightgroupspawner import FlightGroupSpawner +from game.data.weapons import WeaponType if TYPE_CHECKING: from game import Game from game.squadrons import Squadron -class AircraftGenerator: +class PretenseAircraftGenerator: def __init__( self, mission: Mission, @@ -101,6 +101,7 @@ class AircraftGenerator: def generate_flights( self, country: Country, + cp: ControlPoint, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData], ) -> None: @@ -115,6 +116,61 @@ class AircraftGenerator: ato: The ATO to spawn aircraft for. dynamic_runways: Runway data for carriers and FARPs. """ + + num_of_sead = 0 + num_of_cas = 0 + num_of_strike = 0 + num_of_cap = 0 + for squadron in cp.squadrons: + squadron.owned_aircraft += 1 + squadron.untasked_aircraft += 1 + package = Package(cp, squadron.flight_db, auto_asap=False) + mission_types = squadron.auto_assignable_mission_types + if ( + FlightType.TRANSPORT in mission_types + or FlightType.AIR_ASSAULT in mission_types + ): + flight_type = FlightType.TRANSPORT + elif ( + FlightType.SEAD in mission_types + or FlightType.SEAD_SWEEP + or FlightType.SEAD_ESCORT in mission_types + ) and num_of_sead < 2: + flight_type = FlightType.SEAD + num_of_sead += 1 + elif FlightType.DEAD in mission_types and num_of_sead < 2: + flight_type = FlightType.DEAD + num_of_sead += 1 + elif ( + FlightType.CAS in mission_types or FlightType.BAI in mission_types + ) and num_of_cas < 2: + flight_type = FlightType.CAS + num_of_cas += 1 + elif ( + FlightType.STRIKE in mission_types + or FlightType.OCA_RUNWAY in mission_types + or FlightType.OCA_AIRCRAFT in mission_types + ) and num_of_strike < 2: + flight_type = FlightType.STRIKE + num_of_strike += 1 + elif ( + FlightType.BARCAP in mission_types + or FlightType.TARCAP in mission_types + or FlightType.ESCORT in mission_types + ) and num_of_cap < 2: + flight_type = FlightType.BARCAP + num_of_cap += 1 + else: + flight_type = random.choice(list(mission_types)) + flight = Flight( + package, squadron, 1, flight_type, StartType.COLD, divert=cp + ) + flight.state = WaitingForStart( + flight, self.game.settings, self.game.conditions.start_time + ) + package.add_flight(flight) + ato.add_package(package) + self._reserve_frequencies_and_tacan(ato) for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)): @@ -134,103 +190,6 @@ class AircraftGenerator: flight, country, dynamic_runways ) self.unit_map.add_aircraft(group, flight) - if ( - package.primary_flight is not None - and package.primary_flight.flight_plan.is_formation( - package.primary_flight.flight_plan - ) - ): - splittrigger = TriggerOnce(Event.NoEvent, f"Split-{id(package)}") - splittrigger.add_condition(FlagIsTrue(flag=f"split-{id(package)}")) - splittrigger.add_condition(Or()) - splittrigger.add_condition(FlagIsFalse(flag=f"split-{id(package)}")) - splittrigger.add_condition(GroupDead(package.primary_flight.group_id)) - for flight in package.flights: - if flight.flight_type in [ - FlightType.ESCORT, - FlightType.SEAD_ESCORT, - ]: - splittrigger.add_action(AITaskPush(flight.group_id, 1)) - if len(splittrigger.actions) > 0: - self.mission.triggerrules.triggers.append(splittrigger) - - def spawn_unused_aircraft( - self, player_country: Country, enemy_country: Country - ) -> None: - for control_point in self.game.theater.controlpoints: - if not ( - isinstance(control_point, Airfield) or isinstance(control_point, Fob) - ): - continue - - if control_point.captured: - country = player_country - else: - country = enemy_country - - for squadron in control_point.squadrons: - try: - self._spawn_unused_for(squadron, country) - except NoParkingSlotError: - # If we run out of parking, stop spawning aircraft at this base. - break - - def _spawn_unused_for(self, squadron: Squadron, country: Country) -> None: - assert isinstance(squadron.location, Airfield) or isinstance( - squadron.location, Fob - ) - if ( - squadron.coalition.player - and self.game.settings.perf_disable_untasked_blufor_aircraft - ): - return - elif ( - not squadron.coalition.player - and self.game.settings.perf_disable_untasked_opfor_aircraft - ): - return - - for _ in range(squadron.untasked_aircraft): - # Creating a flight even those this isn't a fragged mission lets us - # reuse the existing debriefing code. - # TODO: Special flight type? - flight = Flight( - Package(squadron.location, self.game.db.flights), - squadron, - 1, - FlightType.BARCAP, - StartType.COLD, - divert=None, - claim_inv=False, - ) - flight.state = Completed(flight, self.game.settings) - - group = FlightGroupSpawner( - flight, - country, - self.mission, - self.helipads, - self.ground_spawns_roadbase, - self.ground_spawns, - self.mission_data, - ).create_idle_aircraft() - if group: - if ( - not squadron.coalition.player - and squadron.aircraft.flyable - and ( - self.game.settings.enable_squadron_pilot_limits - or squadron.number_of_available_pilots > 0 - ) - and self.game.settings.untasked_opfor_client_slots - ): - flight.state = WaitingForStart( - flight, self.game.settings, self.game.conditions.start_time - ) - group.uncontrolled = False - group.units[0].skill = Skill.Client - AircraftPainter(flight, group).apply_livery() - self.unit_map.add_aircraft(group, flight) def create_and_configure_flight( self, flight: Flight, country: Country, dynamic_runways: Dict[str, RunwayData] @@ -245,21 +204,21 @@ class AircraftGenerator: self.ground_spawns, self.mission_data, ).create_flight_group() - self.flights.append( - FlightGroupConfigurator( - flight, - group, - self.game, - self.mission, - self.time, - self.radio_registry, - self.tacan_registy, - self.laser_code_registry, - self.mission_data, - dynamic_runways, - self.use_client, - ).configure() - ) + # self.flights.append( + # FlightGroupConfigurator( + # flight, + # group, + # self.game, + # self.mission, + # self.time, + # self.radio_registry, + # self.tacan_registy, + # self.laser_code_registry, + # self.mission_data, + # dynamic_runways, + # self.use_client, + # ).configure() + # ) if self.ewrj: self._track_ewrj_flight(flight, group) diff --git a/game/pretense/pretensemissiongenerator.py b/game/pretense/pretensemissiongenerator.py index 85c5e5c9..16478f27 100644 --- a/game/pretense/pretensemissiongenerator.py +++ b/game/pretense/pretensemissiongenerator.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING, cast import dcs.lua +from dataclasses import field from dcs import Mission, Point from dcs.coalition import Coalition from dcs.countries import country_dict @@ -13,38 +14,40 @@ from dcs.task import OptReactOnThreat from game.atcdata import AtcData from game.dcs.beacons import Beacons -from game.dcs.helpers import unit_type_from_name -from game.missiongenerator.aircraft.aircraftgenerator import ( - AircraftGenerator, -) + from game.naming import namegen from game.radio.radios import RadioFrequency, RadioRegistry, MHz from game.radio.tacan import TacanRegistry from game.theater import Airfield from game.theater.bullseye import Bullseye from game.unitmap import UnitMap -from .briefinggenerator import BriefingGenerator, MissionInfoGenerator -from .cargoshipgenerator import CargoShipGenerator -from .convoygenerator import ConvoyGenerator -from .drawingsgenerator import DrawingsGenerator -from .environmentgenerator import EnvironmentGenerator -from .flotgenerator import FlotGenerator -from .forcedoptionsgenerator import ForcedOptionsGenerator -from .frontlineconflictdescription import FrontLineConflictDescription -from .kneeboard import KneeboardGenerator -from .lasercoderegistry import LaserCodeRegistry -from .luagenerator import LuaGenerator -from .missiondata import MissionData -from .tgogenerator import TgoGenerator -from .triggergenerator import TriggerGenerator -from .visualsgenerator import VisualsGenerator +from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator +from game.missiongenerator.briefinggenerator import ( + BriefingGenerator, + MissionInfoGenerator, +) +from game.missiongenerator.convoygenerator import ConvoyGenerator +from game.missiongenerator.environmentgenerator import EnvironmentGenerator +from game.missiongenerator.flotgenerator import FlotGenerator +from game.missiongenerator.forcedoptionsgenerator import ForcedOptionsGenerator +from game.missiongenerator.frontlineconflictdescription import ( + FrontLineConflictDescription, +) +from game.missiongenerator.kneeboard import KneeboardGenerator +from game.missiongenerator.lasercoderegistry import LaserCodeRegistry +from game.missiongenerator.luagenerator import LuaGenerator +from game.missiongenerator.missiondata import MissionData +from game.missiongenerator.tgogenerator import TgoGenerator +from .pretensetriggergenerator import PretenseTriggerGenerator +from game.missiongenerator.visualsgenerator import VisualsGenerator +from ..ato import Flight from ..radio.TacanContainer import TacanContainer if TYPE_CHECKING: from game import Game -class MissionGenerator: +class PretenseMissionGenerator: def __init__(self, game: Game, time: datetime) -> None: self.game = game self.time = time @@ -94,20 +97,16 @@ class MissionGenerator: tgo_generator.generate() ConvoyGenerator(self.mission, self.game, self.unit_map).generate() - CargoShipGenerator(self.mission, self.game, self.unit_map).generate() - - self.generate_destroyed_units() # Generate ground conflicts first so the JTACs get the first laser code (1688) # rather than the first player flight with a TGP. self.generate_ground_conflicts() self.generate_air_units(tgo_generator) - TriggerGenerator(self.mission, self.game).generate() + PretenseTriggerGenerator(self.mission, self.game).generate() ForcedOptionsGenerator(self.mission, self.game).generate() VisualsGenerator(self.mission, self.game).generate() LuaGenerator(self.game, self.mission, self.mission_data).generate() - DrawingsGenerator(self.mission, self.game).generate() self.setup_combined_arms() @@ -119,22 +118,6 @@ class MissionGenerator: return self.unit_map - @staticmethod - def _configure_ewrj(gen: AircraftGenerator) -> None: - for groups in gen.ewrj_package_dict.values(): - optrot = groups[0].points[0].tasks[0] - assert isinstance(optrot, OptReactOnThreat) - if ( - len(groups) == 1 - and optrot.value != OptReactOnThreat.Values.PassiveDefense - ): - # primary flight with no EWR-Jamming capability - continue - for group in groups: - group.points[0].tasks[0] = OptReactOnThreat( - OptReactOnThreat.Values.PassiveDefense - ) - def setup_mission_coalitions(self) -> None: self.mission.coalition["blue"] = Coalition( "blue", bullseye=self.game.blue.bullseye.to_pydcs() @@ -232,7 +215,7 @@ class MissionGenerator: """Generate the air units for the Operation""" # Generate Aircraft Activity on the map - aircraft_generator = AircraftGenerator( + aircraft_generator = PretenseAircraftGenerator( self.mission, self.game.settings, self.game, @@ -247,22 +230,25 @@ class MissionGenerator: ground_spawns=tgo_generator.ground_spawns, ) + # Clear parking slots and ATOs aircraft_generator.clear_parking_slots() + self.game.blue.ato.clear() + self.game.red.ato.clear() - aircraft_generator.generate_flights( - self.p_country, - self.game.blue.ato, - tgo_generator.runways, - ) - aircraft_generator.generate_flights( - self.e_country, - self.game.red.ato, - tgo_generator.runways, - ) - aircraft_generator.spawn_unused_aircraft( - self.p_country, - self.e_country, - ) + for cp in self.game.theater.controlpoints: + if cp.captured: + ato = self.game.blue.ato + cp_country = self.p_country + else: + ato = self.game.red.ato + cp_country = self.e_country + print(f"Generating flights for {cp_country.name} at {cp}") + aircraft_generator.generate_flights( + cp_country, + cp, + ato, + tgo_generator.runways, + ) self.mission_data.flights = aircraft_generator.flights @@ -274,35 +260,6 @@ class MissionGenerator: if self.game.settings.plugins.get("ewrj"): self._configure_ewrj(aircraft_generator) - def generate_destroyed_units(self) -> None: - """Add destroyed units to the Mission""" - if not self.game.settings.perf_destroyed_units: - return - - for d in self.game.get_destroyed_units(): - try: - type_name = d["type"] - if not isinstance(type_name, str): - raise TypeError( - "Expected the type of the destroyed static to be a string" - ) - utype = unit_type_from_name(type_name) - except KeyError: - logging.warning(f"Destroyed unit has no type: {d}") - continue - - pos = Point(cast(float, d["x"]), cast(float, d["z"]), self.mission.terrain) - if utype is not None and not self.game.position_culled(pos): - self.mission.static_group( - country=self.p_country, - name="", - _type=utype, - hidden=True, - position=pos, - heading=d["orientation"], - dead=True, - ) - def notify_info_generators( self, ) -> None: diff --git a/game/pretense/pretensetriggergenerator.py b/game/pretense/pretensetriggergenerator.py index cc31b4e5..11932f87 100644 --- a/game/pretense/pretensetriggergenerator.py +++ b/game/pretense/pretensetriggergenerator.py @@ -56,7 +56,7 @@ class Silence(Option): Key = 7 -class TriggerGenerator: +class PretenseTriggerGenerator: capture_zone_types = (Fob, Airfield) capture_zone_flag = 600 @@ -146,38 +146,7 @@ class TriggerGenerator: v += 1 self.mission.triggerrules.triggers.append(mark_trigger) - def _generate_clear_statics_trigger(self, scenery_clear_zones: List[Point]) -> None: - for zone_center in scenery_clear_zones: - trigger_zone = self.mission.triggers.add_triggerzone( - zone_center, - radius=TRIGGER_RADIUS_CLEAR_SCENERY, - hidden=False, - name="CLEAR", - ) - clear_trigger = TriggerCondition(Event.NoEvent, "Clear Trigger") - clear_flag = self.get_capture_zone_flag() - clear_trigger.add_condition(TimeSinceFlag(clear_flag, 30)) - clear_trigger.add_action(ClearFlag(clear_flag)) - clear_trigger.add_action(SetFlag(clear_flag)) - clear_trigger.add_action( - RemoveSceneObjects( - objects_mask=RemoveSceneObjectsMask.OBJECTS_ONLY, - zone=trigger_zone.id, - ) - ) - clear_trigger.add_action( - SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id) - ) - self.mission.triggerrules.triggers.append(clear_trigger) - - enable_clear_trigger = TriggerOnce(Event.NoEvent, "Enable Clear Trigger") - enable_clear_trigger.add_condition(TimeAfter(30)) - enable_clear_trigger.add_action(ClearFlag(clear_flag)) - enable_clear_trigger.add_action(SetFlag(clear_flag)) - # clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),)) - self.mission.triggerrules.triggers.append(enable_clear_trigger) - - def _generate_capture_triggers( + def _generate_pretense_zone_triggers( self, player_coalition: str, enemy_coalition: str ) -> None: """Creates a pair of triggers for each control point of `cls.capture_zone_types`. @@ -185,62 +154,16 @@ class TriggerGenerator: Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua` """ for cp in self.game.theater.controlpoints: - if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: - if cp.captured: - attacking_coalition = enemy_coalition - attack_coalition_int = 1 # 1 is the Event int for Red - defending_coalition = player_coalition - defend_coalition_int = 2 # 2 is the Event int for Blue - else: - attacking_coalition = player_coalition - attack_coalition_int = 2 - defending_coalition = enemy_coalition - defend_coalition_int = 1 + if isinstance(cp, self.capture_zone_types) and not cp.is_fleet: + zone_color = {1: 0.0, 2: 0.0, 3: 0.0, 4: 0.149} trigger_zone = self.mission.triggers.add_triggerzone( cp.position, radius=TRIGGER_RADIUS_CAPTURE, hidden=False, - name="CAPTURE", + name=cp.name, + color=zone_color, ) - flag = self.get_capture_zone_flag() - capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") - capture_trigger.add_condition( - AllOfCoalitionOutsideZone( - defending_coalition, trigger_zone.id, unit_type="GROUND" - ) - ) - capture_trigger.add_condition( - PartOfCoalitionInZone( - attacking_coalition, trigger_zone.id, unit_type="GROUND" - ) - ) - capture_trigger.add_condition(FlagIsFalse(flag=flag)) - script_string = String( - f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"' - ) - capture_trigger.add_action(DoScript(script_string)) - capture_trigger.add_action(SetFlag(flag=flag)) - self.mission.triggerrules.triggers.append(capture_trigger) - - recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") - recapture_trigger.add_condition( - AllOfCoalitionOutsideZone( - attacking_coalition, trigger_zone.id, unit_type="GROUND" - ) - ) - recapture_trigger.add_condition( - PartOfCoalitionInZone( - defending_coalition, trigger_zone.id, unit_type="GROUND" - ) - ) - recapture_trigger.add_condition(FlagIsTrue(flag=flag)) - script_string = String( - f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"' - ) - recapture_trigger.add_action(DoScript(script_string)) - recapture_trigger.add_action(ClearFlag(flag=flag)) - self.mission.triggerrules.triggers.append(recapture_trigger) def generate(self) -> None: player_coalition = "blue" @@ -249,13 +172,7 @@ class TriggerGenerator: self._set_skill(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition) self._gen_markers() - self._generate_capture_triggers(player_coalition, enemy_coalition) - if self.game.settings.ground_start_scenery_remove_triggers: - try: - self._generate_clear_statics_trigger(self.game.scenery_clear_zones) - self.game.scenery_clear_zones.clear() - except AttributeError: - logging.info(f"Unable to create Clear Statics triggers") + self._generate_pretense_zone_triggers(player_coalition, enemy_coalition) @classmethod def get_capture_zone_flag(cls) -> int: diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index dcb95b1a..8edc69a0 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -32,6 +32,7 @@ def load_icons(): "./resources/ui/misc/" + get_theme_icons() + "/github.png" ) ICONS["Ukraine"] = QPixmap("./resources/ui/misc/ukraine.png") + ICONS["Pretense"] = QPixmap("./resources/ui/misc/pretense.png") ICONS["Control Points"] = QPixmap( "./resources/ui/misc/" + get_theme_icons() + "/circle.png" diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index bfe61851..c6c32c48 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -21,6 +21,7 @@ from game import Game, VERSION, persistency, Migrator from game.debriefing import Debriefing from game.game import TurnState from game.layout import LAYOUTS +from game.pretense.pretensemissiongenerator import PretenseMissionGenerator from game.server import EventStream, GameContext from game.server.dependencies import QtCallbacks, QtContext from game.theater import ControlPoint, MissionTarget, TheaterGroundObject @@ -41,6 +42,7 @@ from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.logs.QLogsWindow import QLogsWindow from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard +from qt_ui.windows.newgame.QNewPretenseWizard import NewPretenseWizard from qt_ui.windows.notes.QNotesWindow import QNotesWindow from qt_ui.windows.preferences.QLiberationPreferencesWindow import ( QLiberationPreferencesWindow, @@ -193,6 +195,10 @@ class QLiberationWindow(QMainWindow): lambda: webbrowser.open_new_tab("https://shdwp.github.io/ukraine/") ) + self.newPretenseAction = QAction("&New Pretense Campaign", self) + self.newPretenseAction.setIcon(QIcon(CONST.ICONS["Pretense"])) + self.newPretenseAction.triggered.connect(self.newPretenseCampaign) + self.openLogsAction = QAction("Show &logs", self) self.openLogsAction.triggered.connect(self.showLogsDialog) @@ -234,6 +240,7 @@ class QLiberationWindow(QMainWindow): self.links_bar.addAction(self.openDiscordAction) self.links_bar.addAction(self.openGithubAction) self.links_bar.addAction(self.ukraineAction) + self.links_bar.addAction(self.newPretenseAction) self.actions_bar = self.addToolBar("Actions") self.actions_bar.addAction(self.openSettingsAction) @@ -303,6 +310,15 @@ class QLiberationWindow(QMainWindow): wizard.show() wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame)) + def newPretenseCampaign(self): + output = persistency.mission_path_for("pretense_campaign.miz") + PretenseMissionGenerator( + self.game, self.game.conditions.start_time + ).generate_miz(output) + title = "Pretense campaign generated" + msg = f"A Pretense campaign mission has been successfully generated in {output}" + QMessageBox.information(QApplication.focusWidget(), title, msg, QMessageBox.Ok) + def openFile(self): if self.game is not None and self.game.savepath: save_dir = self.game.savepath