// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 11.1.0 // @description A tool to 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_deleteValue // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/jsSHA/3.3.1/sha256.js // @require https://cdnjs.cloudflare.com/ajax/libs/i18next/23.7.16/i18next.min.js // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // ============================== // STYLES // ============================== 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: all 0.3s ease-in-out; max-height: 1000px; opacity: 1; color: #f8fafc; overflow: hidden; } #mz_tactics_panel.collapsed {max-height: 0; padding: 0; margin: 0; opacity: 0; border: none; } .mz-group-main-title {display: flex; justify-content: space-between; align-items: center; 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); } #toggle_panel_btn {background: none; border: none; color: #94a3b8; cursor: pointer; padding: 4px; margin-left: auto; font-size: 18px; transition: all 0.3s ease; display: inline-flex; align-items: center; justify-content: center; opacity: 0.7; } #toggle_panel_btn:hover {opacity: 1; transform: translateY(-1px); } #toggle_panel_btn.collapsed {transform: rotate(180deg); } #toggle_panel_btn.collapsed:hover {transform: rotate(180deg) translateY(-1px); } #collapsed_icon {position: fixed; top: 20px; right: 20px; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transition: all 0.3s ease; transform: scale(0); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); z-index: 1000; color: #94a3b8; font-size: 18px; } #collapsed_icon.visible {opacity: 1; transform: scale(1); } #collapsed_icon:hover {transform: scale(1.1); opacity: 1; } .mz-group {background: linear-gradient(135deg, #334155 0%, #1e293b 100%); border-radius: 8px; padding: 16px; margin: 8px; border: 1px solid #334155; position: relative; } .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-version-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: 2px; 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; } #mz-modal-overlay {position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 10000; opacity: 0; transition: opacity 0.3s ease; } #mz-modal-container {background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); border: 1px solid #334155; max-width: 500px; width: 90%; transform: scale(0.9); transition: transform 0.3s ease; color: #f8fafc; font-family: "Space Grotesk", -apple-system, sans-serif; } #mz-modal-overlay.active {opacity: 1; } #mz-modal-overlay.active #mz-modal-container {transform: scale(1); } #mz-modal-header {display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(248, 250, 252, 0.1); padding-bottom: 10px; } #mz-modal-title {font-size: 18px; font-weight: 500; margin: 0; } #mz-modal-close {background: none; border: none; color: #94a3b8; font-size: 20px; cursor: pointer; transition: all 0.2s ease; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; } #mz-modal-close:hover {color: #f8fafc; background: rgba(148, 163, 184, 0.1); } #mz-modal-content {margin-bottom: 20px; white-space: pre-line; } #mz-modal-input {width: calc(100% - 28px); background: #1e293b; border: 1px solid #334155; color: #f8fafc; padding: 10px 14px; border-radius: 6px; font-family: "Space Grotesk", sans-serif; font-size: 14px; margin-bottom: 16px; transition: all 0.2s ease; box-sizing: border-box; } #mz-modal-input:focus {outline: none; border-color: #64748b; box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.1); } #mz-modal-buttons {display: flex; justify-content: flex-start; gap: 10px; } .mz-modal-btn {display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; font-family: "Space Grotesk", sans-serif; font-size: 14px; font-weight: 500; color: #f8fafc; background: #334155; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; min-width: 80px; } .mz-modal-btn:hover {background: #475569; transform: translateY(-1px); } .mz-modal-btn:focus {outline: 2px solid #94a3b8; outline-offset: 1px; } .mz-modal-btn.primary {background: #2563eb; } .mz-modal-btn.primary:hover {background: #3b82f6; } .mz-modal-icon {display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; margin-right: 12px; } .mz-modal-icon.success {background: rgba(34, 197, 94, 0.2); color: #22c55e; } .mz-modal-icon.error {background: rgba(239, 68, 68, 0.2); color: #ef4444; } .mz-modal-icon.info {background: rgba(59, 130, 246, 0.2); color: #3b82f6; } .mz-modal-title-with-icon {display: flex; align-items: center; } .mz-modal-btn.cancel {background: #475569; } .mz-modal-btn.cancel:hover {background: #64748b; } .mz-modal-icon {display: flex; align-items: center; justify-content: center; }`); // ============================== // CONSTANTS AND VARIABLES // ============================== 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 中国地区 = ['CN', 'HK', 'MO', 'TW']; const CDN_URLS = { default: { tactics: "https://u18mz.vercel.app/mz/userscript/tactics/json/defaultTactics.json", lang: "https://u18mz.vercel.app/mz/userscript/tactics/json/lang/" }, china: { tactics: "https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/defaultTactics.json", lang: "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 = "11.1.0"; const VERSION_KEY = "mz_tactics_version"; 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 from Clipboard", exportButton: "Export to Clipboard", 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 from clipboard.", exportAlert: "Tactics copied to clipboard 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 data.", modalContentInfoText: "This is the tactic selector.", modalContentFeedbackText: "Send your feedback.", usefulContent: "Some useful resources:", tacticsDropdownMenuLabel: "Tactics:", languageDropdownMenuLabel: "Language:", errorTitle: "Error", doneTitle: "Success", 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 v11.1.0!\n\nWhat's new:\n• Import/Export now uses clipboard to copy/paste tactics data\n• Customized alerts\n\nIf you have 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" }; const DEFAULT_MODAL_STRINGS = { ok: "OK", cancel: "Cancel", error: "Error", close: "×" }; const region = isLikelyFromChina() ? 'china' : 'default'; const defaultTacticsDataUrl = CDN_URLS[region].tactics; const langDataBaseUrl = CDN_URLS[region].lang; let dropdownMenuTactics = []; let activeLanguage; let infoModal; let usefulLinksModal; // ============================== // CUSTOM ALERT SYSTEM // ============================== function createModalIcon(type) { if (!type) return null; const icon = document.createElement('div'); icon.classList.add('mz-modal-icon'); if (type === 'success') { icon.classList.add('success'); icon.innerHTML = '✓'; } else if (type === 'error') { icon.classList.add('error'); icon.innerHTML = '✗'; } return icon; } function validateModalInput(input, validator, errorContainerId) { if (!validator) return null; const validationError = validator(input.value); if (!validationError) return null; const errorText = document.createElement('div'); errorText.style.color = '#ef4444'; errorText.style.marginTop = '-10px'; errorText.style.marginBottom = '10px'; errorText.style.fontSize = '13px'; errorText.textContent = validationError; const existingError = document.getElementById(errorContainerId); if (existingError) { existingError.remove(); } errorText.id = errorContainerId; input.parentNode.insertBefore(errorText, input.nextSibling); return validationError; } function closeModal(overlay, callback) { overlay.classList.remove('active'); setTimeout(() => { document.body.removeChild(overlay); if (callback) callback(); }, 300); } function handleAlertConfirm(options, input, overlay, resolve) { if (options.input === 'text' && options.inputValidator) { const hasError = validateModalInput(input, options.inputValidator, 'mz-modal-error'); if (hasError) return; } closeModal(overlay, () => { if (options.input === 'text') { resolve({ value: input.value, isConfirmed: true }); } else { resolve({ isConfirmed: true }); } }); } function handleAlertCancel(overlay, resolve) { closeModal(overlay, () => { resolve({ isConfirmed: false, value: null }); }); } function setUpKeyboardHandler(handleConfirm, handleCancel, input) { return function (e) { if (e.key === 'Escape') { handleCancel(); } else if (e.key === 'Enter' && !(input && document.activeElement !== input)) { handleConfirm(); } }; } function showAlert(options) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'mz-modal-overlay'; const container = document.createElement('div'); container.id = 'mz-modal-container'; const header = document.createElement('div'); header.id = 'mz-modal-header'; const titleContainer = document.createElement('div'); titleContainer.classList.add('mz-modal-title-with-icon'); const icon = createModalIcon(options.type); if (icon) titleContainer.appendChild(icon); const title = document.createElement('h2'); title.id = 'mz-modal-title'; title.textContent = options.title || ''; titleContainer.appendChild(title); header.appendChild(titleContainer); const closeBtn = document.createElement('button'); closeBtn.id = 'mz-modal-close'; closeBtn.innerHTML = DEFAULT_MODAL_STRINGS.close; header.appendChild(closeBtn); const content = document.createElement('div'); content.id = 'mz-modal-content'; content.textContent = options.text || ''; let input; if (options.input === 'text') { input = document.createElement('input'); input.id = 'mz-modal-input'; input.type = 'text'; input.value = options.inputValue || ''; input.placeholder = options.placeholder || ''; } const buttons = document.createElement('div'); buttons.id = 'mz-modal-buttons'; const confirmHandler = () => handleAlertConfirm(options, input, overlay, resolve); const cancelHandler = () => handleAlertCancel(overlay, resolve); const confirmBtn = document.createElement('button'); confirmBtn.classList.add('mz-modal-btn', 'primary'); confirmBtn.textContent = options.confirmButtonText || DEFAULT_MODAL_STRINGS.ok; confirmBtn.addEventListener('click', confirmHandler); buttons.appendChild(confirmBtn); if (options.showCancelButton) { const cancelBtn = document.createElement('button'); cancelBtn.classList.add('mz-modal-btn', 'cancel'); cancelBtn.textContent = options.cancelButtonText || DEFAULT_MODAL_STRINGS.cancel; cancelBtn.addEventListener('click', cancelHandler); buttons.appendChild(cancelBtn); } closeBtn.addEventListener('click', cancelHandler); const keydownHandler = setUpKeyboardHandler(confirmHandler, cancelHandler, input); document.addEventListener('keydown', keydownHandler); container.appendChild(header); container.appendChild(content); if (input) container.appendChild(input); container.appendChild(buttons); overlay.appendChild(container); document.body.appendChild(overlay); setTimeout(() => { overlay.classList.add('active'); if (input) input.focus(); }, 10); overlay.addEventListener('transitionend', () => { if (!overlay.classList.contains('active')) { document.removeEventListener('keydown', keydownHandler); } }); }); } function showSuccessMessage(title, text) { return showAlert({ title: title || USERSCRIPT_STRINGS.doneTitle, text: text, type: 'success' }); } function showErrorMessage(title, text) { return showAlert({ title: title || USERSCRIPT_STRINGS.errorTitle, text: text, type: 'error' }); } function showWelcomeMessage() { return showAlert({ title: "Welcome!", text: USERSCRIPT_STRINGS.welcomeMessage, }); } // ============================== // UTILITY FUNCTIONS // ============================== 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"); } 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() { try { const response = await fetch(defaultTacticsDataUrl); if (!response.ok) { throw new Error('Primary URL failed'); } return await response.json(); } catch (error) { console.log('Primary tactics URL failed, trying fallback URL'); const fallbackURL = (defaultTacticsDataUrl === CDN_URLS.default.tactics) ? CDN_URLS.china.tactics : CDN_URLS.default.tactics; const fallbackResponse = await fetch(fallbackURL); return await fallbackResponse.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"; } } // ============================== // TACTICS MANAGEMENT FUNCTIONS // ============================== 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; } // ============================== // TACTIC CRUD OPERATIONS // ============================== 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: '', placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, 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 xmlResult = await showAlert({ title: USERSCRIPT_STRINGS.xmlPlaceholder, input: 'text', showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); const xml = xmlResult.value; if (!xml) { return; } const nameResult = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, 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 name = nameResult.value; if (!name) { return; } try { 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 confirmResult = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace("{}", selectedTactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmResult.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, placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, 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({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.updateConfirmation.replace("{}", selectedTactic.name), 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 confirmResult = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.clearConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmResult.isConfirmed) { return; } await GM_deleteValue("ls_tactics"); dropdownMenuTactics = []; const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); tacticsDropdownMenu.innerHTML = ""; tacticsDropdownMenu.appendChild(createPlaceholderOption()); tacticsDropdownMenu.disabled = true; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert); } async function resetTactics() { const confirmResult = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.resetConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmResult.isConfirmed) { return; } await GM_deleteValue("ls_tactics"); try { const response = await fetch(defaultTacticsDataUrl); if (!response.ok) { throw new Error('Primary tactics URL failed'); } const data = await response.json(); const defaultTactics = data.tactics; await GM_setValue("ls_tactics", { tactics: defaultTactics }); dropdownMenuTactics = defaultTactics; } catch (error) { console.log('Primary tactics URL failed, trying fallback URL'); const fallbackURL = (defaultTacticsDataUrl === CDN_URLS.default.tactics) ? CDN_URLS.china.tactics : CDN_URLS.default.tactics; const fallbackResponse = await fetch(fallbackURL); const fallbackData = await fallbackResponse.json(); const defaultTactics = fallbackData.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); } // ============================== // IMPORT/EXPORT // ============================== async function importTactics() { try { const result = await showAlert({ title: 'Import Tactics', input: 'text', inputValue: '', placeholder: 'Tactics JSON', showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.importButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) { return; } let importedData; try { importedData = JSON.parse(result.value); } 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); } catch (error) { console.error(error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, DEFAULT_MODAL_STRINGS.error); } } async function exportTactics() { try { const tactics = GM_getValue("ls_tactics", { tactics: [] }); const tacticsJson = JSON.stringify(tactics, null, 2); if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(tacticsJson); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportAlert); return; } catch (clipboardError) { console.warn(DEFAULT_MODAL_STRINGS.error, clipboardError); } } await showAlert({ title: "Copy to Clipboard", text: "Please copy this JSON data manually:", input: 'text', inputValue: tacticsJson, confirmButtonText: "Done" }); } catch (error) { console.error(error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, DEFAULT_MODAL_STRINGS.error); } } // ============================== // XML HANDLING // ============================== 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 }; } // ============================== // UI ELEMENT CREATION // ============================== function createButton(id, text, clickHandler) { const button = document.createElement("button"); setUpButton(button, id, text); button.addEventListener("click", function () { clickHandler().catch((_) => { }); }); return button; } function createAddNewTacticButton() { return createButton("add_tactic_button", USERSCRIPT_STRINGS.addButton, addNewTactic); } function createAddNewTacticWithXmlButton() { return createButton("add_tactic_with_xml_button", USERSCRIPT_STRINGS.addWithXmlButton, addNewTacticWithXml); } function createDeleteTacticButton() { return createButton("delete_tactic_button", USERSCRIPT_STRINGS.deleteButton, deleteTactic); } function createRenameTacticButton() { return createButton("rename_tactic_button", USERSCRIPT_STRINGS.renameButton, renameTactic); } function createUpdateTacticButton() { return createButton("update_tactic_button", USERSCRIPT_STRINGS.updateButton, updateTactic); } function createClearTacticsButton() { return createButton("clear_tactics_button", USERSCRIPT_STRINGS.clearButton, clearTactics); } function createResetTacticsButton() { return createButton("reset_tactics_button", USERSCRIPT_STRINGS.resetButton, resetTactics); } function createImportTacticsButton() { return createButton("import_tactics_button", USERSCRIPT_STRINGS.importButton, importTactics); } function createExportTacticsButton() { return createButton("export_tactics_button", USERSCRIPT_STRINGS.exportButton, exportTactics); } // ============================== // VERSION HANDLING // ============================== async function checkVersion() { const storedVersion = GM_getValue(VERSION_KEY, null); if (!storedVersion || storedVersion !== VERSION) { await showWelcomeMessage(); GM_setValue(VERSION_KEY, VERSION); } } // ============================== // AUDIO FEATURES // ============================== function playRandomAudio(audios) { if (audios.length === 0) { return; } const randomIdx = Math.floor(Math.random() * audios.length); const activeAudio = audios.splice(randomIdx, 1)[0]; playAudio(activeAudio, audios); return activeAudio; } function playAudio(currAudio, audios) { currAudio.play(); currAudio.onended = function () { playRandomAudio(audios); }; } function pauseAudio(audio) { if (audio) { audio.pause(); audio.currentTime = 0; } } function updateAudioIcon(button, isPlaying) { button.textContent = isPlaying ? "⏸️" : "🔊"; } function createAudioButton() { const button = document.createElement("button"); setUpButton(button, "audio_button", "🔊"); const audioUrls = [ "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2003%20Special%20Discount.mp3", "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2004%20First%20Floor.mp3", "https://ia801901.us.archive.org/31/items/corp.-palm-mall-01-palm-mall/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20-%2006%20Second%20Floor.mp3", "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20%26%20SEPHORA%E8%84%B3%E3%83%90%E3%82%A4%E3%83%96%E3%82%B9%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2006%20Second%20floor-%20%ED%99%98%EB%8C%80%20%26%20%EC%9D%8C%EC%95%85.mp3", "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2001%20%E3%82%B9%E3%82%AD%E3%83%9D%E3%83%BC%E3%83%AB%E7%A9%BA%E6%B8%AFPlaza.mp3", "https://ia801901.us.archive.org/7/items/palm-mall-mars-remastered/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20Palm%20Mall%20Mars%20%28remastered%29%20-%2009%20Sembikiya%20Restaurant.mp3", "https://ia804504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2001%20FORUM%20%E6%B6%88%E8%B2%BB%E8%80%85-kuluttaja-.mp3", "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2002%20Pelican%20Self%20Storage%20-Tilaa%20Kaikelle-.mp3", "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2003%20%E8%B2%B7%E3%81%86%40JUMBO%20-Kauppakeskus-.mp3", "https://ia904504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2005%20Hesburger%20%E6%98%A0%E7%94%BB%E9%A4%A8%20-hampurilainen-.mp3", "https://ia804504.us.archive.org/20/items/5-wn9896/%E7%8C%AB%20%E3%82%B7%20Corp.%20-%20%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%20%40%20%E3%83%98%E3%83%AB%E3%82%B7%E3%83%B3%E3%82%AD%20-%2006%20%E9%83%BD%E5%B8%82%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A9%E3%83%A0%20Consumer%20-kahvi-.mp3" ]; const audios = audioUrls.map(url => new Audio(url)); let isPlaying = false; let currentAudio = null; button.addEventListener("click", function () { if (!isPlaying) { currentAudio = playRandomAudio(audios); isPlaying = true; } else { pauseAudio(currentAudio); isPlaying = false; } updateAudioIcon(button, isPlaying); }); return button; } // ============================== // UI CONSTRUCTION // ============================== function createMainContainer() { const container = document.createElement("div"); container.id = "mz_tactics_panel"; container.classList.add("mz-panel"); const tacticGroup = document.createElement("div"); tacticGroup.classList.add("mz-group"); const mainTitle = document.createElement("h2"); mainTitle.classList.add("mz-group-main-title"); const titleText = document.createElement("span"); titleText.textContent = "MZ Tactics Manager"; mainTitle.appendChild(titleText); const vText = document.createElement("span"); vText.textContent = "v11"; vText.classList.add("mz-version-text"); mainTitle.appendChild(vText); const dropdownSection = document.createElement("div"); const tacticsDropdownMenuLabel = createDropdownMenuLabel("tactics_dropdown_menu_label"); const tacticsDropdownMenu = createTacticsDropdownMenu(); const tacticsDropdownGroup = createLabelDropdownMenuGroup(tacticsDropdownMenuLabel, tacticsDropdownMenu); dropdownSection.appendChild(tacticsDropdownGroup); const buttonsSection = document.createElement("div"); buttonsSection.style.marginTop = "10px"; const addNewTacticBtn = createAddNewTacticButton(); const addNewTacticWithXmlBtn = createAddNewTacticWithXmlButton(); const deleteTacticBtn = createDeleteTacticButton(); const renameTacticBtn = createRenameTacticButton(); const updateTacticBtn = createUpdateTacticButton(); const clearTacticsBtn = createClearTacticsButton(); const resetTacticsBtn = createResetTacticsButton(); const importTacticsBtn = createImportTacticsButton(); const exportTacticsBtn = createExportTacticsButton(); appendChildren(buttonsSection, [ addNewTacticBtn, addNewTacticWithXmlBtn, deleteTacticBtn, renameTacticBtn, updateTacticBtn, clearTacticsBtn, resetTacticsBtn, importTacticsBtn, exportTacticsBtn ]); appendChildren(tacticGroup, [ mainTitle, dropdownSection, buttonsSection, createHiddenTriggerButton() ]); const otherGroup = document.createElement("div"); otherGroup.classList.add("mz-group"); const otherContainer = document.createElement("div"); otherContainer.style.display = "flex"; otherContainer.style.justifyContent = "space-between"; otherContainer.style.alignItems = "center"; otherContainer.style.width = "100%"; const otherLeftGroup = document.createElement("div"); otherLeftGroup.style.display = "flex"; otherLeftGroup.style.alignItems = "center"; const usefulLinksBtn = createUsefulLinksButton(); const aboutBtn = createAboutButton(); const audioBtn = createAudioButton(); appendChildren(otherLeftGroup, [usefulLinksBtn, aboutBtn, audioBtn]); const otherRightGroup = document.createElement("div"); otherRightGroup.style.display = "flex"; otherRightGroup.style.alignItems = "center"; const languageDropdownMenuLabel = createDropdownMenuLabel("language_dropdown_menu_label"); const languageDropdownMenu = createLanguageDropdownMenu(); const languageDropdownGroup = createLabelDropdownMenuGroup(languageDropdownMenuLabel, languageDropdownMenu); const flagImage = createFlagImage(); appendChildren(otherRightGroup, [languageDropdownGroup, flagImage]); appendChildren(otherContainer, [otherLeftGroup, otherRightGroup]); appendChildren(otherGroup, [otherContainer]); appendChildren(container, [tacticGroup, otherGroup]); return container; } function createHiddenTriggerButton() { const button = document.createElement("button"); button.id = "hidden_trigger_button"; button.textContent = ""; button.style.visibility = "hidden"; button.addEventListener("click", function () { const tacticsPresetInfo = { elem: document.getElementById("tactics_preset"), resetValue: "5-3-2" }; tacticsPresetInfo.elem.value = tacticsPresetInfo.resetValue; tacticsPresetInfo.elem.dispatchEvent(new Event("change")); }); return button; } function insertAfterElement(something, element) { element.parentNode.insertBefore(something, element.nextSibling); } function appendChildren(parent, children) { children.forEach((ch) => { parent.appendChild(ch); }); } function setUpButton(button, id, textContent) { button.id = id; button.classList.add('mzbtn'); button.textContent = textContent; } function createTacticsDropdownMenu() { const dropdown = document.createElement("select"); setUpDropdownMenu(dropdown, "tactics_dropdown_menu"); appendChildren(dropdown, [createPlaceholderOption()]); return dropdown; } function createDropdownMenuLabel(labelId) { const label = document.createElement("span"); setUpDropdownMenuLabel(label, labelId, USERSCRIPT_STRINGS.languageDropdownMenuLabel); return label; } function createLabelDropdownMenuGroup(label, dropdown) { const group = document.createElement("div"); group.classList.add('dropdown-group'); group.appendChild(label); group.appendChild(dropdown); return group; } function setUpDropdownMenu(dropdown, id) { dropdown.id = id; } function createPlaceholderOption() { const placeholderOption = document.createElement("option"); placeholderOption.value = ""; placeholderOption.text = ""; placeholderOption.disabled = true; placeholderOption.selected = true; return placeholderOption; } function addTacticsToDropdownMenu(dropdown, tactics) { for (const tactic of tactics) { const option = document.createElement("option"); option.value = tactic.name; option.text = tactic.name; dropdown.appendChild(option); } } function setUpDropdownMenuLabel(description, id, textContent) { description.id = id; description.textContent = textContent; } function createLanguageDropdownMenu() { const dropdown = document.createElement("select"); setUpDropdownMenu(dropdown, "language_dropdown_menu"); for (const lang of LANGUAGES) { const option = document.createElement("option"); option.value = lang.code; option.textContent = lang.name; if (lang.code === activeLanguage) { option.selected = true; } dropdown.appendChild(option); } dropdown.addEventListener("change", function () { changeLanguage(this.value).catch((_) => { }); }); return dropdown; } function createFlagImage() { const img = document.createElement("img"); img.id = "language_flag"; const activeLang = LANGUAGES.find((lang) => lang.code === activeLanguage); if (activeLang) { img.src = activeLang.flag; } return img; } // ============================== // LOCALIZATION // ============================== function getActiveLanguage() { let language = GM_getValue("language"); if (!language) { let browserLanguage = navigator.language || "en"; browserLanguage = browserLanguage.split("-")[0]; const languageExists = LANGUAGES.some((lang) => lang.code === browserLanguage); language = languageExists ? browserLanguage : "en"; } return language; } function updateTranslation() { for (const key in USERSCRIPT_STRINGS) { USERSCRIPT_STRINGS[key] = i18next.t(key); } for (const id in ELEMENT_STRING_KEYS) { const element = document.getElementById(id); if (id === "info_modal_info_text" || id === "info_modal_feedback_text") { if (element) element.innerHTML = USERSCRIPT_STRINGS[ELEMENT_STRING_KEYS[id]]; } else if (element) { element.textContent = USERSCRIPT_STRINGS[ELEMENT_STRING_KEYS[id]]; } } } async function changeLanguage(languageCode) { try { const translationDataUrl = langDataBaseUrl + languageCode + ".json"; let translations; try { const response = await fetch(translationDataUrl); if (!response.ok) { throw new Error('Primary language URL failed'); } translations = await response.json(); } catch (error) { console.log('Primary language URL failed, trying fallback URL'); const fallbackBaseUrl = (langDataBaseUrl === CDN_URLS.default.lang) ? CDN_URLS.china.lang : CDN_URLS.default.lang; const fallbackUrl = fallbackBaseUrl + languageCode + ".json"; const fallbackResponse = await fetch(fallbackUrl); translations = await fallbackResponse.json(); } i18next.changeLanguage(languageCode); i18next.addResourceBundle(languageCode, "translation", translations); GM_setValue("language", languageCode); updateTranslation(); const language = LANGUAGES.find((lang) => lang.code === languageCode); if (language) { const flagImage = document.getElementById("language_flag"); if (flagImage) flagImage.src = language.flag; } } catch (e) { console.error('Failed to change language:', e); } } // ============================== // UTILITY FUNCTIONS // ============================== function generateUniqueId(coordinates) { const sortedCoordinates = coordinates.sort((a, b) => a[1] - b[1] || a[0] - b[0]); const coordString = sortedCoordinates.map((coord) => coord[1] + "_" + coord[0]).join("_"); return sha256Hash(coordString); } // ============================== // MODALS // ============================== function createUsefulLinksModal() { const modal = document.createElement("div"); setUpModal(modal, "useful_links_modal"); const modalContent = createUsefulLinksModalContent(); modal.appendChild(modalContent); return modal; } function createUsefulLinksModalContent() { const modalContent = document.createElement("div"); const usefulContent = createUsefulContent(); const resources = new Map([ ["gewlaht - BoooM", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=11415137&forum_id=49&sport=soccer"], ["taktikskola by honken91", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12653892&forum_id=4&sport=soccer"], ["peto - mix de dibujos", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12196312&forum_id=255&sport=soccer"], ["The Zone Chile", "https://www.managerzone.com/thezone/paper.php?paper_id=18036&page=9&sport=soccer"], ["Tactics guide by lukasz87o/filipek4", "https://www.managerzone.com/?p=forum&sub=topic&topic_id=12766444&forum_id=12&sport=soccer&share_sport=soccer"], ["MZExtension/van.mz.playerAdvanced by vanjoge", "https://greasyfork.org/pt-BR/scripts/373382-van-mz-playeradvanced"], ["Mazyar Userscript", "https://greasyfork.org/pt-BR/scripts/476290-mazyar"], ["Stats Xente Userscript", "https://greasyfork.org/pt-BR/scripts/491442-stats-xente-script"], ["More userscripts", "https://greasyfork.org/pt-BR/users/1088808-douglasdotv"] ]); const usefulLinksList = createLinksList(resources); modalContent.appendChild(usefulContent); modalContent.appendChild(usefulLinksList); return modalContent; } function createUsefulContent() { const usefulContent = document.createElement("p"); usefulContent.id = "useful_content"; usefulContent.textContent = ""; return usefulContent; } function createLinksList(hrefs) { const list = document.createElement("ul"); hrefs.forEach((href, title) => { const listItem = document.createElement("li"); const link = document.createElement("a"); link.href = href; link.textContent = title; listItem.appendChild(link); list.appendChild(listItem); }); return list; } function setUsefulLinksModal() { usefulLinksModal = createUsefulLinksModal(); document.body.appendChild(usefulLinksModal); } function createInfoModal() { const modal = document.createElement("div"); setUpModal(modal, "info_modal"); const modalContent = createModalContent(); modal.appendChild(modalContent); return modal; } function createModalContent() { const modalContent = document.createElement("div"); const title = createTitle(); const infoText = createInfoText(); const feedbackText = createFeedbackText(); modalContent.appendChild(title); modalContent.appendChild(infoText); modalContent.appendChild(feedbackText); return modalContent; } function createTitle() { const title = document.createElement("h2"); title.id = "info_modal_title"; title.style.fontSize = "24px"; title.style.fontWeight = "bold"; title.style.marginBottom = "20px"; return title; } function createInfoText() { const infoText = document.createElement("p"); infoText.id = "info_modal_info_text"; infoText.innerHTML = USERSCRIPT_STRINGS.modalContentInfoText; return infoText; } function createFeedbackText() { const feedbackText = document.createElement("p"); feedbackText.id = "info_modal_feedback_text"; feedbackText.innerHTML = USERSCRIPT_STRINGS.modalContentFeedbackText; return feedbackText; } function setInfoModal() { infoModal = createInfoModal(); document.body.appendChild(infoModal); } function setUpModal(modal, id) { modal.id = id; modal.style.display = "none"; modal.style.position = "fixed"; modal.style.zIndex = "1"; modal.style.left = "50%"; modal.style.top = "50%"; modal.style.transform = "translate(-50%, -50%)"; modal.style.opacity = "0"; modal.style.transition = "opacity 0.5s ease-in-out"; } function toggleModal(modal) { if (modal.style.display === "none" || modal.style.opacity === "0") { showModal(modal); } else { hideModal(modal); } } function showModal(modal) { modal.style.display = "block"; setTimeout(function () { modal.style.opacity = "1"; }, 0); } function hideModal(modal) { modal.style.opacity = "0"; setTimeout(function () { modal.style.display = "none"; }, 500); } function setUpModalsWindowClickListener() { window.addEventListener("click", function (event) { if (usefulLinksModal.style.display === "block" && !usefulLinksModal.contains(event.target)) { hideModal(usefulLinksModal); } if (infoModal.style.display === "block" && !infoModal.contains(event.target)) { hideModal(infoModal); } }); } function createUsefulLinksButton() { const button = document.createElement("button"); setUpButton(button, "useful_links_button", USERSCRIPT_STRINGS.usefulLinksButton); button.addEventListener("click", function (event) { event.stopPropagation(); toggleModal(usefulLinksModal); }); return button; } function createAboutButton() { const button = document.createElement("button"); setUpButton(button, "about_button", USERSCRIPT_STRINGS.aboutButton); button.addEventListener("click", function (event) { event.stopPropagation(); toggleModal(infoModal); }); return button; } function createToggleButton() { const button = document.createElement('button'); button.id = 'toggle_panel_btn'; button.innerHTML = '⌃'; button.title = 'Hide panel'; return button; } function createCollapsedIcon() { const icon = document.createElement('div'); icon.id = 'collapsed_icon'; icon.innerHTML = '👇'; icon.title = 'Show MZ Tactics Manager'; document.body.appendChild(icon); return icon; } // ============================== // REGION DETECTION // ============================== function isLikelyFromChina() { const lang = navigator.language || navigator.userLanguage || ''; const ua = navigator.userAgent.toLowerCase(); const region = navigator.language?.split('-')[1] || ''; return lang.startsWith('zh-') || ua.includes('micromessenger') || ua.includes('qq') || ua.includes('ucbrowser') || 中国地区.includes(region); } // ============================== // INITIALIZATION // ============================== function initializeLanguage() { return new Promise((resolve, reject) => { activeLanguage = getActiveLanguage(); i18next.init({ lng: activeLanguage, resources: { [activeLanguage]: { translation: {} } } }).then(async () => { try { let json; try { const url = langDataBaseUrl + activeLanguage + ".json"; const res = await fetch(url); if (!res.ok) { throw new Error('Primary language URL failed during initialization'); } json = await res.json(); } catch (error) { console.log('Primary language URL failed during initialization, trying fallback URL'); const fallbackBaseURL = (langDataBaseUrl === CDN_URLS.default.lang) ? CDN_URLS.china.lang : CDN_URLS.default.lang; const fallbackUrl = fallbackBaseURL + activeLanguage + ".json"; const fallbackRes = await fetch(fallbackUrl); json = await fallbackRes.json(); } i18next.addResourceBundle(activeLanguage, "translation", json); await checkVersion(); resolve(); } catch (error) { reject(error); } }).catch(reject); }); } function setUpTacticsInterface(mainContainer) { const mainTitle = mainContainer.querySelector('.mz-group-main-title'); const toggleBtn = createToggleButton(); const collapsedIcon = createCollapsedIcon(); mainTitle.appendChild(toggleBtn); let isCollapsed = false; function togglePanel() { isCollapsed = !isCollapsed; mainContainer.classList.toggle('collapsed'); toggleBtn.classList.toggle('collapsed'); collapsedIcon.classList.toggle('visible'); } toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); }); collapsedIcon.addEventListener('click', () => { togglePanel(); }); } async function loadTacticsData() { try { const data = await fetchTacticsFromGMStorage(); const tacticsDropdownMenu = document.getElementById("tactics_dropdown_menu"); tacticsDropdownMenu.addEventListener('click', function () { if (this.value) { handleTacticsSelection(this.value); } }); dropdownMenuTactics = data.tactics; dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); addTacticsToDropdownMenu(tacticsDropdownMenu, dropdownMenuTactics); tacticsDropdownMenu.addEventListener("change", function () { handleTacticsSelection(this.value); }); } catch (error) { console.error("Error loading tactics data:", error); } } function initialize() { const tacticsBox = document.getElementById("tactics_box"); if (!tacticsBox) return; initializeLanguage() .then(() => { const mainContainer = createMainContainer(); setUpTacticsInterface(mainContainer); if (isFootball()) { insertAfterElement(mainContainer, tacticsBox); } setInfoModal(); setUsefulLinksModal(); setUpModalsWindowClickListener(); updateTranslation(); return loadTacticsData(); }) .catch(error => { console.error("Initialization error:", error); }); } window.addEventListener("load", initialize); })();