// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 10.0.0 // @description Lets you manage your tactics in ManagerZone // @author Douglas Vieira // @match https://www.managerzone.com/?p=tactics // @match https://www.managerzone.com/?p=national_teams&sub=tactics&type=* // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_getResourceText // @require https://unpkg.com/jssha@3.3.0/dist/sha256.js // @require https://unpkg.com/i18next@21.6.3/i18next.min.js // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @resource mzTacticsManagerStyles https://u18mz.vercel.app/mz/userscript/tactics/ljubljana.css // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(GM_getResourceText('mzTacticsManagerStyles')); const OUTFIELD_PLAYERS_SELECTOR = ".fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper), .fieldpos.fieldpos-collision.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper)"; const GOALKEEPER_SELECTOR = ".fieldpos.fieldpos-ok.goalkeeper.ui-draggable"; const FORMATION_TEXT_SELECTOR = "#formation_text"; const TACTIC_SLOT_SELECTOR = ".ui-state-default.ui-corner-top.ui-tabs-selected.ui-state-active.invalid"; const MIN_OUTFIELD_PLAYERS = 10; const MAX_TACTIC_NAME_LENGTH = 50; const DEFAULT_TACTICS_DATA_URL = "https://u18mz.vercel.app/mz/userscript/tactics/json/defaultTactics.json"; const LANG_DATA_BASE_URL = "https://u18mz.vercel.app/mz/userscript/tactics/json/lang/"; const BASE_FLAG_URL = "https://raw.githubusercontent.com/lipis/flag-icons/d6785f2434e54e775d55a304733d17b048eddfb5/flags/4x3/"; const VERSION = "10.0.0"; const VERSION_KEY = "mz_tactics_version"; const LANGUAGES = [ { code: "en", name: "English", flag: BASE_FLAG_URL + "gb.svg" }, { code: "pt", name: "Português", flag: BASE_FLAG_URL + "br.svg" }, { code: "zh", name: "中文", flag: BASE_FLAG_URL + "cn.svg" }, { code: "sv", name: "Svenska", flag: BASE_FLAG_URL + "se.svg" }, { code: "no", name: "Norsk", flag: BASE_FLAG_URL + "no.svg" }, { code: "da", name: "Dansk", flag: BASE_FLAG_URL + "dk.svg" }, { code: "es", name: "Español", flag: BASE_FLAG_URL + "ar.svg" }, { code: "pl", name: "Polski", flag: BASE_FLAG_URL + "pl.svg" }, { code: "nl", name: "Nederlands", flag: BASE_FLAG_URL + "nl.svg" }, { code: "id", name: "Bahasa Indonesia", flag: BASE_FLAG_URL + "id.svg" }, { code: "de", name: "Deutsch", flag: BASE_FLAG_URL + "de.svg" }, { code: "it", name: "Italiano", flag: BASE_FLAG_URL + "it.svg" }, { code: "fr", name: "Français", flag: BASE_FLAG_URL + "fr.svg" }, { code: "ro", name: "Română", flag: BASE_FLAG_URL + "ro.svg" }, { code: "tr", name: "Türkçe", flag: BASE_FLAG_URL + "tr.svg" }, { code: "ko", name: "한국어", flag: BASE_FLAG_URL + "kr.svg" }, { code: "ru", name: "Русский", flag: BASE_FLAG_URL + "ru.svg" }, { code: "ar", name: "العربية", flag: BASE_FLAG_URL + "sa.svg" }, { code: "jp", name: "日本語", flag: BASE_FLAG_URL + "jp.svg" } ]; const SWAL_CONSTANTS = { ICONS: { SUCCESS: 'success', ERROR: 'error', WARNING: 'warning' } }; const SWAL_CUSTOM_CLASS = { popup: 'swal-mz-popup', title: 'swal-mz-title', htmlContainer: 'swal-mz-html-container', input: 'swal-mz-input', validationMessage: 'swal-mz-validation', actions: 'swal-mz-actions', confirmButton: 'swal-mz-confirm', cancelButton: 'swal-mz-cancel', closeButton: 'swal-mz-close' }; const USERSCRIPT_STRINGS = { addButton: "Add Tactic", addWithXmlButton: "Add Tactic via XML", deleteButton: "Delete Tactic", renameButton: "Rename Tactic", updateButton: "Update Tactic", clearButton: "Clear Tactics", resetButton: "Reset Tactics", importButton: "Import Tactics", exportButton: "Export Tactics", usefulLinksButton: "Useful Links", aboutButton: "About", tacticNamePrompt: "Tactic Name:", addAlert: "Tactic {} added successfully.", deleteAlert: "Tactic {} deleted successfully.", renameAlert: "Tactic {} renamed to {} successfully.", updateAlert: "Tactic {} updated successfully.", clearAlert: "Tactics cleared successfully.", resetAlert: "Tactics reset successfully.", importAlert: "Tactics imported successfully.", exportAlert: "Tactics exported successfully.", deleteConfirmation: "Do you really want to delete {}?", updateConfirmation: "Do you really want to update {}?", clearConfirmation: "Do you really want to clear tactics?", resetConfirmation: "Do you really want to reset tactics?", invalidTacticError: "Invalid tactic.", noTacticNameProvidedError: "No tactic name provided.", alreadyExistingTacticNameError: "Tactic name already exists.", tacticNameMaxLengthError: "Tactic name is too long.", noTacticSelectedError: "No tactic selected.", duplicateTacticError: "Duplicate tactic.", noChangesMadeError: "No changes made.", invalidImportError: "Invalid import file.", modalContentInfoText: "This is the tactic selector.", modalContentFeedbackText: "Send your feedback.", usefulContent: "Some useful resources:", tacticsDropdownMenuLabel: "Tactics:", languageDropdownMenuLabel: "Language:", errorTitle: "Error", doneTitle: "Done", confirmationTitle: "Confirmation", deleteTacticConfirmButton: "Delete", cancelConfirmButton: "Cancel", updateConfirmButton: "Update", clearTacticsConfirmButton: "Clear", resetTacticsConfirmButton: "Reset", addConfirmButton: "Add", xmlValidationError: "Invalid XML.", xmlParsingError: "Error parsing XML.", xmlPlaceholder: "Paste XML here", tacticNamePlaceholder: "Name", managerTitle: "MZ Tactics Manager", tacticActionsTitle: "Actions", otherActionsTitle: "Other", welcomeMessage: "Welcome to MZ Tactics Manager! I hope you enjoy using it. If you got any questions or suggestions, feel free to reach out via chat or guestbook.", welcomeGotIt: "Got it!" }; const ELEMENT_STRING_KEYS = { add_tactic_button: "addButton", add_tactic_with_xml_button: "addWithXmlButton", delete_tactic_button: "deleteButton", rename_tactic_button: "renameButton", update_tactic_button: "updateButton", clear_tactics_button: "clearButton", reset_tactics_button: "resetButton", import_tactics_button: "importButton", export_tactics_button: "exportButton", about_button: "aboutButton", tactics_dropdown_menu_label: "tacticsDropdownMenuLabel", language_dropdown_menu_label: "languageDropdownMenuLabel", info_modal_info_text: "modalContentInfoText", info_modal_feedback_text: "modalContentFeedbackText", useful_links_button: "usefulLinksButton", }; let dropdownMenuTactics = []; let activeLanguage; let infoModal; let usefulLinksModal; function isFootball() { const element = document.querySelector("div#tactics_box.soccer.clearfix"); return !!element; } function sha256Hash(str) { const shaObj = new jsSHA("SHA-256", "TEXT"); shaObj.update(str); return shaObj.getHash("HEX"); } function showAlert(options) { const defaultOptions = { customClass: SWAL_CUSTOM_CLASS, buttonsStyling: true, showClass: { popup: 'swal-mz-popup modalFadeIn' }, hideClass: { popup: 'modalFadeOut' }, allowOutsideClick: true, allowEscapeKey: true, width: options.html?.includes('swal-xml-input') ? '600px' : '300px', padding: '20px' }; if (options.customClass) { options.customClass = { ...SWAL_CUSTOM_CLASS, ...options.customClass }; } return Swal.fire({ ...defaultOptions, ...options }); } function showSuccessMessage(title, text) { return showAlert({ title, text, icon: SWAL_CONSTANTS.ICONS.SUCCESS }); } function showErrorMessage(title, text) { return showAlert({ title, text, icon: SWAL_CONSTANTS.ICONS.ERROR }); } async function fetchTacticsFromGMStorage() { const storedTactics = GM_getValue("ls_tactics"); if (storedTactics) { return storedTactics; } else { const jsonTactics = await fetchTacticsFromJson(); storeTacticsInGMStorage(jsonTactics); return jsonTactics; } } async function fetchTacticsFromJson() { const response = await fetch(DEFAULT_TACTICS_DATA_URL); return await response.json(); } function storeTacticsInGMStorage(data) { GM_setValue("ls_tactics", data); } async function validateDuplicateTactic(id) { const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] }; return tacticsData.tactics.some((tactic) => tactic.id === id); } async function saveTacticToStorage(tactic) { const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] }; tacticsData.tactics.push(tactic); await GM_setValue("ls_tactics", tacticsData); } async function validateDuplicateTacticWithUpdatedCoord(newId, selectedTac, tacticsData) { if (newId === selectedTac.id) { return "unchanged"; } else if (tacticsData.tactics.some((tac) => tac.id === newId)) { return "duplicate"; } else { return "unique"; } } function handleTacticsSelection(tactic) { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = dropdownMenuTactics.find((tacticData) => tacticData.name === tactic); if (selectedTactic) { if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS) { const hiddenTriggerButton = document.getElementById("hidden_trigger_button"); hiddenTriggerButton.click(); setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 1); } else { rearrangePlayers(selectedTactic.coordinates); } } } function rearrangePlayers(coordinates) { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); findBestPositions(outfieldPlayers, coordinates); for (let i = 0; i < outfieldPlayers.length; ++i) { outfieldPlayers[i].style.left = coordinates[i][0] + "px"; outfieldPlayers[i].style.top = coordinates[i][1] + "px"; removeCollision(outfieldPlayers[i]); } removeTacticSlotInvalidStatus(); updateFormationText(getFormation(coordinates)); } function findBestPositions(players, coordinates) { players.sort((a, b) => parseInt(a.style.top) - parseInt(b.style.top)); coordinates.sort((a, b) => a[1] - b[1]); } function removeCollision(player) { if (player.classList.contains("fieldpos-collision")) { player.classList.remove("fieldpos-collision"); player.classList.add("fieldpos-ok"); } } function removeTacticSlotInvalidStatus() { const slot = document.querySelector(TACTIC_SLOT_SELECTOR); if (slot) { slot.classList.remove("invalid"); } } function updateFormationText(formation) { const formationTextElement = document.querySelector(FORMATION_TEXT_SELECTOR); formationTextElement.querySelector(".defs").textContent = formation.defenders; formationTextElement.querySelector(".mids").textContent = formation.midfielders; formationTextElement.querySelector(".atts").textContent = formation.strikers; } function getFormation(coordinates) { let strikers = 0; let midfielders = 0; let defenders = 0; for (const coo of coordinates) { const y = coo[1]; if (y < 103) { strikers++; } else if (y <= 204) { midfielders++; } else { defenders++; } } return { strikers, midfielders, defenders }; } function validateTacticPlayerCount(outfieldPlayers) { const isGoalkeeper = document.querySelector(GOALKEEPER_SELECTOR); outfieldPlayers = outfieldPlayers.filter((player) => !player.classList.contains("fieldpos-collision")); if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS || !isGoalkeeper) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError); return false; } return true; } async function addNewTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); const tacticCoordinates = outfieldPlayers.map((player) => [parseInt(player.style.left), parseInt(player.style.top)]); if (!validateTacticPlayerCount(outfieldPlayers)) { return; } const tacticId = generateUniqueId(tacticCoordinates); const isDuplicate = await validateDuplicateTactic(tacticId); if (isDuplicate) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError); return; } const result = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', inputValue: '', inputValidator: (value) => { if (!value) { return USERSCRIPT_STRINGS.noTacticNameProvidedError; } if (value.length > MAX_TACTIC_NAME_LENGTH) { return USERSCRIPT_STRINGS.tacticNameMaxLengthError; } if (dropdownMenuTactics.some((t) => t.name === value)) { return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; } }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); const tacticName = result.value; if (!tacticName) { return; } const tactic = { name: tacticName, coordinates: tacticCoordinates, id: tacticId }; await saveTacticToStorage(tactic); addTacticsToDropdownMenu(tacticsDropdownMenu, [tactic]); dropdownMenuTactics.push(tactic); const placeholderOption = tacticsDropdownMenu.querySelector('option[value=""]'); if (placeholderOption) { placeholderOption.remove(); } if (tacticsDropdownMenu.disabled) { tacticsDropdownMenu.disabled = false; } tacticsDropdownMenu.value = tactic.name; const changeEvent = new Event('change', { bubbles: true }); tacticsDropdownMenu.dispatchEvent(changeEvent); handleTacticsSelection(tactic.name); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace("{}", tactic.name)); } async function addNewTacticWithXml() { const result = await showAlert({ title: USERSCRIPT_STRINGS.addWithXmlButton, html: `` + ``, focusConfirm: false, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, preConfirm: () => { const xml = document.getElementById('swal-xml-input').value; const name = document.getElementById('swal-name-input').value; if (!xml) { Swal.showValidationMessage(USERSCRIPT_STRINGS.xmlValidationError); return false; } if (!name) { Swal.showValidationMessage(USERSCRIPT_STRINGS.noTacticNameProvidedError); return false; } if (name.length > MAX_TACTIC_NAME_LENGTH) { Swal.showValidationMessage(USERSCRIPT_STRINGS.tacticNameMaxLengthError); return false; } if (dropdownMenuTactics.some((t) => t.name === name)) { Swal.showValidationMessage(USERSCRIPT_STRINGS.alreadyExistingTacticNameError); return false; } return { xml, name }; } }); if (!result.value) { return; } try { const { xml, name } = result.value; const newTactic = await convertXmlToTacticJson(xml, name); const tacticId = generateUniqueId(newTactic.coordinates); const isDuplicate = await validateDuplicateTactic(tacticId); if (isDuplicate) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError); return; } newTactic.id = tacticId; await saveTacticToStorage(newTactic); const tacticsDropdownMenu = document.getElementById('tactics_dropdown_menu'); addTacticsToDropdownMenu(tacticsDropdownMenu, [newTactic]); dropdownMenuTactics.push(newTactic); tacticsDropdownMenu.value = newTactic.name; handleTacticsSelection(newTactic.name); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', newTactic.name)); } catch (e) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError); } } async function deleteTactic() { const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const result = await showAlert({ text: USERSCRIPT_STRINGS.deleteConfirmation.replace("{}", selectedTactic.name), icon: SWAL_CONSTANTS.ICONS.WARNING, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) { return; } const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] }; tacticsData.tactics = tacticsData.tactics.filter((tactic) => tactic.id !== selectedTactic.id); await GM_setValue("ls_tactics", tacticsData); dropdownMenuTactics = dropdownMenuTactics.filter((tactic) => tactic.id !== selectedTactic.id); const selectedOption = Array.from(tacticsDropdownMenu.options).find((option) => option.value === selectedTactic.name); tacticsDropdownMenu.remove(selectedOption.index); if (tacticsDropdownMenu.options[0]?.disabled) { tacticsDropdownMenu.selectedIndex = 0; } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace("{}", selectedTactic.name)); } async function renameTactic() { const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const oldName = selectedTactic.name; const result = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', inputValue: oldName, inputValidator: (value) => { if (!value) { return USERSCRIPT_STRINGS.noTacticNameProvidedError; } if (value.length > MAX_TACTIC_NAME_LENGTH) { return USERSCRIPT_STRINGS.tacticNameMaxLengthError; } if (value !== oldName && dropdownMenuTactics.some((t) => t.name === value)) { return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; } }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); const newName = result.value; if (!newName) { return; } const selectedOption = Array.from(tacticsDropdownMenu.options).find((option) => option.value === selectedTactic.name); const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] }; tacticsData.tactics = tacticsData.tactics.map((tactic) => { if (tactic.id === selectedTactic.id) { tactic.name = newName; } return tactic; }); await GM_setValue("ls_tactics", tacticsData); dropdownMenuTactics = dropdownMenuTactics.map((tactic) => { if (tactic.id === selectedTactic.id) { tactic.name = newName; } return tactic; }); selectedOption.value = newName; selectedOption.textContent = newName; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace("{}", oldName).replace("{}", newName)); } async function updateTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsDropdownMenu.value); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const updatedCoordinates = outfieldPlayers.map((player) => [parseInt(player.style.left), parseInt(player.style.top)]); const newId = generateUniqueId(updatedCoordinates); const tacticsData = (await GM_getValue("ls_tactics")) || { tactics: [] }; const validationOutcome = await validateDuplicateTacticWithUpdatedCoord(newId, selectedTactic, tacticsData); if (validationOutcome === "unchanged") { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noChangesMadeError); return; } else if (validationOutcome === "duplicate") { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError); return; } const result = await showAlert({ text: USERSCRIPT_STRINGS.updateConfirmation.replace("{}", selectedTactic.name), icon: SWAL_CONSTANTS.ICONS.WARNING, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) { return; } for (const tactic of tacticsData.tactics) { if (tactic.id === selectedTactic.id) { tactic.coordinates = updatedCoordinates; tactic.id = newId; } } for (const tactic of dropdownMenuTactics) { if (tactic.id === selectedTactic.id) { tactic.coordinates = updatedCoordinates; tactic.id = newId; } } await GM_setValue("ls_tactics", tacticsData); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.updateAlert.replace("{}", selectedTactic.name)); } async function clearTactics() { const result = await showAlert({ text: USERSCRIPT_STRINGS.clearConfirmation, icon: SWAL_CONSTANTS.ICONS.WARNING, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) { return; } await GM_setValue("ls_tactics", { tactics: [] }); dropdownMenuTactics = []; const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); tacticsDropdownMenu.innerHTML = ""; tacticsDropdownMenu.disabled = true; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert); } async function resetTactics() { const result = await showAlert({ text: USERSCRIPT_STRINGS.resetConfirmation, icon: SWAL_CONSTANTS.ICONS.WARNING, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) { return; } const response = await fetch(DEFAULT_TACTICS_DATA_URL); const data = await response.json(); const defaultTactics = data.tactics; await GM_setValue("ls_tactics", { tactics: defaultTactics }); dropdownMenuTactics = defaultTactics; const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); tacticsDropdownMenu.innerHTML = ""; tacticsDropdownMenu.appendChild(createPlaceholderOption()); addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics); tacticsDropdownMenu.disabled = false; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert); } async function importTactics() { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = async function (event) { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = async function (event) { let importedData; try { importedData = JSON.parse(event.target.result); } catch (e) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError); return; } if (!importedData || !Array.isArray(importedData.tactics)) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError); return; } const importedTactics = importedData.tactics; let existingTactics = await GM_getValue("ls_tactics", { tactics: [] }); existingTactics = existingTactics.tactics; const mergedTactics = [...existingTactics]; for (const importedTactic of importedTactics) { if (!existingTactics.some((tactic) => tactic.id === importedTactic.id)) { mergedTactics.push(importedTactic); } } await GM_setValue("ls_tactics", { tactics: mergedTactics }); mergedTactics.sort((a, b) => a.name.localeCompare(b.name)); dropdownMenuTactics = mergedTactics; const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); tacticsDropdownMenu.innerHTML = ""; tacticsDropdownMenu.append(createPlaceholderOption()); addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics); tacticsDropdownMenu.disabled = false; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.importAlert); }; reader.readAsText(file); }; input.click(); } function exportTactics() { const tactics = GM_getValue("ls_tactics", { tactics: [] }); const tacticsJson = JSON.stringify(tactics); const blob = new Blob([tacticsJson], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "tactics.json"; const onFocus = () => { window.removeEventListener('focus', onFocus); URL.revokeObjectURL(url); }; window.addEventListener('focus', onFocus, { once: true }); link.click(); } async function convertXmlToTacticJson(xmlString, tacticName) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const parserError = xmlDoc.getElementsByTagName('parsererror'); if (parserError.length > 0) { throw new Error('Invalid XML'); } const posElements = Array.from(xmlDoc.getElementsByTagName('Pos')); const normalPosElements = posElements.filter(el => el.getAttribute('pos') === 'normal'); const coordinates = normalPosElements.map(el => { const x = parseInt(el.getAttribute('x')); const y = parseInt(el.getAttribute('y')); const htmlLeft = x - 7; const htmlTop = y - 9; return [htmlLeft, htmlTop]; }); return { name: tacticName, coordinates: coordinates }; } function createAddNewTacticButton() { const button = document.createElement("button"); setUpButton(button, "add_tactic_button", USERSCRIPT_STRINGS.addButton); button.addEventListener("click", function () { addNewTactic().catch((_) => { }); }); return button; } function createAddNewTacticWithXmlButton() { const button = document.createElement("button"); setUpButton(button, "add_tactic_with_xml_button", USERSCRIPT_STRINGS.addWithXmlButton); button.addEventListener("click", function () { addNewTacticWithXml().catch((_) => { }); }); return button; } function createDeleteTacticButton() { const button = document.createElement("button"); setUpButton(button, "delete_tactic_button", USERSCRIPT_STRINGS.deleteButton); button.addEventListener("click", function () { deleteTactic().catch((_) => { }); }); return button; } function createRenameTacticButton() { const button = document.createElement("button"); setUpButton(button, "rename_tactic_button", USERSCRIPT_STRINGS.renameButton); button.addEventListener("click", function () { renameTactic().catch((_) => { }); }); return button; } function createUpdateTacticButton() { const button = document.createElement("button"); setUpButton(button, "update_tactic_button", USERSCRIPT_STRINGS.updateButton); button.addEventListener("click", function () { updateTactic().catch((_) => { }); }); return button; } function createClearTacticsButton() { const button = document.createElement("button"); setUpButton(button, "clear_tactics_button", USERSCRIPT_STRINGS.clearButton); button.addEventListener("click", function () { clearTactics().catch((_) => { }); }); return button; } function createResetTacticsButton() { const button = document.createElement("button"); setUpButton(button, "reset_tactics_button", USERSCRIPT_STRINGS.resetButton); button.addEventListener("click", function () { resetTactics().catch((_) => { }); }); return button; } function createImportTacticsButton() { const button = document.createElement("button"); setUpButton(button, "import_tactics_button", USERSCRIPT_STRINGS.importButton); button.addEventListener("click", function () { importTactics().catch((_) => { }); }); return button; } function createExportTacticsButton() { const button = document.createElement("button"); setUpButton(button, "export_tactics_button", USERSCRIPT_STRINGS.exportButton); button.addEventListener("click", function () { exportTactics(); }); return button; } async function checkVersion() { const storedVersion = GM_getValue(VERSION_KEY, null); if (!storedVersion || storedVersion !== VERSION) { await showWelcomeMessage(); GM_setValue(VERSION_KEY, VERSION); } } async function showWelcomeMessage() { await showAlert({ html: `
${USERSCRIPT_STRINGS.welcomeMessage}