diff --git a/.gitignore b/.gitignore index 2d61eba..aea95bd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,10 @@ Generator/.idea Generator/__pycache__ Generator/build Generator/venv -Generator/dist \ No newline at end of file +Generator/dist +Generator/generator.log +MissionGenerator.exe +incoming templates/ +Generator/utils/extract units/source.miz +Generator/utils/extract units/units.txt +generator.log diff --git a/Generator/Forces/_How to add your own templates.txt b/Generator/Forces/_How to add your own templates.txt index 20b8d42..1838d7d 100644 --- a/Generator/Forces/_How to add your own templates.txt +++ b/Generator/Forces/_How to add your own templates.txt @@ -4,6 +4,15 @@ You can add your own unit templates in this directory and they will appear in th 2) Add ground unit groups. 3) Save the mission in this directory. +Optional: +4) Add helicopters with "CAS" main task for attack helicopters. +5) Add helicopters with "Transport" main task for transport helicopters. +6) Add planes with "CAS" main task for attack planes. +7) Add planes with "CAP" main task for fighters. +8) Configure loadouts, liveries, and skill for aircraft. + Tips: -Drop your templates in the RotorOps Discord if you'd like to have them added in a release for everyone. -The mission generator will only extract blue ground units from the template when selected from the "Blue Forces" menu, and vice versa. +-Only unit types are used from ground units. Liveries or other attributes are able to be copied. +-For aircraft, group size is currently capped at 2 units per group to help prevent issues with parking. Only the first unit in the group is used as a source. diff --git a/Generator/Forces/blue/BLUE Default Armor.miz b/Generator/Forces/blue/BLUE Default Armor.miz index ffd71fd..fc9903a 100644 Binary files a/Generator/Forces/blue/BLUE Default Armor.miz and b/Generator/Forces/blue/BLUE Default Armor.miz differ diff --git a/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz b/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz new file mode 100644 index 0000000..8d049d1 Binary files /dev/null and b/Generator/Forces/blue/BLUE Vietnam Armor (Mr Nobody).miz differ diff --git a/Generator/Forces/red/RED Armor (Hard).miz b/Generator/Forces/red/RED Armor (Hard).miz index 17e12e3..b599356 100644 Binary files a/Generator/Forces/red/RED Armor (Hard).miz and b/Generator/Forces/red/RED Armor (Hard).miz differ diff --git a/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz b/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz index fa4ada1..b2eec21 100644 Binary files a/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz and b/Generator/Forces/red/RED Armor, Infantry & Artillery (Med).miz differ diff --git a/Generator/Forces/red/RED Trucks & Infantry (Easy).miz b/Generator/Forces/red/RED Trucks & Infantry (Easy).miz index 85e75b8..5713a14 100644 Binary files a/Generator/Forces/red/RED Trucks & Infantry (Easy).miz and b/Generator/Forces/red/RED Trucks & Infantry (Easy).miz differ diff --git a/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz b/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz new file mode 100644 index 0000000..b939702 Binary files /dev/null and b/Generator/Forces/red/RED Vietnam Armor & Infantry (Mr Nobody).miz differ diff --git a/Generator/MissionGenerator.py b/Generator/MissionGenerator.py index b9d8367..ccb4384 100644 --- a/Generator/MissionGenerator.py +++ b/Generator/MissionGenerator.py @@ -5,7 +5,7 @@ import dcs import RotorOpsMission as ROps import RotorOpsUtils import RotorOpsUnits - +import logging from PyQt5.QtWidgets import ( QApplication, QDialog, QMainWindow, QMessageBox @@ -13,12 +13,38 @@ from PyQt5.QtWidgets import ( from PyQt5 import QtGui from MissionGeneratorUI import Ui_MainWindow + +#Setup logfile and exception handler +logger = logging.getLogger(__name__) +logging.basicConfig(filename='generator.log', encoding='utf-8', level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') +handler = logging.StreamHandler(stream=sys.stdout) +logger.addHandler(handler) + +def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): #example of handling error subclasses + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + msg = QMessageBox() + msg.setWindowTitle("Uncaught exception") + msg.setText("Oops, there was a problem. Please check the log file or post it in the RotorOps discord where some helpful people will have a look.") + x = msg.exec_() + + +sys.excepthook = handle_exception + + maj_version = 0 -minor_version = 3 +minor_version = 4 version_string = str(maj_version) + "." + str(minor_version) scenarios = [] red_forces_files = [] blue_forces_files = [] +defenders_text = "Defending Forces:" +attackers_text = "Attacking Forces:" + +logger.info("RotorOps v" + version_string) class Window(QMainWindow, Ui_MainWindow): @@ -27,11 +53,11 @@ class Window(QMainWindow, Ui_MainWindow): super().__init__(parent) if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - print('running in a PyInstaller bundle') + logger.info('running in a PyInstaller bundle') home_dir = os.getcwd() os.chdir(home_dir + "/Generator") else: - print('running in a normal Python process') + logger.info('running in a normal Python process') self.m = ROps.RotorOpsMission() @@ -42,6 +68,8 @@ class Window(QMainWindow, Ui_MainWindow): self.populateForces("blue", self.blueforces_comboBox, blue_forces_files) self.populateSlotSelection() + self.blue_forces_label.setText(attackers_text) + self.red_forces_label.setText(defenders_text) self.background_label.setPixmap(QtGui.QPixmap(self.m.assets_dir + "/background.PNG")) self.statusbar.setStyleSheet( "QStatusBar{padding-left:5px;color:black;font-weight:bold;}") @@ -50,15 +78,15 @@ class Window(QMainWindow, Ui_MainWindow): def connectSignalsSlots(self): - # self.action_Exit.triggered.connect(self.close) self.action_generateMission.triggered.connect(self.generateMissionAction) self.action_scenarioSelected.triggered.connect(self.scenarioChanged) + self.action_defensiveModeChanged.triggered.connect(self.defensiveModeChanged) def populateScenarios(self): os.chdir(self.m.scenarios_dir) path = os.getcwd() dir_list = os.listdir(path) - print("Looking for mission files in '", path, "' :") + logger.info("Looking for mission files in " + path) for filename in dir_list: if filename.endswith(".miz"): @@ -70,7 +98,7 @@ class Window(QMainWindow, Ui_MainWindow): os.chdir(self.m.forces_dir + "/" + side) path = os.getcwd() dir_list = os.listdir(path) - print("Looking for " + side + " Forces files in '", os.getcwd(), "' :") + logger.info("Looking for " + side + " Forces files in '" + path) for filename in dir_list: if filename.endswith(".miz"): @@ -82,65 +110,71 @@ class Window(QMainWindow, Ui_MainWindow): for type in RotorOpsUnits.client_helos: self.slot_template_comboBox.addItem(type.id) + def defensiveModeChanged(self): + if self.defense_checkBox.isChecked(): + self.red_forces_label.setText(attackers_text) + self.blue_forces_label.setText(defenders_text) + else: + self.red_forces_label.setText(defenders_text) + self.blue_forces_label.setText(attackers_text) + def scenarioChanged(self): - try: - os.chdir(self.m.scenarios_dir) - filename = scenarios[self.scenario_comboBox.currentIndex()] - source_mission = dcs.mission.Mission() - source_mission.load_file(filename) - zones = source_mission.triggers.zones() - conflict_zones = 0 - staging_zones = 0 - conflict_zone_size_sum = 0 - conflict_zone_distance_sum = 0 - spawn_zones = 0 - conflict_zone_positions = [] - #friendly_airports = source_mission.getCoalitionAirports("blue") - #enemy_airports = source_mission.getCoalitionAirports("red") - friendly_airports = True - enemy_airports = True + os.chdir(self.m.scenarios_dir) + filename = scenarios[self.scenario_comboBox.currentIndex()] + source_mission = dcs.mission.Mission() + source_mission.load_file(filename) + zones = source_mission.triggers.zones() + conflict_zones = 0 + staging_zones = 0 + conflict_zone_size_sum = 0 + conflict_zone_distance_sum = 0 + spawn_zones = 0 + conflict_zone_positions = [] + #friendly_airports = source_mission.getCoalitionAirports("blue") + #enemy_airports = source_mission.getCoalitionAirports("red") + friendly_airports = True + enemy_airports = True - ## TODO: we should be creating a new instance of RotorOpsMission each time scenario is changed so we can access all methods and vars + for zone in zones: + if zone.name == "STAGING": + staging_zones += 1 + if zone.name == "ALPHA" or zone.name == "BRAVO" or zone.name == "CHARLIE" or zone.name == "DELTA": + conflict_zones += 1 + conflict_zone_size_sum += zone.radius + conflict_zone_positions.append(zone.position) + if zone.name.rfind("_SPAWN") > 0: + spawn_zones += 1 + if conflict_zones > 1: + for index, position in enumerate(conflict_zone_positions): + if index > 0: + conflict_zone_distance_sum += RotorOpsUtils.getDistance(conflict_zone_positions[index], conflict_zone_positions[index - 1]) - for zone in zones: - if zone.name == "STAGING": - staging_zones += 1 - if zone.name == "ALPHA" or zone.name == "BRAVO" or zone.name == "CHARLIE" or zone.name == "DELTA": - conflict_zones += 1 - conflict_zone_size_sum += zone.radius - conflict_zone_positions.append(zone.position) - if zone.name.rfind("_SPAWN") > 0: - spawn_zones += 1 - if conflict_zones > 1: - for index, position in enumerate(conflict_zone_positions): - if index > 0: - conflict_zone_distance_sum += RotorOpsUtils.getDistance(conflict_zone_positions[index], conflict_zone_positions[index - 1]) + def validateTemplate(): + valid = True + if len(staging_zones) < 1: + valid = False + if len(conflict_zones) < 1: + valid = False + if not friendly_airports: + valid = False + if not enemy_airports: + valid = False + return valid - def validateTemplate(): - valid = True - if len(staging_zones) < 1: - valid = False - if len(conflict_zones) < 1: - valid = False - if not friendly_airports: - valid = False - if not enemy_airports: - valid = False - return valid - - if conflict_zones and staging_zones : - average_zone_size = conflict_zone_size_sum / conflict_zones - self.description_textBrowser.setText( - "Map: " + source_mission.terrain.name + "\n" + - "Conflict Zones: " + str(conflict_zones) + "\n" + - "Average Zone Size " + str(math.floor(average_zone_size)) + "m \n" + - "Infantry Spawn Zones: " + str(spawn_zones) + "\n" + - "Approx Distance: " + str(math.floor(RotorOpsUtils.convertMeterToNM(conflict_zone_distance_sum))) + "nm \n" - #"Validity Check:" + str(validateTemplate()) - ) - except: - self.description_textBrowser.setText("File error occured.") + if conflict_zones and staging_zones : + average_zone_size = conflict_zone_size_sum / conflict_zones + self.description_textBrowser.setText( + "Map: " + source_mission.terrain.name + "\n" + + "Conflict Zones: " + str(conflict_zones) + "\n" + + "Average Zone Size " + str(math.floor(average_zone_size)) + "m \n" + + "Infantry Spawn Zones: " + str(spawn_zones) + "\n" + + "Approx Distance: " + str(math.floor(RotorOpsUtils.convertMeterToNM(conflict_zone_distance_sum))) + "nm \n" + #"Validity Check:" + str(validateTemplate()) + + "\n== BRIEFING ==\n\n" + + source_mission.description_text() + ) + #self.description_textBrowser.setText("File error occured.") def generateMissionAction(self): @@ -168,19 +202,22 @@ class Window(QMainWindow, Ui_MainWindow): "zone_protect_sams": self.zone_sams_checkBox.isChecked(), "zone_farps": self.farp_buttonGroup.checkedButton().objectName(), "inf_spawn_msgs": self.inf_spawn_voiceovers_checkBox.isChecked(), + "e_transport_helos": self.e_transport_helos_spinBox.value(), + "transport_drop_qty": self.troop_drop_spinBox.value(), + "smoke_pickup_zones": self.smoke_pickup_zone_checkBox.isChecked(), } os.chdir(self.m.home_dir + '/Generator') n = ROps.RotorOpsMission() result = n.generateMission(data) - print("Generating mission with options:") - print(str(data)) + logger.info("Generating mission with options:") + logger.info(str(data)) # generate the mission #result = self.m.generateMission(data) #display results if result["success"]: - print(result["filename"] + "' successfully generated in " + result["directory"]) + logger.info(result["filename"] + "' successfully generated in " + result["directory"]) self.statusbar.showMessage(result["filename"] + "' successfully generated in " + result["directory"], 10000) msg = QMessageBox() msg.setWindowTitle("Mission Generated") @@ -199,7 +236,7 @@ class Window(QMainWindow, Ui_MainWindow): ) x = msg.exec_() elif not result["success"]: - print(result["failure_msg"]) + logger.warning(result["failure_msg"]) msg = QMessageBox() msg.setWindowTitle("Error") msg.setText(result["failure_msg"]) diff --git a/Generator/MissionGeneratorUI.py b/Generator/MissionGeneratorUI.py index a318da0..fafade2 100644 --- a/Generator/MissionGeneratorUI.py +++ b/Generator/MissionGeneratorUI.py @@ -48,7 +48,7 @@ class Ui_MainWindow(object): "padding: 4px;") self.generateButton.setObjectName("generateButton") self.description_textBrowser = QtWidgets.QTextBrowser(self.centralwidget) - self.description_textBrowser.setGeometry(QtCore.QRect(710, 20, 331, 131)) + self.description_textBrowser.setGeometry(QtCore.QRect(670, 30, 501, 131)) font = QtGui.QFont() font.setPointSize(9) self.description_textBrowser.setFont(font) @@ -57,23 +57,23 @@ class Ui_MainWindow(object): self.blueforces_comboBox = QtWidgets.QComboBox(self.centralwidget) self.blueforces_comboBox.setGeometry(QtCore.QRect(790, 230, 291, 31)) self.blueforces_comboBox.setObjectName("blueforces_comboBox") - self.scenario_label_2 = QtWidgets.QLabel(self.centralwidget) - self.scenario_label_2.setGeometry(QtCore.QRect(690, 180, 141, 31)) + self.blue_forces_label = QtWidgets.QLabel(self.centralwidget) + self.blue_forces_label.setGeometry(QtCore.QRect(690, 180, 241, 31)) font = QtGui.QFont() font.setPointSize(12) - self.scenario_label_2.setFont(font) - self.scenario_label_2.setObjectName("scenario_label_2") - self.scenario_label_3 = QtWidgets.QLabel(self.centralwidget) - self.scenario_label_3.setGeometry(QtCore.QRect(60, 180, 141, 31)) + self.blue_forces_label.setFont(font) + self.blue_forces_label.setObjectName("blue_forces_label") + self.red_forces_label = QtWidgets.QLabel(self.centralwidget) + self.red_forces_label.setGeometry(QtCore.QRect(60, 180, 261, 31)) font = QtGui.QFont() font.setPointSize(12) - self.scenario_label_3.setFont(font) - self.scenario_label_3.setObjectName("scenario_label_3") + self.red_forces_label.setFont(font) + self.red_forces_label.setObjectName("red_forces_label") self.redforces_comboBox = QtWidgets.QComboBox(self.centralwidget) self.redforces_comboBox.setGeometry(QtCore.QRect(170, 230, 291, 31)) self.redforces_comboBox.setObjectName("redforces_comboBox") self.background_label = QtWidgets.QLabel(self.centralwidget) - self.background_label.setGeometry(QtCore.QRect(-40, 440, 801, 371)) + self.background_label.setGeometry(QtCore.QRect(-40, 490, 801, 371)) self.background_label.setAutoFillBackground(False) self.background_label.setStyleSheet("") self.background_label.setText("") @@ -113,7 +113,7 @@ class Ui_MainWindow(object): self.scenario_label_4.setAlignment(QtCore.Qt.AlignCenter) self.scenario_label_4.setObjectName("scenario_label_4") self.game_status_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.game_status_checkBox.setGeometry(QtCore.QRect(810, 790, 191, 16)) + self.game_status_checkBox.setGeometry(QtCore.QRect(810, 760, 191, 16)) font = QtGui.QFont() font.setPointSize(9) self.game_status_checkBox.setFont(font) @@ -121,7 +121,7 @@ class Ui_MainWindow(object): self.game_status_checkBox.setTristate(False) self.game_status_checkBox.setObjectName("game_status_checkBox") self.voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.voiceovers_checkBox.setGeometry(QtCore.QRect(810, 820, 191, 16)) + self.voiceovers_checkBox.setGeometry(QtCore.QRect(810, 790, 191, 16)) font = QtGui.QFont() font.setPointSize(9) self.voiceovers_checkBox.setFont(font) @@ -150,13 +150,14 @@ class Ui_MainWindow(object): self.tankers_checkBox.setChecked(True) self.tankers_checkBox.setObjectName("tankers_checkBox") self.apcs_spawn_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(470, 400, 251, 31)) + self.apcs_spawn_checkBox.setGeometry(QtCore.QRect(450, 420, 251, 31)) font = QtGui.QFont() font.setPointSize(10) self.apcs_spawn_checkBox.setFont(font) + self.apcs_spawn_checkBox.setChecked(True) self.apcs_spawn_checkBox.setObjectName("apcs_spawn_checkBox") self.inf_spawn_spinBox = QtWidgets.QSpinBox(self.centralwidget) - self.inf_spawn_spinBox.setGeometry(QtCore.QRect(680, 360, 71, 31)) + self.inf_spawn_spinBox.setGeometry(QtCore.QRect(670, 340, 51, 31)) font = QtGui.QFont() font.setPointSize(12) self.inf_spawn_spinBox.setFont(font) @@ -176,7 +177,7 @@ class Ui_MainWindow(object): self.forces_hint_label_2.setAlignment(QtCore.Qt.AlignCenter) self.forces_hint_label_2.setObjectName("forces_hint_label_2") self.label = QtWidgets.QLabel(self.centralwidget) - self.label.setGeometry(QtCore.QRect(470, 360, 191, 31)) + self.label.setGeometry(QtCore.QRect(450, 340, 211, 21)) font = QtGui.QFont() font.setPointSize(10) self.label.setFont(font) @@ -190,14 +191,8 @@ class Ui_MainWindow(object): font.setPointSize(11) self.label_2.setFont(font) self.label_2.setObjectName("label_2") - self.scenario_label_6 = QtWidgets.QLabel(self.centralwidget) - self.scenario_label_6.setGeometry(QtCore.QRect(470, 320, 141, 31)) - font = QtGui.QFont() - font.setPointSize(11) - self.scenario_label_6.setFont(font) - self.scenario_label_6.setObjectName("scenario_label_6") self.force_offroad_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.force_offroad_checkBox.setGeometry(QtCore.QRect(810, 760, 191, 16)) + self.force_offroad_checkBox.setGeometry(QtCore.QRect(810, 820, 191, 16)) font = QtGui.QFont() font.setPointSize(9) self.force_offroad_checkBox.setFont(font) @@ -247,15 +242,15 @@ class Ui_MainWindow(object): self.zone_sams_checkBox.setFont(font) self.zone_sams_checkBox.setObjectName("zone_sams_checkBox") self.scenario_label_9 = QtWidgets.QLabel(self.centralwidget) - self.scenario_label_9.setGeometry(QtCore.QRect(740, 490, 171, 31)) + self.scenario_label_9.setGeometry(QtCore.QRect(810, 450, 171, 31)) font = QtGui.QFont() font.setPointSize(10) self.scenario_label_9.setFont(font) self.scenario_label_9.setObjectName("scenario_label_9") self.inf_spawn_voiceovers_checkBox = QtWidgets.QCheckBox(self.centralwidget) - self.inf_spawn_voiceovers_checkBox.setGeometry(QtCore.QRect(470, 430, 251, 31)) + self.inf_spawn_voiceovers_checkBox.setGeometry(QtCore.QRect(810, 720, 251, 31)) font = QtGui.QFont() - font.setPointSize(10) + font.setPointSize(9) self.inf_spawn_voiceovers_checkBox.setFont(font) self.inf_spawn_voiceovers_checkBox.setChecked(True) self.inf_spawn_voiceovers_checkBox.setObjectName("inf_spawn_voiceovers_checkBox") @@ -287,14 +282,51 @@ class Ui_MainWindow(object): self.version_label.setGeometry(QtCore.QRect(920, 840, 241, 21)) self.version_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.version_label.setObjectName("version_label") + self.scenario_label_10 = QtWidgets.QLabel(self.centralwidget) + self.scenario_label_10.setGeometry(QtCore.QRect(140, 410, 241, 31)) + font = QtGui.QFont() + font.setPointSize(11) + self.scenario_label_10.setFont(font) + self.scenario_label_10.setObjectName("scenario_label_10") + self.e_transport_helos_spinBox = QtWidgets.QSpinBox(self.centralwidget) + self.e_transport_helos_spinBox.setGeometry(QtCore.QRect(70, 410, 51, 31)) + font = QtGui.QFont() + font.setPointSize(12) + self.e_transport_helos_spinBox.setFont(font) + self.e_transport_helos_spinBox.setMinimum(0) + self.e_transport_helos_spinBox.setMaximum(8) + self.e_transport_helos_spinBox.setProperty("value", 1) + self.e_transport_helos_spinBox.setObjectName("e_transport_helos_spinBox") + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setGeometry(QtCore.QRect(450, 380, 191, 31)) + font = QtGui.QFont() + font.setPointSize(10) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.troop_drop_spinBox = QtWidgets.QSpinBox(self.centralwidget) + self.troop_drop_spinBox.setGeometry(QtCore.QRect(670, 380, 51, 31)) + font = QtGui.QFont() + font.setPointSize(12) + self.troop_drop_spinBox.setFont(font) + self.troop_drop_spinBox.setMinimum(0) + self.troop_drop_spinBox.setMaximum(10) + self.troop_drop_spinBox.setProperty("value", 4) + self.troop_drop_spinBox.setObjectName("troop_drop_spinBox") + self.smoke_pickup_zone_checkBox = QtWidgets.QCheckBox(self.centralwidget) + self.smoke_pickup_zone_checkBox.setGeometry(QtCore.QRect(810, 690, 251, 31)) + font = QtGui.QFont() + font.setPointSize(9) + self.smoke_pickup_zone_checkBox.setFont(font) + self.smoke_pickup_zone_checkBox.setChecked(True) + self.smoke_pickup_zone_checkBox.setObjectName("smoke_pickup_zone_checkBox") self.background_label.raise_() self.scenario_comboBox.raise_() self.scenario_label.raise_() self.generateButton.raise_() self.description_textBrowser.raise_() self.blueforces_comboBox.raise_() - self.scenario_label_2.raise_() - self.scenario_label_3.raise_() + self.blue_forces_label.raise_() + self.red_forces_label.raise_() self.redforces_comboBox.raise_() self.scenario_hint_label.raise_() self.forces_hint_label.raise_() @@ -313,7 +345,6 @@ class Ui_MainWindow(object): self.label.raise_() self.slot_template_comboBox.raise_() self.label_2.raise_() - self.scenario_label_6.raise_() self.force_offroad_checkBox.raise_() self.defense_checkBox.raise_() self.e_attack_helos_spinBox.raise_() @@ -327,6 +358,11 @@ class Ui_MainWindow(object): self.farp_gunits.raise_() self.farp_always.raise_() self.version_label.raise_() + self.scenario_label_10.raise_() + self.e_transport_helos_spinBox.raise_() + self.label_3.raise_() + self.troop_drop_spinBox.raise_() + self.smoke_pickup_zone_checkBox.raise_() MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 1209, 26)) @@ -344,10 +380,13 @@ class Ui_MainWindow(object): self.action_blueforcesSelected.setObjectName("action_blueforcesSelected") self.action_redforcesSelected = QtWidgets.QAction(MainWindow) self.action_redforcesSelected.setObjectName("action_redforcesSelected") + self.action_defensiveModeChanged = QtWidgets.QAction(MainWindow) + self.action_defensiveModeChanged.setObjectName("action_defensiveModeChanged") self.retranslateUi(MainWindow) self.generateButton.clicked.connect(self.action_generateMission.trigger) self.scenario_comboBox.currentIndexChanged['int'].connect(self.action_scenarioSelected.trigger) + self.defense_checkBox.stateChanged['int'].connect(self.action_defensiveModeChanged.trigger) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -357,13 +396,13 @@ class Ui_MainWindow(object): self.scenario_label.setText(_translate("MainWindow", "Scenario Template:")) self.generateButton.setText(_translate("MainWindow", "Generate Mission")) self.description_textBrowser.setHtml(_translate("MainWindow", "\n" -"\n" -"

