From a99a30a71d4752f7d4d9e032b43578a911114fde Mon Sep 17 00:00:00 2001 From: frarol96 Date: Sat, 20 Jul 2024 01:48:36 +0000 Subject: [PATCH] Added tooltips and experimental save file generator - Added basic tooltips for most fields - Added advanced tooltips for mod scaling parameters, which calculates example values for up to 4 players - Added basic save file generator - This simply clones the vanilla save file into an identical one with the user-specified file ending. [EXPERIMENTAL] --- ERSCMU.py | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/ERSCMU.py b/ERSCMU.py index 50544db..fba0d60 100644 --- a/ERSCMU.py +++ b/ERSCMU.py @@ -1,5 +1,5 @@ # Define version number -PROGRAM_VERSION = "1.7.0.3" +PROGRAM_VERSION = "1.7.0.4" # Define a persistent path for the configuration files in the AppData folder PERSISTENT_DIR = os.path.join(os.getenv('APPDATA'), 'ERSC Mod Updater') @@ -435,6 +435,60 @@ def open_settings_window(): except Exception as e: print(f"Error in save_settings: {e}") + def clone_save_file(): + config = load_config() + save_ext = config["settings"]["SAVE"].get("save_file_extension", "co2") + if save_ext == "sl2": + QMessageBox.critical(settings_window, "Error", "Save file extension cannot be set to .sl2") + return + + appdata_dir = os.path.join(os.getenv('APPDATA'), 'EldenRing') + if not os.path.exists(appdata_dir): + QMessageBox.critical(settings_window, "Error", "Elden Ring AppData folder not found.") + return + + subfolders = [f.path for f in os.scandir(appdata_dir) if f.is_dir() and f.name.isdigit()] + if not subfolders: + QMessageBox.critical(settings_window, "Error", "No subfolders found in Elden Ring AppData folder.") + return + + save_folder = subfolders[0] + sl2_file = None + co2_file = None + + for file in os.listdir(save_folder): + if file.endswith(".sl2"): + sl2_file = os.path.join(save_folder, file) + co2_file = os.path.join(save_folder, file.replace(".sl2", f".{save_ext}")) + break + + if sl2_file: + if os.path.exists(co2_file): + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Warning) + msg_box.setWindowTitle("Warning") + msg_box.setText(f"The coop mod save file already exists.\n({co2_file})\nDo you want to overwrite it?\n\n!!! THIS MAY LEAD TO PROGRESSION LOSS !!!") + + yes_button = msg_box.addButton("Yes", QMessageBox.YesRole) + no_button = msg_box.addButton("No", QMessageBox.NoRole) + + yes_button.setStyleSheet("background-color: red; margin-left: 0px;") + no_button.setStyleSheet("background-color: green; margin-right: 0px;") + + msg_box.exec_() + + if msg_box.clickedButton() == no_button: + return + + try: + shutil.copyfile(sl2_file, co2_file) + QMessageBox.information(settings_window, "Success", f"Save file cloned successfully to '{co2_file}'") + except Exception as e: + QMessageBox.critical(settings_window, "Error", f"Failed to clone save file: {e}") + else: + QMessageBox.critical(settings_window, "Error", "No .sl2 save file found in the folder.") + + settings_grid = QtWidgets.QGridLayout() row = 0 @@ -458,8 +512,15 @@ def open_settings_window(): reset_button = QPushButton("Reset") reset_button.clicked.connect(lambda cb=checkbox, dv=default_value: cb.setChecked(dv == '1')) settings_grid.addWidget(reset_button, row, 1, 1, 1) + if key == 'allow_invaders': + checkbox.setToolTip(str("Allow hostile invaders to invade your party.")) + elif key == 'death_debuffs': + checkbox.setToolTip(str("Grant Death Rot debuffs upon death.\nCan be cleared by resting at a grace.")) + elif key == 'allow_summons': + checkbox.setToolTip(str("Allow players to use summons.")) elif key == 'overhead_player_display': dropdown = QComboBox() + dropdown.setToolTip(str("How other players should be displayed")) options = vocabulary[section][key] dropdown.addItems(options.values()) dropdown.setCurrentIndex(list(options.keys()).index(value)) @@ -471,18 +532,26 @@ def open_settings_window(): settings_grid.addWidget(reset_button, row, 2, 1, 1) elif section == 'SAVE': entry = QLineEdit(value) + entry.setToolTip(str("Select a save file extension you'd like the mod to use.\nDo not include a period.\nCannot be set to \"sl2\"")) settings_grid.addWidget(QLabel(display_name), row, 0, 1, 1) settings_grid.addWidget(entry, row, 1, 1, 1) settings[section][key] = entry - disclaimer = QLabel("(Do not use sl2)") + disclaimer = QLabel("(Do not use \"sl2\")") disclaimer.setFont(QFont("Arial", 8, QFont.StyleItalic)) settings_grid.addWidget(disclaimer, row+1, 1, 1, 1, alignment=Qt.AlignHCenter) reset_button = QPushButton("Reset") reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv)) settings_grid.addWidget(reset_button, row, 2, 1, 1) row += 1 + # Add the clone save button below the save file extension entry + clone_button = QPushButton("Generate Mod Save File") + clone_button.setToolTip(str("Clone your vanilla save file into a coop save file")) + clone_button.clicked.connect(clone_save_file) + settings_grid.addWidget(clone_button, row+1, 0, 1, 3) + row += 1 elif section == 'LANGUAGE': dropdown = QComboBox() + dropdown.setToolTip(str("Select a Language Override for the mod")) options = get_locale_options(config["mod_path"]) dropdown.addItems(options) dropdown.setCurrentText(value.capitalize() if value else "Default") @@ -500,11 +569,144 @@ def open_settings_window(): reset_button = QPushButton("Reset") reset_button.clicked.connect(lambda e=entry, dv=default_value: e.setText(dv)) settings_grid.addWidget(reset_button, row, 2, 1, 1) + if key == 'enemy_health_scaling': + tt_example_1 = 346 # Vulgar Militiamen (Liurnia) Example + # Calculate HP for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy non-boss HP per player.\n\n" + f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n" + f"Enemy HP with 1 player: {tt_example_1_formatted}\n" + f"Enemy HP with 2 players: {tt_example_2_formatted}\n" + f"Enemy HP with 3 players: {tt_example_3_formatted}\n" + f"Enemy HP with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) + elif key == 'enemy_damage_scaling': + tt_example_1 = 130 # Vulgar Militiamen (Liurnia) Example + # Calculate damage for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy non-boss damage potential per player.\n\n" + f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n" + f"Enemy Damage with 1 player: {tt_example_1_formatted}\n" + f"Enemy Damage with 2 players: {tt_example_2_formatted}\n" + f"Enemy Damage with 3 players: {tt_example_3_formatted}\n" + f"Enemy Damage with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) + elif key == 'enemy_posture_scaling': + tt_example_1 = 30 # Vulgar Militiamen (Liurnia) Example + # Calculate Poise for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy non-boss Posture/Poise (stun resistance) per player.\n\n" + f"Vulgar Militiamen (Liurnia) example with a {int(value)}% scaling:\n" + f"Enemy Poise with 1 player: {tt_example_1_formatted}\n" + f"Enemy Poise with 2 players: {tt_example_2_formatted}\n" + f"Enemy Poise with 3 players: {tt_example_3_formatted}\n" + f"Enemy Poise with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) + elif key == 'boss_health_scaling': + tt_example_1 = 6080 # Godrick The Grafted HP Example + # Calculate HP for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy boss HP per player.\n\n" + f"Godrick The Grafted example with a {int(value)}% scaling:\n" + f"Boss HP with 1 player: {tt_example_1_formatted}\n" + f"Boss HP with 2 players: {tt_example_2_formatted}\n" + f"Boss HP with 3 players: {tt_example_3_formatted}\n" + f"Boss HP with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) + elif key == 'boss_damage_scaling': + tt_example_1 = 200 # Godrick The Grafted Damage Example + # Calculate Damage for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy boss damage potential per player.\n\n" + f"Godrick The Grafted example with a {int(value)}% scaling:\n" + f"Boss Example Damage with 1 player: {tt_example_1_formatted}\n" + f"Boss Example Damage with 2 players: {tt_example_2_formatted}\n" + f"Boss Example Damage with 3 players: {tt_example_3_formatted}\n" + f"Boss Example Damage with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) + elif key == 'boss_posture_scaling': + tt_example_1 = 105 # Godrick The Grafted Poise Example + # Calculate Poise for 2, 3, and 4 players + tt_example_2 = tt_example_1 * (1 + int(value) / 100) + tt_example_3 = tt_example_1 * (1 + 2 * int(value) / 100) + tt_example_4 = tt_example_1 * (1 + 3 * int(value) / 100) + + # Format numbers with space as thousand separator and no decimals + tt_example_1_formatted = f"{int(tt_example_1):,}".replace(",", " ") + tt_example_2_formatted = f"{int(tt_example_2):,}".replace(",", " ") + tt_example_3_formatted = f"{int(tt_example_3):,}".replace(",", " ") + tt_example_4_formatted = f"{int(tt_example_4):,}".replace(",", " ") + + tooltip_text = ( + f"Percentage increase of enemy boss Posture/Poise (stun resistance) per player.\n\n" + f"Godrick The Grafted example with a {int(value)}% scaling:\n" + f"Boss Poise with 1 player: {tt_example_1_formatted}\n" + f"Boss Poise with 2 players: {tt_example_2_formatted}\n" + f"Boss Poise with 3 players: {tt_example_3_formatted}\n" + f"Boss Poise with 4 players: {tt_example_4_formatted}" + ) + entry.setToolTip(str(tooltip_text)) row += 1 layout.addLayout(settings_grid) save_button = QPushButton("Save Settings") + save_button.setToolTip(str("Save your settings")) save_button.clicked.connect(save_settings) layout.addWidget(save_button) settings_window.setLayout(layout) @@ -615,14 +817,17 @@ def create_gui(): menu_bar.addAction(settings_action) check_updates_action = QtWidgets.QAction("Check for Updates", main_window) + check_updates_action.setToolTip(str("Manually check for new mod updates")) check_updates_action.triggered.connect(check_for_updates) menu_bar.addAction(check_updates_action) info_action = QtWidgets.QAction("Info", main_window) + info_action.setToolTip(str("Check some basic application info")) info_action.triggered.connect(show_info) menu_bar.addAction(info_action) about_action = QtWidgets.QAction("About", main_window) + about_action.setToolTip(str("Disclaimer and license")) about_action.triggered.connect(show_about) menu_bar.addAction(about_action) @@ -631,13 +836,16 @@ def create_gui(): version_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) version_label.setStyleSheet("padding-right: 10px;") # Optional: add some padding to the right menu_bar.setCornerWidget(version_label, Qt.TopRightCorner) + version_label.setToolTip(str("ERSCMU version.\nThis is the version of this program you're currently running.")) mod_path_label = QLabel("Mod Folder:") mod_path_entry = QLineEdit() mod_path_entry.setFixedWidth(400) browse_button = QPushButton("Browse") + browse_button.setToolTip(str("Manually select the Seamless Coop mod folder")) browse_button.clicked.connect(browse_folder) auto_button = QPushButton("Auto") + auto_button.setToolTip(str("Automatically discover the Seamless Coop mod folder")) auto_button.clicked.connect(auto_discover_mod_path) config = load_config() @@ -650,10 +858,12 @@ def create_gui(): mod_path_layout.addWidget(auto_button) check_updates_button = QPushButton("Check for Updates") + check_updates_button.setToolTip(str("Manually check for new mod updates")) check_updates_button.clicked.connect(check_for_updates) password_label = QLabel("Session Password:") password_entry = QLineEdit() + password_entry.setToolTip(str("Define your session password.\nNote that all players must have the same password!")) password_entry.setEchoMode(QLineEdit.Password) toggle_button = QPushButton("Show") toggle_button.clicked.connect(toggle_password) @@ -667,9 +877,11 @@ def create_gui(): password_layout.addWidget(save_password_button) launch_button = QPushButton("Launch Seamless Coop") + launch_button.setToolTip(str("Start Elden Ring Seamless Coop")) launch_button.clicked.connect(launch_mod) info_text = QLabel() + info_text.setToolTip(str("Seamless Coop version and install date")) update_info_text() main_layout.addLayout(mod_path_layout)