// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 10.0.4 // @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://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @require https://unpkg.com/jssha@3.3.0/dist/sha256.js // @require https://unpkg.com/i18next@21.6.3/i18next.min.js // @require https://gcore.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.js // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(`@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap"); #mz_tactics_panel {font-family: "Space Grotesk", -apple-system, sans-serif; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 20px; margin: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); border: 1px solid #1e293b; transition: opacity 0.3s ease, max-height 0.3s ease; max-height: 1000px; opacity: 1; color: #f8fafc; } .mz-group {background: linear-gradient(135deg, #334155 0%, #1e293b 100%); border-radius: 8px; padding: 16px; margin: 8px; border: 1px solid #334155; position: relative; } .mz-group-main-title {color: #f8fafc; font-size: 16px; font-weight: 500; margin: -4px 0 12px 0; padding-bottom: 8px; border-bottom: 1px solid rgba(248, 250, 252, 0.1); } .mz-main-title {color: #f8fafc; font-family: "Space Grotesk", sans-serif; font-size: 18px; font-weight: 500; margin: 0; padding: 0; text-align: center; letter-spacing: 0.2px; } .mz-author-text {color: #ff9933; font-family: "Dancing Script", cursive; font-size: 0.9em; font-weight: 500; margin-left: 4px; } .mz-divider {width: 40px; height: 2px; background: #64748b; margin: 8px auto 0; opacity: 0.3; } #mz_tactics_panel .mzbtn {display: inline-flex; align-items: center; padding: 8px 14px; margin: 4px; font-family: "Space Grotesk", sans-serif; font-size: 13px; font-weight: 500; color: #f8fafc; background: #334155; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } #mz_tactics_panel .mzbtn:hover {background: #475569; transform: translateY(-1px); } #mz_tactics_panel .mzbtn:focus {outline: 2px solid #94a3b8; outline-offset: 1px; } #mz_tactics_panel select {font-family: "Space Grotesk", sans-serif; font-size: 13px; color: #f8fafc; padding: 8px 14px; border: 1px solid #334155; border-radius: 6px; background-color: #1e293b; cursor: pointer; margin: 4px; transition: all 0.2s ease; } #mz_tactics_panel select:focus {outline: none; border-color: #64748b; box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.1); } #language_flag {height: 15px; width: 25px; margin: 6px 0 6px 6px; border: 1px solid #e2e8f0; border-radius: 22px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } #info_modal, #useful_links_modal {background: #1e293b; padding: 20px; border-radius: 12px; color: #f8fafc; width: 90%; max-width: 500px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); } #info_modal a, #useful_links_modal a {color: #60a5fa; } #info_modal ul, #useful_links_modal ul {list-style: none; padding: 0; } #info_modal ul li, #useful_links_modal ul li {margin: 10px 0; } .swal2-popup.swal-mz-popup {font-family: "Space Grotesk", sans-serif; border-radius: 8px; background: #1e293b; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-title {font-size: 18px; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-html-container {font-size: 14px; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-input {font-size: 14px; border: 1px solid #334155; border-radius: 6px; background: #0f172a; color: #f8fafc; } .swal2-popup.swal-mz-popup .swal2-textarea {background: #0f172a; color: #f8fafc; border: 1px solid #334155; } .swal2-popup.swal-mz-popup .swal2-confirm {background: #475569 !important; } .swal2-popup.swal-mz-popup .swal2-cancel {background: #64748b !important; } .swal2-container.swal2-backdrop-show {background: rgba(0, 0, 0, 0.6); } #hidden_trigger_button {position: absolute; visibility: hidden; pointer-events: none; }`); 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://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/defaultTactics.json"; const LANG_DATA_BASE_URL = "https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/lang/"; const BASE_FLAG_URL = "https://flagcdn.com/w320/"; const LANGUAGES = [ { code: "en", name: "English", flag: BASE_FLAG_URL + "gb.png" }, { code: "pt", name: "Português", flag: BASE_FLAG_URL + "br.png" }, { code: "zh", name: "中文", flag: BASE_FLAG_URL + "cn.png" }, { code: "sv", name: "Svenska", flag: BASE_FLAG_URL + "se.png" }, { code: "no", name: "Norsk", flag: BASE_FLAG_URL + "no.png" }, { code: "da", name: "Dansk", flag: BASE_FLAG_URL + "dk.png" }, { code: "es", name: "Español", flag: BASE_FLAG_URL + "ar.png" }, { code: "pl", name: "Polski", flag: BASE_FLAG_URL + "pl.png" }, { code: "nl", name: "Nederlands", flag: BASE_FLAG_URL + "nl.png" }, { code: "id", name: "Bahasa Indonesia", flag: BASE_FLAG_URL + "id.png" }, { code: "de", name: "Deutsch", flag: BASE_FLAG_URL + "de.png" }, { code: "it", name: "Italiano", flag: BASE_FLAG_URL + "it.png" }, { code: "fr", name: "Français", flag: BASE_FLAG_URL + "fr.png" }, { code: "ro", name: "Română", flag: BASE_FLAG_URL + "ro.png" }, { code: "tr", name: "Türkçe", flag: BASE_FLAG_URL + "tr.png" }, { code: "ko", name: "한국어", flag: BASE_FLAG_URL + "kr.png" }, { code: "ru", name: "Русский", flag: BASE_FLAG_URL + "ru.png" }, { code: "ar", name: "العربية", flag: BASE_FLAG_URL + "sa.png" }, { code: "jp", name: "日本語", flag: BASE_FLAG_URL + "jp.png" } ]; const VERSION = "10.0.4"; const VERSION_KEY = "mz_tactics_version"; 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! Enjoy using it! If you got any questions or suggestions, feel free to message douglaskampl 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}