Provide close air support for our convoys as we take back Las Vegas from the enemy!

")) +"\n" +"

Provide close air support for our convoys as we take back Las Vegas from the enemy!

")) self.blueforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated.")) - self.scenario_label_2.setText(_translate("MainWindow", "Friendly Forces:")) - self.scenario_label_3.setText(_translate("MainWindow", "Enemy Forces:")) + self.blue_forces_label.setText(_translate("MainWindow", "Friendly Forces:")) + self.red_forces_label.setText(_translate("MainWindow", "Enemy Forces:")) self.redforces_comboBox.setStatusTip(_translate("MainWindow", "Tip: You can create your own custom ground forces groups to be automatically generated.")) self.scenario_hint_label.setText(_translate("MainWindow", "Scenario templates are .miz files in \'Generator/Scenarios\'")) self.forces_hint_label.setText(_translate("MainWindow", "Forces templates are .miz files in \'Generator/Forces\'")) @@ -378,20 +417,22 @@ class Ui_MainWindow(object): self.logistics_crates_checkBox.setText(_translate("MainWindow", "Logistics")) self.awacs_checkBox.setText(_translate("MainWindow", "Friendly AWACS")) self.tankers_checkBox.setText(_translate("MainWindow", "Friendly Tankers")) - self.apcs_spawn_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone.")) + self.apcs_spawn_checkBox.setStatusTip(_translate("MainWindow", "Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops).")) self.apcs_spawn_checkBox.setText(_translate("MainWindow", "APCs Spawn Infantry")) self.inf_spawn_spinBox.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template.")) self.scenario_label_5.setText(_translate("MainWindow", "Groups Per Zone")) self.forces_hint_label_2.setText(_translate("MainWindow", "Forces templates are .miz files in \'Generator/Forces\'")) - self.label.setText(_translate("MainWindow", "Infantry Groups per zone:")) + self.label.setStatusTip(_translate("MainWindow", "This value is multiplied by the number of spawn zones in the mission template.")) + self.label.setText(_translate("MainWindow", "Infantry Spawns per zone:")) self.slot_template_comboBox.setStatusTip(_translate("MainWindow", "Default player/client spawn locations at a friendly airport.")) self.label_2.setText(_translate("MainWindow", "Player Slots")) - self.scenario_label_6.setText(_translate("MainWindow", "Infantry Spawns:")) - self.force_offroad_checkBox.setStatusTip(_translate("MainWindow", "May help prevent long travel times or pathfinding issues. Tip: You can change this dynamically from mission triggers.")) + self.force_offroad_checkBox.setStatusTip(_translate("MainWindow", "May help prevent long travel times or pathfinding issues. ")) self.force_offroad_checkBox.setText(_translate("MainWindow", "Force Offroad")) self.defense_checkBox.setText(_translate("MainWindow", "Defensive Mode")) self.e_attack_helos_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns.")) + self.scenario_label_7.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack helicopter group spawns.")) self.scenario_label_7.setText(_translate("MainWindow", "Enemy Attack Helicopters")) + self.scenario_label_8.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns.")) self.scenario_label_8.setText(_translate("MainWindow", "Enemy Attack Planes")) self.e_attack_planes_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy attack plane group spawns.")) self.zone_sams_checkBox.setStatusTip(_translate("MainWindow", "Inactive conflict zones will be protected by SAMs. When a zone is cleared, SAMs at next active zone will be destroyed.")) @@ -406,10 +447,19 @@ class Ui_MainWindow(object): self.farp_always.setStatusTip(_translate("MainWindow", "Always spawn a FARP in defeated conflict zones.")) self.farp_always.setText(_translate("MainWindow", "Always")) self.version_label.setText(_translate("MainWindow", "Version string")) + self.scenario_label_10.setStatusTip(_translate("MainWindow", "Approximate number of enemy transport helicopter spawns.")) + self.scenario_label_10.setText(_translate("MainWindow", "Enemy Transport Helicopters")) + self.e_transport_helos_spinBox.setStatusTip(_translate("MainWindow", "Approximate number of enemy transport helicopter spawns.")) + self.label_3.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight.")) + self.label_3.setText(_translate("MainWindow", "Transport Drop Points:")) + self.troop_drop_spinBox.setStatusTip(_translate("MainWindow", "The number of troop drops per transport helicopter flight.")) + self.smoke_pickup_zone_checkBox.setStatusTip(_translate("MainWindow", "Infinite troop pickup zones will be marked with blue smoke.")) + self.smoke_pickup_zone_checkBox.setText(_translate("MainWindow", "Smoke at Troop Pickup Zones")) self.action_generateMission.setText(_translate("MainWindow", "_generateMission")) self.action_scenarioSelected.setText(_translate("MainWindow", "_scenarioSelected")) self.action_blueforcesSelected.setText(_translate("MainWindow", "_blueforcesSelected")) self.action_redforcesSelected.setText(_translate("MainWindow", "_redforcesSelected")) + self.action_defensiveModeChanged.setText(_translate("MainWindow", "_defensiveModeChanged")) if __name__ == "__main__": diff --git a/Generator/MissionGeneratorUI.ui b/Generator/MissionGeneratorUI.ui index 7838cc9..1898384 100644 --- a/Generator/MissionGeneratorUI.ui +++ b/Generator/MissionGeneratorUI.ui @@ -96,9 +96,9 @@ padding: 4px; - 710 - 20 - 331 + 670 + 30 + 501 131 @@ -112,10 +112,10 @@ padding: 4px; <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:9pt; font-weight:400; font-style:normal;"> -<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Provide close air support for our convoys as we take back Las Vegas from the enemy!</span></p></body></html> +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:10pt;">Provide close air support for our convoys as we take back Las Vegas from the enemy!</span></p></body></html> @@ -131,12 +131,12 @@ p, li { white-space: pre-wrap; } Tip: You can create your own custom ground forces groups to be automatically generated. - + 690 180 - 141 + 241 31 @@ -149,12 +149,12 @@ p, li { white-space: pre-wrap; } Friendly Forces: - + 60 180 - 141 + 261 31 @@ -184,7 +184,7 @@ p, li { white-space: pre-wrap; } -40 - 440 + 490 801 371 @@ -313,7 +313,7 @@ p, li { white-space: pre-wrap; } 810 - 790 + 760 191 16 @@ -340,7 +340,7 @@ p, li { white-space: pre-wrap; } 810 - 820 + 790 191 16 @@ -432,8 +432,8 @@ p, li { white-space: pre-wrap; } - 470 - 400 + 450 + 420 251 31 @@ -444,18 +444,21 @@ p, li { white-space: pre-wrap; } - Friendly/enemy APCs will drop infantry when reaching a new conflict zone. + Friendly/enemy APCs will drop infantry when reaching a new conflict zone. Disables infinite troop pickups from conflict zones (you must pick up existing troops). APCs Spawn Infantry + + true + - 680 - 360 - 71 + 670 + 340 + 51 31 @@ -517,10 +520,10 @@ p, li { white-space: pre-wrap; } - 470 - 360 - 191 - 31 + 450 + 340 + 211 + 21 @@ -528,8 +531,11 @@ p, li { white-space: pre-wrap; } 10 + + This value is multiplied by the number of spawn zones in the mission template. + - Infantry Groups per zone: + Infantry Spawns per zone: @@ -563,29 +569,11 @@ p, li { white-space: pre-wrap; } Player Slots - - - - 470 - 320 - 141 - 31 - - - - - 11 - - - - Infantry Spawns: - - 810 - 760 + 820 191 16 @@ -596,7 +584,7 @@ p, li { white-space: pre-wrap; } - May help prevent long travel times or pathfinding issues. Tip: You can change this dynamically from mission triggers. + May help prevent long travel times or pathfinding issues. Force Offroad @@ -667,6 +655,9 @@ p, li { white-space: pre-wrap; } 11 + + Approximate number of enemy attack helicopter group spawns. + Enemy Attack Helicopters @@ -685,6 +676,9 @@ p, li { white-space: pre-wrap; } 11 + + Approximate number of enemy attack plane group spawns. + Enemy Attack Planes @@ -740,8 +734,8 @@ p, li { white-space: pre-wrap; } - 740 - 490 + 810 + 450 171 31 @@ -758,15 +752,15 @@ p, li { white-space: pre-wrap; } - 470 - 430 + 810 + 720 251 31 - 10 + 9 @@ -870,14 +864,134 @@ p, li { white-space: pre-wrap; } Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + 140 + 410 + 241 + 31 + + + + + 11 + + + + Approximate number of enemy transport helicopter spawns. + + + Enemy Transport Helicopters + + + + + + 70 + 410 + 51 + 31 + + + + + 12 + + + + Approximate number of enemy transport helicopter spawns. + + + 0 + + + 8 + + + 1 + + + + + + 450 + 380 + 191 + 31 + + + + + 10 + + + + The number of troop drops per transport helicopter flight. + + + Transport Drop Points: + + + + + + 670 + 380 + 51 + 31 + + + + + 12 + + + + The number of troop drops per transport helicopter flight. + + + 0 + + + 10 + + + 4 + + + + + + 810 + 690 + 251 + 31 + + + + + 9 + + + + Infinite troop pickup zones will be marked with blue smoke. + + + Smoke at Troop Pickup Zones + + + true + + background_label scenario_comboBox scenario_label generateButton description_textBrowser blueforces_comboBox - scenario_label_2 - scenario_label_3 + blue_forces_label + red_forces_label redforces_comboBox scenario_hint_label forces_hint_label @@ -896,7 +1010,6 @@ p, li { white-space: pre-wrap; } label slot_template_comboBox label_2 - scenario_label_6 force_offroad_checkBox defense_checkBox e_attack_helos_spinBox @@ -910,6 +1023,11 @@ p, li { white-space: pre-wrap; } farp_gunits farp_always version_label + scenario_label_10 + e_transport_helos_spinBox + label_3 + troop_drop_spinBox + smoke_pickup_zone_checkBox @@ -946,6 +1064,11 @@ p, li { white-space: pre-wrap; } _redforcesSelected + + + _defensiveModeChanged + + @@ -960,8 +1083,8 @@ p, li { white-space: pre-wrap; } 591 - 589 - 409 + -1 + -1 @@ -981,6 +1104,22 @@ p, li { white-space: pre-wrap; } + + defense_checkBox + stateChanged(int) + action_defensiveModeChanged + trigger() + + + 150 + 131 + + + -1 + -1 + + + diff --git a/Generator/RotorOpsGroups.py b/Generator/RotorOpsGroups.py index a9f125d..25bc383 100644 --- a/Generator/RotorOpsGroups.py +++ b/Generator/RotorOpsGroups.py @@ -11,20 +11,21 @@ import random class VehicleTemplate: - class USA: + class CombinedJointTaskForcesBlue: @staticmethod - def invisible_farp(mission, country, position, heading, name, late_activation): - - farp = mission.farp(country, name, position, hidden=False, dead=False, farp_type=dcs.unit.InvisibleFARP) + def zone_farp(mission, country, farp_country, position, heading, name, late_activation): + # ai air units attack farp with late activation units, so we will set it to enemy coalition. It will be captured when frienly units spawn + farp = mission.farp(farp_country, name, position, hidden=False, dead=False, farp_type=dcs.unit.InvisibleFARP) vg = mission.vehicle_group_platoon( country, - name, + name + " Static", [ dcs.vehicles.Unarmed.M_818, dcs.vehicles.AirDefence.Vulcan, - dcs.vehicles.Unarmed.Ural_375 + dcs.vehicles.Unarmed.Ural_375, + dcs.vehicles.Unarmed.M978_HEMTT_Tanker ], position.point_from_heading(45, 7), heading=random.randint(0, 359), diff --git a/Generator/RotorOpsMission.py b/Generator/RotorOpsMission.py index 880a8c0..c9fc812 100644 --- a/Generator/RotorOpsMission.py +++ b/Generator/RotorOpsMission.py @@ -7,14 +7,12 @@ import random import RotorOpsGroups import RotorOpsUnits import time +from MissionGenerator import logger + class RotorOpsMission: - - - - def __init__(self): self.m = dcs.mission.Mission() os.chdir("../") @@ -30,7 +28,7 @@ class RotorOpsMission: self.staging_zones = {} self.spawn_zones = {} self.scripts = {} - + self.res_map = {} class RotorOpsZone: def __init__(self, name: str, flag: int, position: dcs.point, size: int): @@ -56,7 +54,9 @@ class RotorOpsMission: for filename in dir_list: if filename.endswith(".ogg"): #print(filename) - self.m.map_resource.add_resource_file(filename) + key = self.m.map_resource.add_resource_file(filename) + self.res_map[filename] = key + #add all of our lua scripts os.chdir(script_directory) @@ -67,56 +67,86 @@ class RotorOpsMission: for filename in dir_list: if filename.endswith(".lua"): - print("Adding script to mission: " + filename) + logger.info("Adding script to mission: " + filename) self.scripts[filename] = self.m.map_resource.add_resource_file(filename) def getUnitsFromMiz(self, filename, side): - forces = [] + + forces = {} + vehicles = [] + attack_helos = [] + transport_helos = [] + attack_planes = [] + fighter_planes = [] + os.chdir(self.home_dir) os.chdir(self.forces_dir + "/" + side) - print("Looking for " + side + " Forces files in '", os.getcwd(), "' :") + logger.info("Looking for " + side + " Forces files in '" + os.getcwd()) source_mission = dcs.mission.Mission() + try: source_mission.load_file(filename) for country_name in source_mission.coalition.get(side).countries: country_obj = source_mission.coalition.get(side).countries[country_name] for vehicle_group in country_obj.vehicle_group: - forces.append(vehicle_group) - return forces - except: - print("Failed to load units from " + filename) + vehicles.append(vehicle_group) + for helicopter_group in country_obj.helicopter_group: + if helicopter_group.task == 'CAS': + attack_helos.append(helicopter_group) + elif helicopter_group.task == 'Transport': + transport_helos.append(helicopter_group) + for plane_group in country_obj.plane_group: + if plane_group.task == 'CAS': + attack_planes.append(plane_group) + elif plane_group.task == 'CAP': + fighter_planes.append(plane_group) + forces["vehicles"] = vehicles + forces["attack_helos"] = attack_helos + forces["transport_helos"] = transport_helos + forces["attack_planes"] = attack_planes + forces["fighter_planes"] = fighter_planes + + return forces + + except: + logger.error("Failed to load units from " + filename) def generateMission(self, options): - #get the template mission file - os.chdir(self.scenarios_dir) - print("Looking for mission files in '", os.getcwd(), "' :") + logger.info("Looking for mission files in " + os.getcwd()) self.m.load_file(options["scenario_filename"]) - if not self.m.country("Russia") or not self.m.country("USA"): - failure_msg = "You must include a USA and Russia unit in the scenario template. See the instructions in " + self.scenarios_dir + if not self.m.country("Combined Joint Task Forces Red") or not self.m.country("Combined Joint Task Forces Blue"): + failure_msg = "You must include a CombinedJointTaskForcesBlue and CombinedJointTaskForcesRed unit in the scenario template. See the instructions in " + self.scenarios_dir return {"success": False, "failure_msg": failure_msg} - red_forces = self.getUnitsFromMiz(options["red_forces_filename"], "red") blue_forces = self.getUnitsFromMiz(options["blue_forces_filename"], "blue") + # Add coalitions (we may be able to add CJTF here instead of requiring templates to have objects of those coalitions) + self.m.coalition.get("red").add_country(dcs.countries.Russia()) + self.m.coalition.get("blue").add_country(dcs.countries.USA()) + + self.m.add_picture_blue(self.assets_dir + '/briefing1.png') + self.m.add_picture_blue(self.assets_dir + '/briefing2.png') + # add zones to target mission + zone_names = ["ALPHA", "BRAVO", "CHARLIE", "DELTA"] + zone_flag = 101 + for zone_name in zone_names: + for zone in self.m.triggers.zones(): + if zone.name == zone_name: + self.addZone(self.conflict_zones, self.RotorOpsZone(zone_name, zone_flag, zone.position, zone.radius)) + zone_flag = zone_flag + 1 + + for zone in self.m.triggers.zones(): - if zone.name == "ALPHA": - self.addZone(self.conflict_zones, self.RotorOpsZone("ALPHA", 101, zone.position, zone.radius)) - elif zone.name == "BRAVO": - self.addZone(self.conflict_zones, self.RotorOpsZone("BRAVO", 102, zone.position, zone.radius)) - elif zone.name == "CHARLIE": - self.addZone(self.conflict_zones, self.RotorOpsZone("CHARLIE", 103, zone.position, zone.radius)) - elif zone.name == "DELTA": - self.addZone(self.conflict_zones, self.RotorOpsZone("DELTA", 104, zone.position, zone.radius)) - elif zone.name.rfind("STAGING") >= 0: + if zone.name.rfind("STAGING") >= 0: self.addZone(self.staging_zones, self.RotorOpsZone(zone.name, None, zone.position, zone.radius)) elif zone.name.rfind("SPAWN") >= 0: self.addZone(self.spawn_zones, self.RotorOpsZone(zone.name, None, zone.position, zone.radius)) @@ -128,28 +158,25 @@ class RotorOpsMission: blue_zones = self.conflict_zones red_zones = self.staging_zones #swap airport sides - blue_airports = self.getCoalitionAirports("blue") - red_airports = self.getCoalitionAirports("red") - for airport_name in blue_airports: - self.m.terrain.airports[airport_name].set_red() - for airport_name in red_airports: - self.m.terrain.airports[airport_name].set_blue() + self.swapSides(options) + #Populate Red zones with ground units for zone_name in red_zones: - if red_forces: - self.addGroundGroups(red_zones[zone_name], self.m.country('Russia'), red_forces, options["red_quantity"]) + if red_forces["vehicles"]: + self.addGroundGroups(red_zones[zone_name], self.m.country('Combined Joint Task Forces Red'), red_forces["vehicles"], options["red_quantity"]) - #Add blue FARPS + #Add red FARPS if options["zone_farps"] != "farp_never" and not options["defending"]: - RotorOpsGroups.VehicleTemplate.USA.invisible_farp(self.m, self.m.country('USA'), + RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country('Combined Joint Task Forces Blue'), + self.m.country('Combined Joint Task Forces Blue'), red_zones[zone_name].position, 180, zone_name + " FARP", late_activation=True) if options["zone_protect_sams"]: self.m.vehicle_group( - self.m.country('Russia'), + self.m.country('Combined Joint Task Forces Red'), "Static " + zone_name + " Protection SAM", random.choice(RotorOpsUnits.e_zone_sams), red_zones[zone_name].position, @@ -162,13 +189,19 @@ class RotorOpsMission: #Populate Blue zones with ground units for zone_name in blue_zones: - if blue_forces: - self.addGroundGroups(blue_zones[zone_name], self.m.country('USA'), blue_forces, + if blue_forces["vehicles"]: + self.addGroundGroups(blue_zones[zone_name], self.m.country('Combined Joint Task Forces Blue'), blue_forces["vehicles"], options["blue_quantity"]) + #Add blue FARPS + if options["zone_farps"] != "farp_never" and options["defending"]: + RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.zone_farp(self.m, self.m.country('Combined Joint Task Forces Blue'), + self.m.country('Combined Joint Task Forces Blue'), + blue_zones[zone_name].position, + 180, zone_name + " FARP", late_activation=False) #add logistics sites if options["crates"] and zone_name in self.staging_zones: - RotorOpsGroups.VehicleTemplate.USA.logistics_site(self.m, self.m.country('USA'), + RotorOpsGroups.VehicleTemplate.CombinedJointTaskForcesBlue.logistics_site(self.m, self.m.country('Combined Joint Task Forces Blue'), blue_zones[zone_name].position, 180, zone_name) @@ -178,7 +211,7 @@ class RotorOpsMission: if options["zone_protect_sams"] and options["defending"]: vg = self.m.vehicle_group( - self.m.country('USA'), + self.m.country('Combined Joint Task Forces Blue'), "Static " + zone_name + " Protection SAM", random.choice(RotorOpsUnits.e_zone_sams), blue_zones[zone_name].position, @@ -188,20 +221,14 @@ class RotorOpsMission: ) - #Add player slots - if options["slots"] == "Multiple Slots": - self.addMultiplayerHelos() - else: - for helicopter in dcs.helicopters.helicopter_map: - if helicopter == options["slots"]: - self.addSinglePlayerHelos(dcs.helicopters.helicopter_map[helicopter]) + self.addPlayerHelos(options) #Add AI Flights - self.addFlights(options) + self.addFlights(options, red_forces, blue_forces) #Set the Editor Map View - self.m.map.position = self.m.terrain.airports[self.getCoalitionAirports("blue")[0]].position + self.m.map.position = self.conflict_zones["ALPHA"].position self.m.map.zoom = 100000 #add files and triggers necessary for RotorOps.lua script @@ -209,7 +236,6 @@ class RotorOpsMission: self.scriptTriggerSetup(options) #Save the mission file - print(self.m.triggers.zones()) os.chdir(self.output_dir) output_filename = options["scenario_filename"].removesuffix('.miz') + " " + time.strftime('%a%H%M%S') + '.miz' success = self.m.save(output_filename) @@ -224,14 +250,11 @@ class RotorOpsMission: if dcs.vehicles.vehicle_map[unit.type]: unit_types.append(dcs.vehicles.vehicle_map[unit.type]) country = self.m.country(_country.name) - #pos1 = zone.position.point_from_heading(5, 200) - #for i in range(0, quantity): self.m.vehicle_group_platoon( country, zone.name + '-GND ' + str(a+1), unit_types, zone.position.random_point_within(zone.size / 1.2, 100), - #pos1.random_point_within(zone.size / 2.5, 100), heading=random.randint(0, 359), formation=dcs.unitgroup.VehicleGroup.Formation.Scattered, ) @@ -239,66 +262,159 @@ class RotorOpsMission: def getCoalitionAirports(self, side: str): coalition_airports = [] + primary_airport = None + shortest_dist = 1000000 for airport_name in self.m.terrain.airports: airportobj = self.m.terrain.airports[airport_name] if airportobj.coalition == str.upper(side): - coalition_airports.append(airport_name) - return coalition_airports - def getParking(self, airport, aircraft): - slot = airport.free_parking_slot(aircraft) - slots = airport.free_parking_slots(aircraft) - if slot: - return airport - else: - print("No parking available for " + aircraft.id + " at " + airport.name) - return None + coalition_airports.append(airportobj) + + start = self.staging_zones[list(self.staging_zones)[0]] + dist_from_start = dcs.mapping._distance(airportobj.position.x, airportobj.position.y, start.position.x, start.position.y) + + if dist_from_start < shortest_dist: + primary_airport = airportobj + shortest_dist = dist_from_start + + return coalition_airports, primary_airport + + def getParking(self, airport, aircraft, alt_airports=None, group_size=1): + + if len(airport.free_parking_slots(aircraft)) >= group_size: + if not (aircraft.id in dcs.planes.plane_map and len(airport.runways) == 0): + return airport + for airport in alt_airports: + if len(airport.free_parking_slots(aircraft)) >= group_size: + if not (aircraft.id in dcs.planes.plane_map and len(airport.runways) == 0): + return airport + + logger.warn("No parking available for " + aircraft.id) + return None #Find parking spots on FARPs and carriers def getUnitParking(self, aircraft): return - def addSinglePlayerHelos(self, helotype): + def swapSides(self, options): - carrier = self.m.country("USA").find_ship_group(name="HELO_CARRIER") - farp = self.m.country("USA").find_static_group("HELO_FARP") - friendly_airports = self.getCoalitionAirports("blue") + #Swap airports - if carrier: - fg = self.m.flight_group_from_unit(self.m.country('USA'), "CARRIER " + helotype.id, helotype, carrier, dcs.task.CAS, group_size=2) + blue_airports, primary_blue = self.getCoalitionAirports("blue") + red_airports, primary_red = self.getCoalitionAirports("red") - elif farp: - fg = self.m.flight_group_from_unit(self.m.country('USA'), "FARP " + helotype.id, helotype, farp, dcs.task.CAS, group_size=2) - fg.units[0].position = fg.units[0].position.point_from_heading(90, 30) + for airport in blue_airports: + self.m.terrain.airports[airport.name].set_red() + for airport in red_airports: + self.m.terrain.airports[airport.name].set_blue() - # invisible farps need manual unit placement for multiple units - if farp.units[0].type == 'Invisible FARP': - fg.points[0].action = dcs.point.PointAction.FromGroundArea - fg.points[0].type = "TakeOffGround" - fg.units[0].position = fg.units[0].position.point_from_heading(0, 30) + combinedJointTaskForcesBlue = self.m.country("Combined Joint Task Forces Blue") + combinedJointTaskForcesRed = self.m.country("Combined Joint Task Forces Red") - else: - for airport_name in friendly_airports: - fg = self.m.flight_group_from_airport(self.m.country('USA'), airport_name + " " + helotype.id, helotype, - self.getParking(self.m.terrain.airports[airport_name], helotype), group_size=2) - fg.units[0].set_player() + + #Swap ships + + blue_ships = combinedJointTaskForcesBlue.ship_group.copy() + red_ships = combinedJointTaskForcesRed.ship_group.copy() + + for group in blue_ships: + group.points[0].tasks.append(dcs.task.OptROE(dcs.task.OptROE.Values.ReturnFire)) + combinedJointTaskForcesRed.add_ship_group(group) + combinedJointTaskForcesBlue.ship_group.remove(group) + + + for group in red_ships: + combinedJointTaskForcesBlue.add_ship_group(group) + combinedJointTaskForcesRed.ship_group.remove(group) - def addMultiplayerHelos(self): - carrier = self.m.country("USA").find_ship_group(name="HELO_CARRIER") - farp = self.m.country("USA").find_static_group("HELO_FARP") - friendly_airports = self.getCoalitionAirports("blue") + #Swap statics + + blue_statics = combinedJointTaskForcesBlue.static_group.copy() + red_statics = combinedJointTaskForcesRed.static_group.copy() + + for group in blue_statics: + combinedJointTaskForcesBlue.static_group.remove(group) + combinedJointTaskForcesRed.add_static_group(group) + + for group in red_statics: + combinedJointTaskForcesRed.static_group.remove(group) + combinedJointTaskForcesBlue.add_static_group(group) + + + #Swap vehicles + + blue_vehicles = combinedJointTaskForcesBlue.vehicle_group.copy() + red_vehicles = combinedJointTaskForcesRed.vehicle_group.copy() + + for group in blue_vehicles: + combinedJointTaskForcesBlue.vehicle_group.remove(group) + combinedJointTaskForcesRed.add_vehicle_group(group) + + for group in red_vehicles: + combinedJointTaskForcesRed.vehicle_group.remove(group) + combinedJointTaskForcesBlue.add_vehicle_group(group) + + + #Swap planes + + blue_planes = combinedJointTaskForcesBlue.plane_group.copy() + red_planes = combinedJointTaskForcesRed.plane_group.copy() + + for group in blue_planes: + combinedJointTaskForcesBlue.plane_group.remove(group) + combinedJointTaskForcesRed.add_plane_group(group) + + for group in red_planes: + combinedJointTaskForcesRed.plane_group.remove(group) + combinedJointTaskForcesBlue.add_plane_group(group) + + + # Swap helicopters + + blue_helos = combinedJointTaskForcesBlue.helicopter_group.copy() + red_helos = combinedJointTaskForcesRed.helicopter_group.copy() + + for group in blue_helos: + combinedJointTaskForcesBlue.helicopter_group.remove(group) + combinedJointTaskForcesRed.add_helicopter_group(group) + + for group in red_helos: + combinedJointTaskForcesRed.helicopter_group.remove(group) + combinedJointTaskForcesBlue.add_helicopter_group(group) + + + def addPlayerHelos(self, options): + client_helos = RotorOpsUnits.client_helos + for helicopter in dcs.helicopters.helicopter_map: + if helicopter == options["slots"]: + client_helos = [dcs.helicopters.helicopter_map[helicopter]] + + #find friendly carriers and farps + carrier = self.m.country("Combined Joint Task Forces Blue").find_ship_group(name="HELO_CARRIER") + if not carrier: + carrier = self.m.country("Combined Joint Task Forces Blue").find_ship_group(name="HELO_CARRIER_1") + + farp = self.m.country("Combined Joint Task Forces Blue").find_static_group("HELO_FARP") + if not farp: + farp = self.m.country("Combined Joint Task Forces Blue").find_static_group("HELO_FARP_1") + + friendly_airports, primary_f_airport = self.getCoalitionAirports("blue") heading = 0 - for helotype in RotorOpsUnits.client_helos: + group_size = 1 + if len(client_helos) == 1: + group_size = 2 #add a wingman if singleplayer + + for helotype in client_helos: if carrier: - fg = self.m.flight_group_from_unit(self.m.country('USA'), "CARRIER " + helotype.id, helotype, carrier, - dcs.task.CAS, group_size=1) + fg = self.m.flight_group_from_unit(self.m.country('Combined Joint Task Forces Blue'), "CARRIER " + helotype.id, helotype, carrier, + dcs.task.CAS, group_size=group_size) elif farp: - fg = self.m.flight_group_from_unit(self.m.country('USA'), "FARP " + helotype.id, helotype, farp, - dcs.task.CAS, group_size=1) + fg = self.m.flight_group_from_unit(self.m.country('Combined Joint Task Forces Blue'), "FARP " + helotype.id, helotype, farp, + dcs.task.CAS, group_size=group_size) #invisible farps need manual unit placement for multiple units if farp.units[0].type == 'Invisible FARP': @@ -307,10 +423,14 @@ class RotorOpsMission: fg.units[0].position = fg.units[0].position.point_from_heading(heading, 30) heading += 90 else: - for airport_name in friendly_airports: - fg = self.m.flight_group_from_airport(self.m.country('USA'), airport_name + " " + helotype.id, helotype, - self.getParking(self.m.terrain.airports[airport_name], helotype), group_size=1) + fg = self.m.flight_group_from_airport(self.m.country('Combined Joint Task Forces Blue'), primary_f_airport.name + " " + helotype.id, helotype, + self.getParking(primary_f_airport, helotype), group_size=group_size) fg.units[0].set_client() + fg.load_task_default_loadout(dcs.task.CAS) + + #setup wingman for single player + if len(fg.units) == 2: + fg.units[1].skill = dcs.unit.Skill.High class TrainingScenario(): @@ -324,41 +444,75 @@ class RotorOpsMission: race_dist = random.randrange(80 * 1000, 120 * 1000) return dcs.mapping.Point(x1, y1), heading, race_dist + @staticmethod + def perpRacetrack(enemy_heading, friendly_pt): + heading = enemy_heading + random.randrange(70,110) + race_dist = random.randrange(40 * 1000, 80 * 1000) + center_pt = dcs.mapping.point_from_heading(friendly_pt.x, friendly_pt.y, enemy_heading - random.randrange(140, 220), 10000) + pt1 = dcs.mapping.point_from_heading(center_pt[0], center_pt[1], enemy_heading - 90, random.randrange(20 * 1000, 40 * 1000)) + return dcs.mapping.Point(pt1[0], pt1[1]), heading, race_dist + def addFlights(self, options, red_forces, blue_forces): + combinedJointTaskForcesBlue = self.m.country(dcs.countries.CombinedJointTaskForcesBlue.name) + combinedJointTaskForcesRed = self.m.country(dcs.countries.CombinedJointTaskForcesRed.name) + friendly_airports, primary_f_airport = self.getCoalitionAirports("blue") + enemy_airports, primary_e_airport = self.getCoalitionAirports("red") - def addFlights(self, options): - usa = self.m.country(dcs.countries.USA.name) - russia = self.m.country(dcs.countries.Russia.name) - friendly_airport = self.m.terrain.airports[self.getCoalitionAirports("blue")[0]] - enemy_airport = self.m.terrain.airports[self.getCoalitionAirports("red")[0]] + #find enemy carriers and farps + carrier = self.m.country("Combined Joint Task Forces Red").find_ship_group(name="HELO_CARRIER") + if not carrier: + carrier = self.m.country("Combined Joint Task Forces Red").find_ship_group(name="HELO_CARRIER_1") + farp = self.m.country("Combined Joint Task Forces Red").find_static_group("HELO_FARP") + if not farp: + farp = self.m.country("Combined Joint Task Forces Red").find_static_group("HELO_FARP_1") - orbit_rect = dcs.mapping.Rectangle( - int(friendly_airport.position.x), int(friendly_airport.position.y - 100 * 1000), int(friendly_airport.position.x - 100 * 1000), - int(friendly_airport.position.y)) + e_airport_heading = dcs.mapping.heading_between_points( + friendly_airports[0].position.x, friendly_airports[0].position.y, enemy_airports[0].position.x, primary_e_airport.position.y + ) + e_airport_distance = dcs.mapping._distance( + primary_f_airport.position.x, primary_f_airport.position.y, primary_f_airport.position.x, primary_f_airport.position.y + ) if options["f_awacs"]: awacs_name = "AWACS" awacs_freq = 266 - pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + #pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position) awacs = self.m.awacs_flight( - usa, + combinedJointTaskForcesBlue, awacs_name, plane_type=dcs.planes.E_3A, - airport=self.getParking(friendly_airport, dcs.planes.E_3A), + airport=self.getParking(primary_f_airport, dcs.planes.E_3A, friendly_airports), position=pos, race_distance=race_dist, heading=heading, altitude=random.randrange(4000, 5500, 100), frequency=awacs_freq) + # AWACS Escort flight + source_plane = None + if blue_forces["fighter_planes"]: + source_group = random.choice(blue_forces["fighter_planes"]) + source_plane = source_group.units[0] + plane_type = source_plane.unit_type + else: + plane_type = dcs.countries.CombinedJointTaskForcesBlue.Plane.F_15C + awacs_escort = self.m.escort_flight( - usa, "AWACS Escort", - dcs.countries.USA.Plane.F_15C, - airport=self.getParking(friendly_airport, dcs.countries.USA.Plane.F_15C), + combinedJointTaskForcesBlue, "AWACS Escort", + plane_type, + airport=self.getParking(primary_f_airport, plane_type, friendly_airports), group_to_escort=awacs, group_size=2) - awacs_escort.load_loadout("Combat Air Patrol") #not working for f-15 + + awacs_escort.points[0].tasks.append(dcs.task.OptROE(dcs.task.OptROE.Values.WeaponFree)) + + if source_plane: + for unit in awacs_escort.units: + unit.pylons = source_plane.pylons + unit.livery_id = source_plane.livery_id + #add text to mission briefing with radio freq briefing = self.m.description_text() + "\n\n" + awacs_name + " " + str(awacs_freq) + ".00 " + "\n" @@ -371,12 +525,13 @@ class RotorOpsMission: t2_name = "Tanker KC_135 Boom" t2_freq = 256 t2_tac = "101Y" - pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + #pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position) refuel_net = self.m.refuel_flight( - usa, + combinedJointTaskForcesBlue, t1_name, dcs.planes.KC130, - airport=self.getParking(friendly_airport, dcs.planes.KC130), + airport=self.getParking(primary_f_airport, dcs.planes.KC130, friendly_airports), position=pos, race_distance=race_dist, heading=heading, @@ -386,12 +541,13 @@ class RotorOpsMission: frequency=t1_freq, tacanchannel=t1_tac) - pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + #pos, heading, race_dist = self.TrainingScenario.random_orbit(orbit_rect) + pos, heading, race_dist = self.TrainingScenario.perpRacetrack(e_airport_heading, primary_f_airport.position) refuel_rod = self.m.refuel_flight( - usa, + combinedJointTaskForcesBlue, t2_name, dcs.planes.KC_135, - airport=self.getParking(friendly_airport, dcs.planes.KC_135), + airport=self.getParking(primary_f_airport, dcs.planes.KC_135, friendly_airports), position=pos, race_distance=race_dist, heading=heading, altitude=random.randrange(4000, 5500, 100), @@ -403,52 +559,141 @@ class RotorOpsMission: briefing = self.m.description_text() + "\n\n" + t1_name + " " + str(t1_freq) + ".00 " + t1_tac + "\n" + t2_name + " " + str(t2_freq) + ".00 " + t2_tac + "\n" self.m.set_description_text(briefing) - def zone_attack(fg, unit_type): - fg.set_skill(dcs.unit.Skill.Random) + def zone_attack(fg, airport): + fg.set_skill(dcs.unit.Skill.High) fg.late_activation = True - fg.points[0].tasks.append(dcs.task.OptROE(0)) - #fg.load_loadout(unit_type["loadout"]) - #task = dcs.task.CAS - #loadout = dcs.planes.Su_25.loadout(task) - #loadout = dcs.planes.Su_25.loadout_by_name("Ground Attack") - #fg.load_task_default_loadout(task) - #fg.load_loadout("Ground Attack") - #fg.load_task_default_loadout(dcs.task.GroundAttack) - #fg.load_loadout("2xB-13L+4xATGM 9M114") if options["defending"]: for zone_name in self.conflict_zones: fg.add_waypoint(self.conflict_zones[zone_name].position, 1000) else: for zone_name in reversed(self.conflict_zones): fg.add_waypoint(self.conflict_zones[zone_name].position, 1000) - fg.add_runway_waypoint(enemy_airport) - fg.land_at(enemy_airport) + if hasattr(airport, 'runways'): + fg.add_runway_waypoint(airport) + if airport: + fg.land_at(airport) + fg.points[0].tasks.append(dcs.task.OptReactOnThreat(dcs.task.OptReactOnThreat.Values.EvadeFire)) + fg.points[0].tasks.append(dcs.task.OptROE(dcs.task.OptROE.Values.OpenFire)) + if options["e_attack_helos"]: - helo = random.choice(RotorOpsUnits.e_attack_helos) - afg = self.m.flight_group_from_airport( - russia, - "Enemy Attack Helicopters", - helo, - airport=enemy_airport, - maintask=dcs.task.CAS, - start_type=dcs.mission.StartType.Cold, - group_size=2) - zone_attack(afg, helo) + source_helo = None + if red_forces["attack_helos"]: + source_group = random.choice(red_forces["attack_helos"]) + source_helo = source_group.units[0] + helo_type = source_helo.unit_type + group_size = len(source_group.units) + if group_size > 2: + group_size = 2 + + else: + group_size = 2 + helo_type = random.choice(RotorOpsUnits.e_attack_helos) + + airport = self.getParking(primary_e_airport, helo_type, enemy_airports, group_size) + + if carrier: + afg = self.m.flight_group_from_unit( + combinedJointTaskForcesRed, + "Enemy Attack Helicopters", + helo_type, + carrier, + maintask=dcs.task.CAS, + start_type=dcs.mission.StartType.Cold, + group_size=group_size) + zone_attack(afg, carrier) + + elif farp: + afg = self.m.flight_group_from_unit( + combinedJointTaskForcesRed, + "Enemy Attack Helicopters", + helo_type, + farp, + maintask=dcs.task.CAS, + start_type=dcs.mission.StartType.Cold, + group_size=group_size) + zone_attack(afg, farp) + + elif airport: + afg = self.m.flight_group_from_airport( + combinedJointTaskForcesRed, + "Enemy Attack Helicopters", + helo_type, + airport=airport, + maintask=dcs.task.CAS, + start_type=dcs.mission.StartType.Cold, + group_size=group_size) + zone_attack(afg, airport) + + else: + return + + if source_helo and afg: + for unit in afg.units: + unit.pylons = source_helo.pylons + unit.livery_id = source_helo.livery_id + + if options["e_attack_planes"]: - plane = random.choice(RotorOpsUnits.e_attack_planes) - afg = self.m.flight_group_from_airport( - russia, "Enemy Attack Planes", plane["type"], - airport=enemy_airport, - maintask=dcs.task.CAS, - start_type=dcs.mission.StartType.Cold, - group_size=2) - zone_attack(afg, plane) + source_plane = None + if red_forces["attack_planes"]: + source_group = random.choice(red_forces["attack_planes"]) + source_plane = source_group.units[0] + plane_type = source_plane.unit_type + group_size = len(source_group.units) + if group_size > 2: + group_size = 2 + else: + group_size = 2 + plane_type = random.choice(RotorOpsUnits.e_attack_planes) + airport = self.getParking(primary_e_airport, plane_type, enemy_airports, group_size) + if airport: + afg = self.m.flight_group_from_airport( + combinedJointTaskForcesRed, "Enemy Attack Planes", plane_type, + airport=airport, + maintask=dcs.task.CAS, + start_type=dcs.mission.StartType.Cold, + group_size=group_size) + zone_attack(afg, airport) + + if source_plane: + for unit in afg.units: + unit.pylons = source_plane.pylons + unit.livery_id = source_plane.livery_id + + if options["e_transport_helos"]: + source_helo = None + if red_forces["transport_helos"]: + source_group = random.choice(red_forces["transport_helos"]) + source_helo = source_group.units[0] + helo_type = source_helo.unit_type + group_size = len(source_group.units) + if group_size > 2: + group_size = 2 + else: + group_size = 1 + helo_type = random.choice(RotorOpsUnits.e_transport_helos) + + airport = self.getParking(primary_e_airport, helo_type, enemy_airports, group_size) + if airport: + afg = self.m.flight_group_from_airport( + combinedJointTaskForcesRed, "Enemy Transport Helicopters", helo_type, + airport=airport, + maintask=dcs.task.Transport, + start_type=dcs.mission.StartType.Cold, + group_size=group_size) + afg.late_activation = True + afg.units[0].skill = dcs.unit.Skill.Excellent + + if source_helo: + for unit in afg.units: + unit.pylons = source_helo.pylons + unit.livery_id = source_helo.livery_id def scriptTriggerSetup(self, options): @@ -464,8 +709,8 @@ class RotorOpsMission: trig.actions.append(dcs.action.DoScriptFile(self.scripts["Splash_Damage_2_0.lua"])) trig.actions.append(dcs.action.DoScriptFile(self.scripts["CTLD.lua"])) trig.actions.append(dcs.action.DoScriptFile(self.scripts["RotorOps.lua"])) - trig.actions.append(dcs.action.DoScript(dcs.action.String(( - "--OPTIONS HERE!\n\n" + + script = "" + script = ("--OPTIONS HERE!\n\n" + "RotorOps.CTLD_crates = " + lb("crates") + "\n\n" + "RotorOps.CTLD_sound_effects = true\n\n" + "RotorOps.force_offroad = " + lb("force_offroad") + "\n\n" + @@ -473,7 +718,10 @@ class RotorOpsMission: "RotorOps.zone_status_display = " + lb("game_display") + "\n\n" + "RotorOps.inf_spawn_messages = " + lb("inf_spawn_msgs") + "\n\n" + "RotorOps.inf_spawns_per_zone = " + lb("inf_spawn_qty") + "\n\n" + - "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n")))) + "RotorOps.apcs_spawn_infantry = " + lb("apc_spawns_inf") + " \n\n") + if not options["smoke_pickup_zones"]: + script = script + 'RotorOps.pickup_zone_smoke = "none"\n\n' + trig.actions.append(dcs.action.DoScript(dcs.action.String((script)))) self.m.triggerrules.triggers.append(trig) #Add the second trigger @@ -510,31 +758,37 @@ class RotorOpsMission: self.m.triggerrules.triggers.append(z_sams_trig) #Zone FARPS always - if options["zone_farps"] == "farp_always" and not options["defending"] and index > 0: + if options["zone_farps"] == "farp_always" and not options["defending"]: for index, zone_name in enumerate(self.conflict_zones): if index > 0: previous_zone = list(self.conflict_zones)[index - 1] - if not self.m.country("USA").find_group(previous_zone + " FARP"): + if not self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static"): continue z_farps_trig = dcs.triggers.TriggerOnce(comment="Activate " + previous_zone + " FARP") z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1)) - z_farps_trig.actions.append(dcs.action.ActivateGroup(self.m.country("USA").find_group(previous_zone + " FARP").id)) + z_farps_trig.actions.append(dcs.action.ActivateGroup(self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static").id)) + #z_farps_trig.actions.append(dcs.action.SoundToAll(str(self.res_map['forward_base_established.ogg']))) + z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String( + "RotorOps.farpEstablished(" + str(index) + ")"))) self.m.triggerrules.triggers.append(z_farps_trig) #Zone FARPS conditional on staged units remaining - if options["zone_farps"] == "farp_gunits": + if options["zone_farps"] == "farp_gunits" and not options["defending"]: for index, zone_name in enumerate(self.conflict_zones): if index > 0: previous_zone = list(self.conflict_zones)[index - 1] - if not self.m.country("USA").find_group(previous_zone + " FARP"): + if not self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static"): continue z_farps_trig = dcs.triggers.TriggerOnce(comment= "Activate " + previous_zone + " FARP") z_farps_trig.rules.append(dcs.condition.FlagEquals(game_flag, index + 1)) z_farps_trig.rules.append(dcs.condition.FlagIsMore(111, 20)) z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String("--The 100 flag indicates which zone is active. The 111 flag value is the percentage of staged units remaining"))) z_farps_trig.actions.append( - dcs.action.ActivateGroup(self.m.country("USA").find_group(previous_zone + " FARP").id)) + dcs.action.ActivateGroup(self.m.country("Combined Joint Task Forces Blue").find_group(previous_zone + " FARP Static").id)) + #z_farps_trig.actions.append(dcs.action.SoundToAll(str(self.res_map['forward_base_established.ogg']))) + z_farps_trig.actions.append(dcs.action.DoScript(dcs.action.String( + "RotorOps.farpEstablished(" + str(index) + ")"))) self.m.triggerrules.triggers.append(z_farps_trig) @@ -546,7 +800,7 @@ class RotorOpsMission: z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Helo") z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1)) z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90))) - z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag value represents the percentage of defending ground units remaining in zone. "))) + z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. "))) z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackHelos()"))) self.m.triggerrules.triggers.append(z_weak_trig) @@ -557,10 +811,24 @@ class RotorOpsMission: z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Attack Plane") z_weak_trig.rules.append(dcs.condition.FlagIsMore(zone.flag, 1)) z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 90))) - z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag value represents the percentage of defending ground units remaining in zone. "))) + z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. "))) z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnAttackPlanes()"))) self.m.triggerrules.triggers.append(z_weak_trig) + #Add transport helos triggers + for index in range(options["e_transport_helos"]): + random_zone_index = random.randrange(1, len(self.conflict_zones)) + random_zone_obj = list(self.conflict_zones.items())[random_zone_index] + zone = random_zone_obj[1] + z_weak_trig = dcs.triggers.TriggerOnce(comment=zone.name + " Transport Helo") + z_weak_trig.rules.append(dcs.condition.FlagEquals(game_flag, random_zone_index + 1)) + z_weak_trig.rules.append(dcs.condition.FlagIsLess(zone.flag, random.randrange(20, 100))) + z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String( + "---Flag " + str(game_flag) + " value represents the index of the active zone. "))) + z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("---Flag " + str(zone.flag) + " value represents the percentage of defending ground units remaining in zone. "))) + z_weak_trig.actions.append(dcs.action.DoScript(dcs.action.String("RotorOps.spawnTranspHelos(8," + str(options["transport_drop_qty"]) + ")"))) + self.m.triggerrules.triggers.append(z_weak_trig) + #Add game won/lost triggers trig = dcs.triggers.TriggerOnce(comment="RotorOps Conflict WON") trig.rules.append(dcs.condition.FlagEquals(game_flag, 99)) diff --git a/Generator/RotorOpsUnits.py b/Generator/RotorOpsUnits.py index 837d793..4bfda12 100644 --- a/Generator/RotorOpsUnits.py +++ b/Generator/RotorOpsUnits.py @@ -14,15 +14,14 @@ e_attack_helos = [ ] e_transport_helos = [ - dcs.helicopters.Mi_26, - dcs.helicopters.Mi_24P, - dcs.helicopters.Mi_8MT, + #dcs.helicopters.Mi_26, + #dcs.helicopters.Mi_24P, + #dcs.helicopters.Mi_8MT, + dcs.helicopters.CH_47D, ] e_attack_planes = [ - #{'type': dcs.planes.Su_34, 'loadout': "APU-8 Vikhr-M*2,Kh-25ML,R-73*2,SPPU-22*2,Mercury LLTV Pod,MPS-410"}, - #{'type': dcs.planes.Su_25, 'loadout': "RKB-250*8,R-60M*2"}, - {'type': dcs.planes.A_10C, 'loadout': ""} + dcs.planes.A_10C, ] e_zone_sams = [ diff --git a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz b/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz index f44a18e..e3734cf 100644 Binary files a/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz and b/Generator/Scenarios/Caucasus Conflict - Batumi to Kobuleti (GRIMM).miz differ diff --git a/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz b/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz index d55089b..22aaf14 100644 Binary files a/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz and b/Generator/Scenarios/Caucasus Conflict - Nalchik to Beslan (GRIMM).miz differ diff --git a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz b/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz index a1bed71..8f0b814 100644 Binary files a/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz and b/Generator/Scenarios/Mariana Conflict - Anderson to Won Pat (GRIMM).miz differ diff --git a/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz new file mode 100644 index 0000000..8d032c6 Binary files /dev/null and b/Generator/Scenarios/Mariana Conflict - Rota Landing (Mr Nobody).miz differ diff --git a/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz b/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz index 38828de..9235775 100644 Binary files a/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz and b/Generator/Scenarios/Nevada Conflict - Vegas Tour (GRIMM).miz differ diff --git a/Generator/Scenarios/PG Conflict - Abu Dhabi to Ras (GRIMM).miz b/Generator/Scenarios/PG Conflict - Abu Dhabi to Ras (GRIMM).miz deleted file mode 100644 index 68638fb..0000000 Binary files a/Generator/Scenarios/PG Conflict - Abu Dhabi to Ras (GRIMM).miz and /dev/null differ diff --git a/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz b/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz new file mode 100644 index 0000000..39550a1 Binary files /dev/null and b/Generator/Scenarios/PG Conflict - Dubai Tour (GRIMM).miz differ diff --git a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz b/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz index 9e3d50d..4de763d 100644 Binary files a/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz and b/Generator/Scenarios/Syria Conflict - Aleppo Tour (GRIMM).miz differ diff --git a/Generator/Scenarios/_How to create your own scenarios.txt b/Generator/Scenarios/_How to create your own scenarios.txt index 9161629..fa6dbc3 100644 --- a/Generator/Scenarios/_How to create your own scenarios.txt +++ b/Generator/Scenarios/_How to create your own scenarios.txt @@ -3,7 +3,7 @@ You can add your own scenarios in this directory and they will appear in the mis A scenario .miz file MUST have: -1) Between 1-4 trigger zones called "ALPHA", "BRAVO", "CHARLIE", "DELTA" +1) Between 1-4 circular trigger zones called "ALPHA", "BRAVO", "CHARLIE", "DELTA" 2) At least one trigger zone with a name that starts with "STAGING". 3) A blue airport (recommend somewhere near/on-side your staging zone). 4) A red airport (recommend somewhere near/on-side your last conflict zone). @@ -13,16 +13,17 @@ A scenario .miz file MUST have: Optional: -7) USA FARP called "HELO_FARP" for automatic player placement. +7) USA FARP called "HELO_FARP" for automatic player placement. (Strongly suggest using immortal and invisible options for support vehicles, and add "static" to the group name to keep them from moving) 8) USA Carrier called "HELO_CARRIER" for automatic player placement. 9) Infantry spawn zones inside conflict zones. Add near buildings to simulate infantry hiding within. Name trigger zones like "ALPHA_SPAWN", "ALPHA_SPAWN_2, etc. +Testing: +You should test your scenarios to ensure that vehicals move between zones as you expect. In some circumstances, vehicles may not be able to calculate a valid route to the next zone, and they will stop. Make sure they can route correctly by testing your scenario in fast forward, with few defending units. You can also get a good idea as to how long your scenario will take to complete. Tips: --Position the center of conflict zones over an open area, as this position may be used to spawn units. --Position the center of staging zones over an open area of at least 1000 x 1000ft to provide space for logistics units. -Position the center of staging zones over an open area of at least 1000 x 1000ft to provide space for logistics units. +-Position the center of conflict zones over an open area, as this position will be used to spawn units and FARPs. +-Position the center of staging zones over an open area of at least 1000 x 1000ft to provide space for logistics zones. -For very scenery dense areas like forests or urban areas, smaller conflict zone sizes are highly recommended (eg 4000ft radius). A zone center near a roadway may also help keep units moving smoothly. -The conflict game type can be played with blue forces on defense. In this mode the last conflict zone is the only troop pickup zone. -Design your template so that it can be played in normal 'attacking' mode or 'defending' the conflict zone from enemy ground units starting from the staging zone. @@ -35,4 +36,6 @@ Position the center of staging zones over an open area of at least 1000 x 1000ft -Friendly AWACs and tankers will be placed at the blue airport if parking is available, otherwise they will start in the air. -Enemy helicopters and planes will spawn at the red airport. -Late activation FARPs might be useful for rearming far from the player spawn point. - +-In "Defense" or with "Swap sides" option, USA and Russia ships, helicopters, planes, and ground units will swap countries. Static objects may not. Test it out. +-Turn off or limit civilian road traffic. +-Pay attention to rivers and bridges, as a far away bridge crossing may break routing if it's the only way across. See the testing notes above. diff --git a/Generator/assets/briefing1.png b/Generator/assets/briefing1.png new file mode 100644 index 0000000..2fc2585 Binary files /dev/null and b/Generator/assets/briefing1.png differ diff --git a/Generator/assets/briefing2.png b/Generator/assets/briefing2.png new file mode 100644 index 0000000..e5d661b Binary files /dev/null and b/Generator/assets/briefing2.png differ diff --git a/Generator/requirements.txt b/Generator/requirements.txt new file mode 100644 index 0000000..dfe21b6 Binary files /dev/null and b/Generator/requirements.txt differ diff --git a/Generator/utils/extract units/main.py b/Generator/utils/extract units/main.py new file mode 100644 index 0000000..ca5493a --- /dev/null +++ b/Generator/utils/extract units/main.py @@ -0,0 +1,39 @@ +import dcs +import os + + +def main(): + filename = "source.miz" + print("Attempting to extract units from " + filename + " relative to 'HELO_FARP' initial point.") + + source_mission = dcs.mission.Mission() + source_mission.load_file(filename) + fo = open("units.txt", "w") + + usa = source_mission.country("USA") + initial_point = usa.find_static_group("HELO_FARP").position + + def p(mystring): + fo.write(mystring + '\n') + print(mystring) + + group_types = [usa.static_group, usa.vehicle_group, usa.helicopter_group, usa.plane_group, usa.ship_group] + + for group_type in group_types: + for group in group_type: + for unit in group.units: + print(str(unit.position.x)) + x_rel = initial_point.x - unit.position.x + y_rel = initial_point.y - unit.position.y + heading = unit.heading + + p(unit.type) + p("x: " + str(round(x_rel, 7))) + p("y: " + str(round(y_rel, 7))) + p("h: " + str(round(heading, 2))) + p('\n') + + fo.close() + + +main() diff --git a/MissionGenerator.exe b/MissionGenerator.exe index cb34568..0f309bf 100644 Binary files a/MissionGenerator.exe and b/MissionGenerator.exe differ diff --git a/RotorOps.lua b/RotorOps.lua index 3ca3b65..6e2b9a2 100644 --- a/RotorOps.lua +++ b/RotorOps.lua @@ -1,8 +1,9 @@ RotorOps = {} -RotorOps.version = "1.2.5" +RotorOps.version = "1.2.6" local debug = true + ---[[ROTOROPS OPTIONS]]--- --- Protip: change these options from the mission editor rather than changing the script file itself. See documentation on github for details. @@ -15,6 +16,7 @@ RotorOps.max_units_left = 0 --allow clearing the zone when a few units are left RotorOps.force_offroad = false --affects "move_to_zone" tasks only RotorOps.apcs_spawn_infantry = false --apcs will unload troops when arriving to a new zone RotorOps.auto_push = true --should attacking ground units move to the next zone after clearing? +RotorOps.defending_vehicles_disperse = true RotorOps.inf_spawns_avail = 0 --this is the number of infantry group spawn events remaining in the active zone RotorOps.inf_spawn_chance = 25 -- 0-100 the chance of spawning infantry in an active zone spawn zone, per 'assessUnitsInZone' loop (10 seconds) @@ -28,6 +30,7 @@ RotorOps.transports = {'UH-1H', 'Mi-8MT', 'Mi-24P', 'SA342M', 'SA342L', 'SA342Mi RotorOps.CTLD_crates = false RotorOps.CTLD_sound_effects = true --sound effects for troop pickup/dropoffs RotorOps.exclude_ai_group_name = "Static" --include this somewhere in a group name to exclude the group from being tasked in the active zone +RotorOps.pickup_zone_smoke = "blue" ---[[END OF OPTIONS]]--- @@ -67,6 +70,7 @@ local inf_spawn_zones = {} local cooldown = { ["attack_helo_msg"] = 0, ["attack_plane_msg"] = 0, + ["trans_helo_msg"] = 0, } @@ -164,13 +168,23 @@ RotorOps.gameMsgs = { {'ENEMY TROOPS IN THE ACTIVE!', 'e_infantry_spawn5.ogg'}, {'VISUAL ON ENEMY TROOPS!', 'e_infantry_spawn6.ogg'}, }, + farp_established = { + {'NEW FARP AVAILABLE!', 'forward_base_established.ogg'}, + {'NEW FARP AT ALPHA!', 'forward_base_established.ogg'}, + {'NEW FARP AT BRAVO!', 'forward_base_established.ogg'}, + {'NEW FARP AT CHARLIE!', 'forward_base_established.ogg'}, + {'NEW FARP AT DELTA!', 'forward_base_established.ogg'}, + }, + transp_helos_toff = { + {'ENEMY TRANSPORT HELICOPTERS INBOUND!', 'enemy_chopper_inbound.ogg'}, + }, } local sound_effects = { - ["troop_pickup"] = {'troops_load_ao.ogg', 'troops_load_ready.ogg', 'troops_load_to_action.ogg',force_offroad = true}, + ["troop_pickup"] = {'troops_load_ao.ogg', 'troops_load_ready.ogg', 'troops_load_to_action.ogg',}, ["troop_dropoff"] = {'troops_unload_thanks.ogg', 'troops_unload_everybody_off.ogg', 'troops_unload_get_off.ogg', 'troops_unload_here_we_go.ogg', 'troops_unload_moving_out.ogg',}, } @@ -184,15 +198,15 @@ end function RotorOps.eventHandler:onEvent(event) ---ENGINE STARTUP EVENTS if (world.event.S_EVENT_ENGINE_STARTUP == event.id) then --play some sound files when a player starts engines - local initiator = event.initiator:getGroup():getID() + local initiator = event.initiator:getGroup():getID() - if #event.initiator:getGroup():getUnits() == 1 then --if there are no other units in the player flight group (preventing duplicated messages for ai wingman flights) - if RotorOps.defending then - trigger.action.outSoundForGroup(initiator , RotorOps.gameMsgs.enemy_pushing[RotorOps.active_zone_index + 1][2]) - else - trigger.action.outSoundForGroup(initiator , RotorOps.gameMsgs.push[RotorOps.active_zone_index + 1][2]) - end - end + if #event.initiator:getGroup():getUnits() == 1 then --if there are no other units in the player flight group (preventing duplicated messages for ai wingman flights) + if RotorOps.defending then + trigger.action.outSoundForGroup(initiator , RotorOps.gameMsgs.enemy_pushing[RotorOps.active_zone_index + 1][2]) + else + trigger.action.outSoundForGroup(initiator , RotorOps.gameMsgs.push[RotorOps.active_zone_index + 1][2]) + end + end end @@ -219,8 +233,28 @@ function RotorOps.eventHandler:onEvent(event) end end + if initiator_name == "Enemy Transport Helicopters" then --we're using mist clone now so group name will not match + env.info("Transport helicopter took off") + + if ((RotorOps.getTime() - cooldown["trans_helo_msg"]) > 90) then + timer.scheduleFunction(function()RotorOps.gameMsg(RotorOps.gameMsgs.transp_helos_toff) end, {}, timer.getTime() + 1) + cooldown["trans_helo_msg"] = RotorOps.getTime() + else + env.warning("RotorOps transport helo message skipped") + end + end + end + ---BASE CAPTURE EVENTS --doesn't work with FARPs.. + if (world.event.S_EVENT_BASE_CAPTURED == event.id) then + env.info("Base captured") + if (event.place:getCoalition() == 2) then + env.info("Blue forces captured a base via place attribute") + end + end + + end @@ -258,7 +292,8 @@ end local function debugTable(table) - trigger.action.outText("dbg: ".. mist.utils.tableShow(table), 5) + --trigger.action.outText("dbg: ".. mist.utils.tableShow(table), 5) + env.info("ROTOROPS_DEBUG: ".. mist.utils.tableShow(table)) end @@ -357,6 +392,7 @@ local function processMsgBuffer(vars) if #game_message_buffer > 0 then local message = table.remove(game_message_buffer, 1) trigger.action.outText(message[1], 10, true) + env.info("RotorOps: "..message[1]) if RotorOps.voice_overs then trigger.action.outSound(message[2]) end @@ -422,45 +458,7 @@ function RotorOps.spawnGroupOnGroup(grp, src_grp_name, ai_task) --allow to spawn end end ---Spawn infantry in a trigger zone. Uses CTLD but may use another method in the future. Side is "red" or "blue" ---function RotorOps.spawnInfantryInZone(vars) --- --local group = {mg=1,at=0,aa=0,inf=4,mortar=0} --- --- local _triggerName = vars.zone --- local _groupSide = vars.side --- local _number = vars.qty --- local _searchRadius = 500 --- --- local _spawnTrigger = trigger.misc.getZone(_triggerName) -- trigger to use as reference position --- --- if _spawnTrigger == nil then --- env.warning("ERROR: Cant find zone called " .. _triggerName) --- return --- end --- --- local _country --- if _groupSide == "red" then --- _groupSide = 1 --- _country = 0 --- else --- _groupSide = 2 --- _country = 2 --- end --- --- if _searchRadius < 0 then --- _searchRadius = 0 --- end --- --- local _pos2 = { x = _spawnTrigger.point.x, y = _spawnTrigger.point.z } --- local _alt = land.getHeight(_pos2) --- local _pos3 = { x = _pos2.x, y = _alt, z = _pos2.y } --- --- local _groupDetails = ctld.generateTroopTypes(_groupSide, _number, _country) --- --- local _droppedTroops = ctld.spawnDroppedGroup(_pos3, _groupDetails, false, _searchRadius); --- --debugMsg(_groupDetails.groupName) --- return _groupDetails.groupName --_ { units = _troops, groupId = _groupId, groupName = string.format("%s %i", _groupName, _groupId), side = _side, country = _country, weight = _weight, jtac = _hasJTAC } ---end + --Easy way to deploy troops from a vehicle with waypoint action. Spawns from the first valid unit found in a group function RotorOps.deployTroops(quantity, target_group, announce) @@ -470,6 +468,7 @@ function RotorOps.deployTroops(quantity, target_group, announce) else target_group_obj = target_group end + debugMsg("DeployTroops on group: "..target_group_obj:getName()) local valid_unit = RotorOps.getValidUnitFromGroup(target_group_obj) if not valid_unit then return end local coalition = valid_unit:getCoalition() @@ -811,6 +810,7 @@ function RotorOps.aiExecute(vars) local speed = RotorOps.ground_speed local force_offroad = RotorOps.force_offroad mist.groupToPoint(group_name, RotorOps.active_zone, formation, final_heading, speed, force_offroad) + end end @@ -882,6 +882,14 @@ function RotorOps.assessUnitsInZone(var) RotorOps.aiTask(group, "clear_zone", RotorOps.active_zone) end end + + for index, group in pairs(RotorOps.ai_defending_vehicle_groups) do + if group then + Group.getByName(group):getController():setOption(AI.Option.Ground.id.DISPERSE_ON_ATTACK , RotorOps.defending_vehicles_disperse) + end + end + + --FIRES ONCE PER ZONE ACTIVATION @@ -945,7 +953,7 @@ function RotorOps.assessUnitsInZone(var) local percent_staged_remain = 0 percent_staged_remain = math.floor((#staged_units_remaining / #RotorOps.staged_units) * 100) trigger.action.setUserFlag(RotorOps.staged_units_flag, percent_staged_remain) - debugMsg("Staged units remaining: "..percent_staged_remain.."%") + debugMsg("Staged units remaining percent: "..percent_staged_remain.."%") --is the game finished? @@ -1156,7 +1164,7 @@ function RotorOps.setActiveZone(new_index) if not RotorOps.defending then - if old_index > 0 then + if old_index > 0 and RotorOps.apcs_spawn_infantry == false then ctld.activatePickupZone(RotorOps.zones[old_index].name) --make the captured zone a pickup zone end ctld.deactivatePickupZone(RotorOps.zones[new_index].name) @@ -1176,7 +1184,8 @@ function RotorOps.setActiveZone(new_index) local staged_groups = RotorOps.groupsFromUnits(RotorOps.staged_units) for index, group in pairs(staged_groups) do - RotorOps.aiTask(group,"move_to_active_zone", RotorOps.zones[RotorOps.active_zone_index].name) --send vehicles to next zone; use move_to_active_zone so units don't get stuck if the active zone moves before they arrive + timer.scheduleFunction(function()RotorOps.aiTask(group,"move_to_active_zone", RotorOps.zones[RotorOps.active_zone_index].name) end, {}, timer.getTime() + index) --add a second between calling aitask + --RotorOps.aiTask(group,"move_to_active_zone", RotorOps.zones[RotorOps.active_zone_index].name) --send vehicles to next zone; use move_to_active_zone so units don't get stuck if the active zone moves before they arrive end @@ -1196,6 +1205,7 @@ function RotorOps.setupCTLD() return end + --ctld.Debug = false ctld.enableCrates = RotorOps.CTLD_crates ctld.enabledFOBBuilding = false ctld.JTAC_lock = "vehicle" @@ -1217,6 +1227,7 @@ function RotorOps.setupCTLD() ["UH-1H"] = 10, ["Mi-8MT"] = 24, ["Mi-24P"] = 8, + ["UH-60L"] = 11, } ctld.loadableGroups = { @@ -1228,8 +1239,10 @@ function RotorOps.setupCTLD() {name = "JTAC Group (4)", inf = 3, jtac = 1 }, {name = "Small Platoon (16)", inf = 9, mg = 3, at = 3, aa = 1 }, {name = "Platoon (24)", inf = 10, mg = 5, at = 6, aa = 3 }, + } + + -} end @@ -1250,7 +1263,7 @@ function RotorOps.addZone(_name, _zone_defenders_flag) table.insert(RotorOps.zones, {name = _name, defenders_status_flag = _zone_defenders_flag}) trigger.action.setUserFlag(_zone_defenders_flag, 101) RotorOps.drawZones() - RotorOps.addPickupZone(_name, "blue", -1, "no", 0) + RotorOps.addPickupZone(_name, RotorOps.pickup_zone_smoke, -1, "no", 2) end function RotorOps.stagingZone(_name) @@ -1258,7 +1271,7 @@ function RotorOps.stagingZone(_name) trigger.action.outText(_name.." trigger zone missing! Check RotorOps setup!", 60) env.warning(_name.." trigger zone missing! Check RotorOps setup!") end - RotorOps.addPickupZone(_name, "blue", -1, "no", 0) + RotorOps.addPickupZone(_name, RotorOps.pickup_zone_smoke, -1, "no", 0) RotorOps.staging_zone = _name end @@ -1332,33 +1345,155 @@ function RotorOps.startConflict() end -function RotorOps.triggerSpawn(groupName, msg) +function RotorOps.triggerSpawn(groupName, msg, resume_task) local group = Group.getByName(groupName) + if not group then + env.warning("RotorOps tried to spawn "..groupName.." but it doesn't exist.") + return nil + end if group and group:isExist() == true and #group:getUnits() > 0 and group:getUnits()[1]:getLife() > 1 and group:getUnits()[1]:isActive() then env.info("RotorOps tried to respawn "..groupName.." but it's already active.") + return nil else - local new_group = mist.respawnGroup(groupName, true) + local new_group = mist.respawnGroup(groupName, resume_task) if new_group then - RotorOps.gameMsg(msg) + if msg ~= nil then + RotorOps.gameMsg(msg) + end env.info("RotorOps spawned "..groupName) return new_group end end - return nil - end function RotorOps.spawnAttackHelos() - RotorOps.triggerSpawn("Enemy Attack Helicopters", RotorOps.gameMsgs.attack_helos_prep) + RotorOps.triggerSpawn("Enemy Attack Helicopters", RotorOps.gameMsgs.attack_helos_prep, true) end function RotorOps.spawnAttackPlanes() - RotorOps.triggerSpawn("Enemy Attack Planes", RotorOps.gameMsgs.attack_planes_prep) + RotorOps.triggerSpawn("Enemy Attack Planes", RotorOps.gameMsgs.attack_planes_prep, true) end +function RotorOps.farpEstablished(index) + env.info("RotorOps FARP established at "..RotorOps.zones[index].name) + timer.scheduleFunction(function()RotorOps.gameMsg(RotorOps.gameMsgs.farp_established, index) end, {}, timer.getTime() + 15) +end + +function RotorOps.getEnemyZones() + local enemy_zones = {} + + if RotorOps.defending then + + for index, zone in pairs(RotorOps.zones) do + if index <= RotorOps.active_zone_index then + enemy_zones[#enemy_zones + 1] = zone.name + end + end + + else --not defending + + for index, zone in pairs(RotorOps.zones) do + if index >= RotorOps.active_zone_index then + enemy_zones[#enemy_zones + 1] = zone.name + end + end + + end + debugMsg("Got enemy zones:") + debugTable(enemy_zones) + return enemy_zones +end + + +function RotorOps.spawnTranspHelos(troops, max_drops) + local script_string = [[local this_grp = ... + this_grp:getController():setOption(AI.Option.Air.id.REACTION_ON_THREAT , AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) + this_grp:getController():setOption(AI.Option.Air.id.FLARE_USING , AI.Option.Air.val.FLARE_USING.WHEN_FLYING_NEAR_ENEMIES)]] + + local setOptions = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = script_string, + + }, + }, + }, + } + + local dropTroops = { + id = 'WrappedAction', + params = { + action = { + id = 'Script', + params = { + command = 'RotorOps.deployTroops('..troops..', ...)', + + }, + }, + }, + } + + local group = Group.getByName("Enemy Transport Helicopters") + local initial_point = group:getUnits()[1]:getPoint() + local gp = mist.getGroupData("Enemy Transport Helicopters") + --debugTable(gp) + + local drop_zones = RotorOps.getEnemyZones() + if RotorOps.defending then + drop_zones = {RotorOps.active_zone} + end + gp.route = {points = {}} + gp.route.points[1] = mist.heli.buildWP(initial_point, initial, 'flyover', 0, 0, 'agl') + gp.route.points[2] = mist.heli.buildWP(initial_point, initial, 'flyover', 100, 100, 'agl') + gp.route.points[2].task = setOptions + + + local failsafe = 100 + local drop_qty = 0 + while drop_qty < max_drops do + + for i = 1, 10 do --pick some random points to evaluate + local zone_name = drop_zones[math.random(#drop_zones)] + local zone_point = trigger.misc.getZone(zone_name).point + local drop_point = mist.getRandomPointInZone(zone_name, 300) + + if mist.isTerrainValid(drop_point, {'LAND', 'ROAD'}) == true then --if the point looks like a good drop point + gp.route.points[#gp.route.points + 1] = mist.heli.buildWP(zone_point, 'flyover', 100, 400, 'agl') + gp.route.points[#gp.route.points + 1] = mist.heli.buildWP(zone_point, 'flyover', 20, 200, 'agl') + gp.route.points[#gp.route.points + 1] = mist.heli.buildWP(drop_point, 'turning point', 10, 70, 'agl') + gp.route.points[#gp.route.points].task = dropTroops + drop_qty = drop_qty + 1 + break + end + + end + + failsafe = failsafe - 1 + if failsafe < 1 then + env.error("ROTOROPS: FINDING DROP POINTS TOOK TOO LONG") + break + end + + end + gp.route.points[#gp.route.points + 1] = mist.heli.buildWP(initial_point, 'flyover', 100, 400, 'agl') + gp.clone = true + local new_group_data = mist.dynAdd(gp) --returns a mist group data table + debugTable(new_group_data) +-- local new_group = Group.getByName(new_group_data.groupName) +-- local grp_controller = new_group:getController() --controller for aircraft can be group or unit level +-- grp_controller:setOption(AI.Option.Air.id.REACTION_ON_THREAT , AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE) +-- grp_controller:setOption(AI.Option.Air.id.FLARE_USING , AI.Option.Air.val.FLARE_USING.WHEN_FLYING_NEAR_ENEMIES) + + env.info("ROTOROPS: TRANSPORT HELICOPTER DEPARTING WITH "..drop_qty.." PLANNED TROOP DROPS.") + + +end diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..e51b136 --- /dev/null +++ b/license.txt @@ -0,0 +1 @@ +Use of this software or derivitives of this software is not permitted on 24/7 public multiplayer servers without permission. Scheduled events are excepted. \ No newline at end of file diff --git a/sound/embedded/forward_base_established.ogg b/sound/embedded/forward_base_established.ogg new file mode 100644 index 0000000..f49cbae Binary files /dev/null and b/sound/embedded/forward_base_established.ogg differ