您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Userscript to manage tactics in ManagerZone
当前为
// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 12.0.1 // @description Userscript to manage 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 // ==/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");:root{--bg-color:#1e2028;--text-color:#e0e5ec;--highlight-color:#ff9933;--accent-color:#4f7cac;--shadow-color-dark:rgba(0,0,0,0.7);--shadow-color-light:rgba(59,66,82,0.5);--border-radius:16px;--neu-shadow-flat:6px 6px 12px var(--shadow-color-dark),-6px -6px 12px var(--shadow-color-light);--neu-shadow-pressed:inset 4px 4px 8px var(--shadow-color-dark),inset -4px -4px 8px var(--shadow-color-light);--neu-shadow-concave:6px 6px 12px var(--shadow-color-dark),-6px -6px 12px var(--shadow-color-light),inset 1px 1px 2px var(--shadow-color-light),inset -1px -1px 2px var(--shadow-color-dark);--short-passing-color:#4682B4;--wing-play-color:#3CB371;--other-style-color:#9370DB;--uncategorized-color:#888888;}#mz_tactics_panel{font-family:"Space Grotesk",-apple-system,sans-serif;background-color:var(--bg-color);border-radius:var(--border-radius);padding:24px;margin:12px;box-shadow:var(--neu-shadow-flat);border:none;transition:all 0.3s ease-in-out;max-height:1000px;opacity:1;color:var(--text-color);overflow:hidden;}#mz_tactics_panel.collapsed{max-height:0;padding:0;margin:0;opacity:0;border:none;}.mz-group{background-color:var(--bg-color);border-radius:var(--border-radius);padding:20px;margin:12px 0;box-shadow:var(--neu-shadow-concave);border:none;position:relative;}.mz-group-main-title{display:flex;justify-content:space-between;align-items:center;color:var(--text-color);font-size:18px;font-weight:500;margin:-4px 0 16px 0;padding-bottom:10px;border-bottom:1px solid rgba(51,51,51,0.1);}.mz-main-title{color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:20px;font-weight:500;margin:0;padding:0;text-align:center;letter-spacing:0.2px;}.mz-version-text{color:var(--highlight-color);font-family:"Dancing Script",cursive;font-size:1em;font-weight:500;margin-left:6px;}.mz-divider{width:50px;height:2px;background:var(--text-color);margin:10px auto 0;opacity:0.2;}#toggle_panel_btn{background:var(--bg-color);border:none;color:var(--text-color);cursor:pointer;padding:8px;width:32px;height:32px;border-radius:50%;box-shadow:var(--neu-shadow-flat);margin-left:auto;font-size:18px;transition:all 0.3s ease;display:inline-flex;align-items:center;justify-content:center;}#toggle_panel_btn:hover{box-shadow:var(--neu-shadow-pressed);}#toggle_panel_btn.collapsed{transform:rotate(180deg);}#toggle_panel_btn.collapsed:hover{transform:rotate(180deg);box-shadow:var(--neu-shadow-pressed);}#collapsed_icon{position:fixed;top:20px;right:20px;background:var(--bg-color);border-radius:50%;width:48px;height:48px;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transition:all 0.3s ease;transform:scale(0);box-shadow:var(--neu-shadow-flat);z-index:1000;color:var(--text-color);font-size:20px;}#collapsed_icon.visible{opacity:1;transform:scale(1);}#collapsed_icon:hover{transform:scale(1.05);box-shadow:var(--neu-shadow-pressed);}#mz_tactics_panel .mzbtn{display:inline-flex;align-items:center;justify-content:center;padding:10px 16px;margin:6px;font-family:"Space Grotesk",sans-serif;font-size:14px;font-weight:500;color:var(--text-color);background:var(--bg-color);border:none;border-radius:10px;cursor:pointer;transition:all 0.3s ease;box-shadow:var(--neu-shadow-flat);}#mz_tactics_panel .mzbtn:hover{box-shadow:var(--neu-shadow-pressed);transform:translateY(-1px);}#mz_tactics_panel .mzbtn:active{box-shadow:var(--neu-shadow-pressed);transform:translateY(0);}#mz_tactics_panel select{font-family:"Space Grotesk",sans-serif;font-size:14px;color:var(--text-color);padding:10px 16px;border:none;border-radius:10px;background-color:var(--bg-color);cursor:pointer;margin:6px 0;transition:all 0.3s ease;box-shadow:var(--neu-shadow-flat);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml;utf8,<svg fill='%23e0e5ec' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");background-repeat:no-repeat;background-position:right 10px top 50%;padding-right:30px;width:100%;}#mz_tactics_panel select:hover{box-shadow:var(--neu-shadow-pressed);}#mz_tactics_panel select:focus{outline:none;box-shadow:var(--neu-shadow-pressed);}.tactics-selector-section{margin-bottom:16px;}.tactics-selector-label{font-size:16px;font-weight:500;margin-bottom:10px;display:block;}#language_flag{height:12px;width:16px;margin:6px;border:none;border-radius:6px;box-shadow:var(--neu-shadow-flat);}#info_modal,#useful_links_modal{background:var(--bg-color);padding:24px;border-radius:var(--border-radius);color:var(--text-color);width:90%;max-width:500px;box-shadow:var(--neu-shadow-flat);}#info_modal a,#useful_links_modal a{color:#70a9ff;text-decoration:none;transition:color 0.3s ease;}#info_modal a:hover,#useful_links_modal a:hover{color:#97c4ff;}#info_modal ul,#useful_links_modal ul{list-style:none;padding:0;}#info_modal ul li,#useful_links_modal ul li{margin:12px 0;padding:8px 12px;border-radius:8px;background:var(--bg-color);box-shadow:var(--neu-shadow-flat);transition:all 0.3s ease;}#info_modal ul li:hover,#useful_links_modal ul li:hover{box-shadow:var(--neu-shadow-pressed);}#mz-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(30,32,40,0.8);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;transition:opacity 0.3s ease;}#mz-modal-container{background:var(--bg-color);border-radius:var(--border-radius);padding:24px;box-shadow:var(--neu-shadow-flat);border:none;max-width:500px;width:90%;transform:scale(0.9);transition:transform 0.3s ease;color:var(--text-color);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:20px;border-bottom:1px solid rgba(51,51,51,0.1);padding-bottom:12px;}#mz-modal-title{font-size:20px;font-weight:500;margin:0;}#mz-modal-close{background:var(--bg-color);border:none;color:var(--text-color);font-size:22px;cursor:pointer;transition:all 0.3s ease;padding:0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:50%;box-shadow:var(--neu-shadow-flat);}#mz-modal-close:hover{box-shadow:var(--neu-shadow-pressed);}#mz-modal-content{margin-bottom:24px;white-space:pre-line;line-height:1.5;}#mz-modal-input{width:calc(100% - 32px);background:var(--bg-color);border:none;color:var(--text-color);padding:14px 16px;border-radius:10px;font-family:"Space Grotesk",sans-serif;font-size:15px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;box-shadow:var(--neu-shadow-pressed);}#mz-modal-input:focus{outline:none;box-shadow:var(--neu-shadow-pressed),0 0 0 3px rgba(74,111,165,0.2);}#mz-modal-buttons{display:flex;justify-content:flex-start;gap:12px;}.mz-modal-btn{display:inline-flex;align-items:center;justify-content:center;padding:10px 18px;font-family:"Space Grotesk",sans-serif;font-size:15px;font-weight:500;color:var(--text-color);background:var(--bg-color);border:none;border-radius:10px;cursor:pointer;transition:all 0.3s ease;min-width:90px;box-shadow:var(--neu-shadow-flat);}.mz-modal-btn:hover{box-shadow:var(--neu-shadow-pressed);transform:translateY(-1px);}.mz-modal-btn:active{box-shadow:var(--neu-shadow-pressed);transform:translateY(0);}.mz-modal-btn.primary{background:var(--bg-color);color:#70a9ff;font-weight:600;}.mz-modal-btn.primary:hover{color:#97c4ff;}.mz-modal-btn.cancel{background:var(--bg-color);color:#666;}.mz-modal-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;margin-right:14px;box-shadow:var(--neu-shadow-flat);}.mz-modal-icon.success{color:#22c55e;}.mz-modal-icon.error{color:#ef4444;}.mz-modal-icon.info{color:#4a6fa5;}.mz-modal-title-with-icon{display:flex;align-items:center;}.mz-modal-icon.success{color:#4ade80;}.mz-modal-icon.error{color:#f87171;}.mz-modal-icon.info{color:#60a5fa;}#mz_tactics_panel select{background-image:url("data:image/svg+xml;utf8,<svg fill='%23e0e5ec' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");}.tactics-selector-container{position:relative;width:100%;}.tactics-dropdown-container{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px;}.tactics-search-box{width:180px !important;padding:10px 12px;margin-bottom:0 !important;border:none;border-radius:10px;background-color:var(--bg-color);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-shadow:var(--neu-shadow-pressed);box-sizing:border-box;height:40px;transition:all 0.3s ease,box-shadow 0.3s ease,transform 0.2s ease;position:relative;}.tactics-search-box:focus{outline:none;box-shadow:var(--neu-shadow-pressed),0 0 0 3px rgba(74,111,165,0.2);transform:translateY(-1px);}.tactics-search-box.filtering{border-bottom:2px solid var(--highlight-color);animation:pulse-border 1.5s infinite;}@keyframes pulse-border{0%{border-color:var(--highlight-color);}50%{border-color:transparent;}100%{border-color:var(--highlight-color);}}.tactics-filter-tabs{display:flex;width:100%;margin-top:10px;margin-bottom:10px;overflow-x:auto;padding-bottom:5px;}.tactics-filter-tab{padding:8px 14px;margin-right:8px;border:none;border-radius:8px;background-color:var(--bg-color);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:13px;cursor:pointer;white-space:nowrap;box-shadow:var(--neu-shadow-flat);transition:all 0.3s ease,transform 0.2s ease;}.tactics-filter-tab:hover{box-shadow:var(--neu-shadow-pressed);transform:translateY(-1px);}.tactics-filter-tab.active{box-shadow:var(--neu-shadow-pressed);font-weight:500;transform:translateY(1px);}.tactics-filter-tab[data-filter="short_passing"]{border-bottom:2px solid var(--short-passing-color);}.tactics-filter-tab[data-filter="wing_play"]{border-bottom:2px solid var(--wing-play-color);}.tactics-filter-tab[data-filter="other"]{border-bottom:2px solid var(--other-style-color);}.tactics-dropdown-wrapper{flex:1;min-width:200px;position:relative;}.tactics-style-indicator{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;}.tactics-style-indicator.short_passing{background-color:var(--short-passing-color);}.tactics-style-indicator.wing_play{background-color:var(--wing-play-color);}.tactics-style-indicator.other{background-color:var(--other-style-color);}.tactics-style-indicator.uncategorized{background-color:var(--uncategorized-color);}.tactics-category-header{color:rgba(224,229,236,0.7);font-size:12px;font-weight:600;padding:4px 10px;background:rgba(0,0,0,0.2);margin-top:4px;border-radius:4px;}.tactics-selector-modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);backdrop-filter:blur(3px);display:flex;justify-content:center;align-items:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity 0.3s ease;}.tactics-selector-modal.active{opacity:1;visibility:visible;}.tactics-modal-content{width:90%;max-width:500px;max-height:80vh;background:var(--bg-color);border-radius:var(--border-radius);box-shadow:var(--neu-shadow-flat);overflow:hidden;transform:scale(0.9);transition:transform 0.3s ease;}.tactics-selector-modal.active .tactics-modal-content{transform:scale(1);}.tactics-modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid rgba(51,51,51,0.2);}.tactics-modal-title{font-size:18px;font-weight:500;color:var(--text-color);}.tactics-modal-close{width:32px;height:32px;border-radius:50%;background:var(--bg-color);color:var(--text-color);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--neu-shadow-flat);transition:all 0.3s ease;}.tactics-modal-close:hover{box-shadow:var(--neu-shadow-pressed);}.tactics-modal-search{padding:16px 20px;border-bottom:1px solid rgba(51,51,51,0.2);}.tactics-modal-filters{padding:12px 20px;border-bottom:1px solid rgba(51,51,51,0.2);display:flex;flex-wrap:wrap;gap:8px;}.tactics-modal-list{padding:16px 20px;max-height:50vh;overflow-y:auto;}.tactics-modal-item{padding:10px 14px;margin-bottom:8px;border-radius:8px;background:var(--bg-color);color:var(--text-color);cursor:pointer;display:flex;align-items:center;box-shadow:var(--neu-shadow-flat);transition:all 0.3s ease;}.tactics-modal-item:hover{box-shadow:var(--neu-shadow-pressed);}.tactics-modal-item.selected{box-shadow:var(--neu-shadow-pressed);background:rgba(74,111,165,0.1);}.tactics-modal-actions{padding:16px 20px;border-top:1px solid rgba(51,51,51,0.2);display:flex;justify-content:flex-end;gap:10px;}#category-selector{width:100%;margin-top:10px;padding:10px 12px;border:none;border-radius:10px;background-color:var(--bg-color);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-shadow:var(--neu-shadow-pressed);box-sizing:border-box;}#category-selector option{padding:8px;}.category-selection-container{margin-top:15px;margin-bottom:5px;}.category-selection-label{display:block;margin-bottom:5px;font-size:14px;color:var(--text-color);opacity:0.9;}.mz-language-container{display:flex;align-items:center;gap:10px;}.mz-language-label{font-size:14px;font-weight:500;}.mz-language-dropdown{flex:1;}.new-category-input-container{margin-top:10px;display:none;}.new-category-input-container.visible{display:block;}#new-category-input{width:100%;padding:10px 12px;border:none;border-radius:10px;background-color:var(--bg-color);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-shadow:var(--neu-shadow-pressed);box-sizing:border-box;}#new-category-input:focus{outline:none;box-shadow:var(--neu-shadow-pressed),0 0 0 3px rgba(74,111,165,0.2);}.filter-tab-custom{border-bottom:2px solid;}#tactics_selector{height:40px;box-sizing:border-box;max-height:300px;overflow-y:auto;}#tactics_selector option{animation:fadeIn 0.3s ease;background-color:var(--bg-color);padding:8px 12px;margin:2px 0;}#tactics_selector optgroup{background-color:rgba(79,124,172,0.2);border-left:3px solid var(--accent-color);font-weight:600;padding:8px 10px;margin-top:5px;border-radius:6px;color:var(--text-color);}@keyframes fadeIn{from{opacity:0;transform:translateY(-5px);}to{opacity:1;transform:translateY(0);}}@keyframes shake{0%,100%{transform:translateX(0);}25%{transform:translateX(-2px);}50%{transform:translateX(0);}75%{transform:translateX(2px);}}.tactics-dropdown-wrapper.filtering:after{content:'';position:absolute;width:10px;height:10px;border-radius:50%;background-color:var(--highlight-color);right:40px;top:15px;animation:pulse 1.5s infinite;}@keyframes pulse{0%{transform:scale(0.8);opacity:0.5;}50%{transform:scale(1.2);opacity:1;}100%{transform:scale(0.8);opacity:0.5;}}`); // ============================== // 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 = "12.0.1"; const VERSION_KEY = "mz_tactics_version"; const CATEGORIES_STORAGE_KEY = "mz_tactics_categories"; const TACTICS_STORAGE_KEY = "ls_tactics"; const DEFAULT_CATEGORIES = { "short_passing": { id: "short_passing", name: "Short Passing", color: "#4682B4" }, "wing_play": { id: "wing_play", name: "Wing Play", color: "#3CB371" } }; const NEW_CATEGORY_ID = "new_category"; const OTHER_CATEGORY_ID = "other"; const USERSCRIPT_STRINGS = { addButton: "Add", addWithXmlButton: "Add with XML", deleteButton: "Delete", renameButton: "Edit", updateButton: "Update Coords", clearButton: "Clear All", resetButton: "Reset", importButton: "Import", exportButton: "Export", usefulLinksButton: "Links", aboutButton: "About", tacticNamePrompt: "Please enter a name for the tactic", addAlert: "Tactic {} added successfully.", deleteAlert: "Tactic {} deleted successfully.", renameAlert: "Tactic {} successfully edited.", updateAlert: "Tactic {} updated successfully.", clearAlert: "Tactics cleared successfully.", resetAlert: "Tactics reset successfully.", importAlert: "Tactics imported successfully.", exportAlert: "Tactics copied to clipboard.", 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: "Select a tactic:", 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: "Tactic name", managerTitle: "MZ Tactics Manager", tacticActionsTitle: "Actions", otherActionsTitle: "Other", searchPlaceholder: "Search tactics", allTacticsFilter: "All", selectTacticButton: "Select", openTacticsSelector: "Browse Tactics", noTacticsFound: "No tactics found", welcomeMessage: "Welcome to MZ Tactics Manager v12!\n\nWhat's new:\n• Categories for organizing tactics (existing tactics categories can be edited)\n• Basic filtering\n• UI\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", /* - - - - Temporarily disabled - - - - */ /* 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; let currentFilter = "all"; let searchTerm = ""; let categories = {}; // ============================== // 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, categorySelector, newCategoryInput, overlay, resolve) { if (options.input === 'text' && options.inputValidator) { const hasError = validateModalInput(input, options.inputValidator, 'mz-modal-error'); if (hasError) return; } let categoryValue = null; let newCategoryName = null; if (categorySelector) { categoryValue = categorySelector.value; if (categoryValue === NEW_CATEGORY_ID && newCategoryInput) { newCategoryName = newCategoryInput.value.trim(); if (!newCategoryName) { const errorText = document.createElement('div'); errorText.style.color = '#ef4444'; errorText.style.marginTop = '-10px'; errorText.style.marginBottom = '10px'; errorText.style.fontSize = '13px'; errorText.textContent = "Category name cannot be empty"; errorText.id = 'new-category-error'; const existingError = document.getElementById('new-category-error'); if (existingError) { existingError.remove(); } newCategoryInput.parentNode.insertBefore(errorText, newCategoryInput.nextSibling); return; } const existingCategory = Object.values(categories).find( cat => cat.name.toLowerCase() === newCategoryName.toLowerCase() ); if (existingCategory) { const errorText = document.createElement('div'); errorText.style.color = '#ef4444'; errorText.style.marginTop = '-10px'; errorText.style.marginBottom = '10px'; errorText.style.fontSize = '13px'; errorText.textContent = "This category already exists"; errorText.id = 'new-category-error'; const existingError = document.getElementById('new-category-error'); if (existingError) { existingError.remove(); } newCategoryInput.parentNode.insertBefore(errorText, newCategoryInput.nextSibling); return; } } } closeModal(overlay, () => { if (options.input === 'text') { const result = { value: input ? input.value : null, isConfirmed: true }; if (categorySelector) { if (categoryValue === NEW_CATEGORY_ID && newCategoryName) { const newCategoryId = generateCategoryId(newCategoryName); const newCategory = { id: newCategoryId, name: newCategoryName, color: generateCategoryColor(newCategoryName) }; result.category = newCategory; addCategory(newCategory); } else { result.category = categories[categoryValue] || categories[Object.keys(categories)[0]]; } } resolve(result); } 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; let categorySelector; let newCategoryInput; if (options.input === 'text') { input = document.createElement('input'); input.id = 'mz-modal-input'; input.type = 'text'; input.value = options.inputValue || ''; input.placeholder = options.placeholder || ''; } if (options.showCategorySelector) { const categoryContainer = document.createElement('div'); categoryContainer.className = 'category-selection-container'; const categoryLabel = document.createElement('label'); categoryLabel.className = 'category-selection-label'; categoryLabel.textContent = "Category:"; categoryContainer.appendChild(categoryLabel); categorySelector = document.createElement('select'); categorySelector.id = 'category-selector'; const categoryList = Object.values(categories); categoryList.sort((a, b) => a.name.localeCompare(b.name)); categoryList.forEach(category => { if (category.id !== OTHER_CATEGORY_ID) { const option = document.createElement('option'); option.value = category.id; option.textContent = category.name; categorySelector.appendChild(option); } }); const addNewOption = document.createElement('option'); addNewOption.value = NEW_CATEGORY_ID; addNewOption.textContent = "New category"; categorySelector.appendChild(addNewOption); if (options.currentCategory && options.currentCategory !== OTHER_CATEGORY_ID) { categorySelector.value = options.currentCategory; } categorySelector.addEventListener('change', function() { const newCategoryContainer = document.querySelector('.new-category-input-container'); if (this.value === NEW_CATEGORY_ID) { newCategoryContainer.classList.add('visible'); newCategoryInput.focus(); } else { newCategoryContainer.classList.remove('visible'); } }); categoryContainer.appendChild(categorySelector); const newCategoryContainer = document.createElement('div'); newCategoryContainer.className = 'new-category-input-container'; newCategoryInput = document.createElement('input'); newCategoryInput.id = 'new-category-input'; newCategoryInput.type = 'text'; newCategoryInput.placeholder = "Category"; newCategoryContainer.appendChild(newCategoryInput); categoryContainer.appendChild(newCategoryContainer); if (content.textContent) { content.appendChild(document.createElement('br')); } content.appendChild(categoryContainer); } const buttons = document.createElement('div'); buttons.id = 'mz-modal-buttons'; const confirmHandler = () => { handleAlertConfirm(options, input, categorySelector, newCategoryInput, 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(TACTICS_STORAGE_KEY); 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(TACTICS_STORAGE_KEY, data); } async function validateDuplicateTactic(id) { const tacticsData = (await GM_getValue(TACTICS_STORAGE_KEY)) || { tactics: [] }; return tacticsData.tactics.some((tactic) => tactic.id === id); } async function saveTacticToStorage(tactic) { const tacticsData = (await GM_getValue(TACTICS_STORAGE_KEY)) || { tactics: [] }; tacticsData.tactics.push(tactic); await GM_setValue(TACTICS_STORAGE_KEY, 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 generateCategoryId(categoryName) { return sha256Hash(categoryName.toLowerCase()).substring(0, 10); } function generateCategoryColor(categoryName) { const hash = sha256Hash(categoryName); const hue = parseInt(hash.substring(0, 6), 16) % 360; const saturation = 60 + (parseInt(hash.substring(6, 8), 16) % 30); const lightness = 45 + (parseInt(hash.substring(8, 10), 16) % 15); return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } function addCategory(category) { categories[category.id] = category; saveCategories(); } function saveCategories() { GM_setValue(CATEGORIES_STORAGE_KEY, categories); } function loadCategories() { const storedCategories = GM_getValue(CATEGORIES_STORAGE_KEY); if (storedCategories) { categories = storedCategories; } else { categories = { ...DEFAULT_CATEGORIES }; saveCategories(); } } function loadCategoryColor(categoryId) { if (categories[categoryId]) { return categories[categoryId].color; } else if (categoryId === 'short_passing') { return '#4682B4'; } else if (categoryId === 'wing_play') { return '#3CB371'; } else if (categoryId === 'other' || !categoryId) { return '#9370DB'; } else { return '#888888'; } } function getCategoryName(categoryId) { if (categories[categoryId]) { return categories[categoryId].name; } else if (categoryId === 'short_passing') { return 'Short Passing'; } else if (categoryId === 'wing_play') { return 'Wing Play'; } else if (categoryId === 'other' || !categoryId) { return 'Other'; } else { return categoryId || 'Uncategorized'; } } // ============================== // 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 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; } }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) { return; } const tacticName = result.value; const tacticCategory = result.category.id; const tactic = { name: tacticName, coordinates: tacticCoordinates, id: tacticId, style: tacticCategory }; await saveTacticToStorage(tactic); dropdownMenuTactics.push(tactic); updateTacticsDropdown(); updateFilterTabs(); const tacticsSelector = document.getElementById("tactics_selector"); tacticsSelector.value = tactic.name; 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 }); if (!xmlResult.isConfirmed || !xmlResult.value) { return; } const xml = xmlResult.value; 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; } }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!nameResult.isConfirmed || !nameResult.value) { return; } const name = nameResult.value; const category = nameResult.category.id; try { const newTactic = await convertXmlToTacticJson(xml, name); newTactic.style = category; 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); dropdownMenuTactics.push(newTactic); updateTacticsDropdown(); updateFilterTabs(); const tacticsSelector = document.getElementById("tactics_selector"); tacticsSelector.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 tacticsSelector = document.getElementById("tactics_selector"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsSelector.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 deletedCategoryId = selectedTactic.style; const tacticsData = (await GM_getValue(TACTICS_STORAGE_KEY)) || { tactics: [] }; tacticsData.tactics = tacticsData.tactics.filter((tactic) => tactic.id !== selectedTactic.id); await GM_setValue(TACTICS_STORAGE_KEY, tacticsData); dropdownMenuTactics = dropdownMenuTactics.filter((tactic) => tactic.id !== selectedTactic.id); if (currentFilter !== 'all' && currentFilter === deletedCategoryId) { const categoryStillHasTactics = dropdownMenuTactics.some(tactic => tactic.style === deletedCategoryId); if (!categoryStillHasTactics) { currentFilter = 'all'; } } updateTacticsDropdown(); updateFilterTabs(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace("{}", selectedTactic.name)); } async function renameTactic() { const tacticsSelector = document.getElementById("tactics_selector"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsSelector.value); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const oldName = selectedTactic.name; const oldCategory = selectedTactic.style; 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; } }, showCategorySelector: true, currentCategory: selectedTactic.style === OTHER_CATEGORY_ID ? null : selectedTactic.style, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) { return; } const newName = result.value; const newCategory = result.category.id; const categoryChanged = oldCategory !== newCategory; const tacticsData = (await GM_getValue(TACTICS_STORAGE_KEY)) || { tactics: [] }; tacticsData.tactics = tacticsData.tactics.map((tactic) => { if (tactic.id === selectedTactic.id) { tactic.name = newName; tactic.style = newCategory; } return tactic; }); await GM_setValue(TACTICS_STORAGE_KEY, tacticsData); if (categoryChanged && currentFilter === oldCategory) { const oldCategoryStillHasTactics = tacticsData.tactics.some(tactic => tactic.style === oldCategory); if (!oldCategoryStillHasTactics) { currentFilter = 'all'; } } dropdownMenuTactics = dropdownMenuTactics.map((tactic) => { if (tactic.id === selectedTactic.id) { tactic.name = newName; tactic.style = newCategory; } return tactic; }); updateTacticsDropdown(); updateFilterTabs(); tacticsSelector.value = newName; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, 'Changes applied!'); } async function updateTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const tacticsSelector = document.getElementById("tactics_selector"); const selectedTactic = dropdownMenuTactics.find((tactic) => tactic.name === tacticsSelector.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(TACTICS_STORAGE_KEY)) || { 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(TACTICS_STORAGE_KEY, 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(TACTICS_STORAGE_KEY); dropdownMenuTactics = []; currentFilter = 'all'; updateTacticsDropdown(); updateFilterTabs(); 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(TACTICS_STORAGE_KEY); currentFilter = 'all'; 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; defaultTactics.forEach(tactic => { if (!tactic.hasOwnProperty('style')) { tactic.style = OTHER_CATEGORY_ID; } }); await GM_setValue(TACTICS_STORAGE_KEY, { 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; defaultTactics.forEach(tactic => { if (!tactic.hasOwnProperty('style')) { tactic.style = OTHER_CATEGORY_ID; } }); await GM_setValue(TACTICS_STORAGE_KEY, { tactics: defaultTactics }); dropdownMenuTactics = defaultTactics; } updateTacticsDropdown(); updateFilterTabs(); 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; importedTactics.forEach(tactic => { if (!tactic.hasOwnProperty('style')) { tactic.style = OTHER_CATEGORY_ID; } }); let existingTactics = await GM_getValue(TACTICS_STORAGE_KEY, { 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(TACTICS_STORAGE_KEY, { tactics: mergedTactics }); mergedTactics.sort((a, b) => a.name.localeCompare(b.name)); dropdownMenuTactics = mergedTactics; updateTacticsDropdown(); updateFilterTabs(); 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(TACTICS_STORAGE_KEY, { 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 }; } // ============================== // ENHANCED TACTICS SELECTOR // ============================== function createTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; const label = document.createElement('label'); label.id = 'tactics_dropdown_menu_label'; label.className = 'tactics-selector-label'; label.textContent = USERSCRIPT_STRINGS.tacticsDropdownMenuLabel; container.appendChild(label); const dropdownContainer = document.createElement('div'); dropdownContainer.className = 'tactics-dropdown-container'; const dropdownWrapper = document.createElement('div'); dropdownWrapper.className = 'tactics-dropdown-wrapper'; const dropdown = document.createElement('select'); dropdown.id = 'tactics_selector'; dropdown.addEventListener('change', function() { handleTacticsSelection(this.value); }); dropdownWrapper.appendChild(dropdown); dropdownContainer.appendChild(dropdownWrapper); const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.className = 'tactics-search-box'; searchBox.placeholder = "Search…" searchBox.addEventListener('input', (e) => { searchTerm = e.target.value.toLowerCase(); updateTacticsDropdown(); }); dropdownContainer.appendChild(searchBox); const filterTabs = document.createElement('div'); filterTabs.className = 'tactics-filter-tabs'; filterTabs.id = 'tactics-filter-tabs'; const allFilter = createFilterTab('all', "All", true); filterTabs.appendChild(allFilter); dropdownContainer.appendChild(filterTabs); container.appendChild(dropdownContainer); return container; } function createFilterTab(filter, label, isActive = false) { const tab = document.createElement('button'); tab.className = 'tactics-filter-tab'; if (isActive) tab.classList.add('active'); tab.textContent = label; tab.dataset.filter = filter; if (filter !== 'all') { if (filter === OTHER_CATEGORY_ID) { tab.classList.add('filter-tab-custom'); tab.style.borderBottomColor = '#9370DB'; } else if (filter in categories) { tab.classList.add('filter-tab-custom'); tab.style.borderBottomColor = categories[filter].color; } } tab.addEventListener('click', () => { document.querySelectorAll('.tactics-filter-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); currentFilter = filter; updateTacticsDropdown(); }); return tab; } function updateFilterTabs() { const filterTabsContainer = document.getElementById('tactics-filter-tabs'); if (!filterTabsContainer) return; filterTabsContainer.innerHTML = ''; const allFilter = createFilterTab('all', "All", currentFilter === 'all'); filterTabsContainer.appendChild(allFilter); const usedCategories = new Set(); dropdownMenuTactics.forEach(tactic => { if (tactic.style && tactic.style !== OTHER_CATEGORY_ID) { usedCategories.add(tactic.style); } }); for (const categoryId of usedCategories) { if (categories[categoryId]) { const categoryFilter = createFilterTab(categoryId, categories[categoryId].name, currentFilter === categoryId); filterTabsContainer.appendChild(categoryFilter); } } if (currentFilter !== 'all' && currentFilter !== OTHER_CATEGORY_ID) { const categoryStillExists = usedCategories.has(currentFilter); if (!categoryStillExists) { currentFilter = 'all'; document.querySelectorAll('.tactics-filter-tab').forEach(tab => { tab.classList.remove('active'); if (tab.dataset.filter === 'all') { tab.classList.add('active'); } }); } } const hasUncategorizedTactics = dropdownMenuTactics.some( tactic => tactic.style === OTHER_CATEGORY_ID || !tactic.style ); if (hasUncategorizedTactics) { const otherFilter = createFilterTab(OTHER_CATEGORY_ID, "Other", currentFilter === OTHER_CATEGORY_ID); filterTabsContainer.appendChild(otherFilter); } else if (currentFilter === OTHER_CATEGORY_ID) { currentFilter = 'all'; document.querySelectorAll('.tactics-filter-tab').forEach(tab => { tab.classList.remove('active'); if (tab.dataset.filter === 'all') { tab.classList.add('active'); } }); } } function updateTacticsDropdown() { const dropdown = document.getElementById('tactics_selector'); const dropdownWrapper = document.querySelector('.tactics-dropdown-wrapper'); const searchBox = document.querySelector('.tactics-search-box'); if (!dropdown) return; dropdown.innerHTML = ''; if (searchTerm.length > 0) { dropdownWrapper.classList.add('filtering'); searchBox.classList.add('filtering'); } else { dropdownWrapper.classList.remove('filtering'); searchBox.classList.remove('filtering'); } const placeholderOption = document.createElement('option'); placeholderOption.value = ''; placeholderOption.textContent = ''; placeholderOption.disabled = true; placeholderOption.selected = dropdownMenuTactics.length === 0; dropdown.appendChild(placeholderOption); const filteredTactics = dropdownMenuTactics.filter(tactic => { const matchesSearch = searchTerm === '' || tactic.name.toLowerCase().includes(searchTerm); const matchesFilter = currentFilter === 'all' || (currentFilter === OTHER_CATEGORY_ID && (tactic.style === OTHER_CATEGORY_ID || !tactic.style)) || tactic.style === currentFilter; return matchesSearch && matchesFilter; }); const groupedTactics = {}; for (const categoryId in categories) { groupedTactics[categoryId] = []; } if (!groupedTactics[OTHER_CATEGORY_ID]) { groupedTactics[OTHER_CATEGORY_ID] = []; } filteredTactics.forEach(tactic => { if (!tactic.style || tactic.style === OTHER_CATEGORY_ID) { groupedTactics[OTHER_CATEGORY_ID].push(tactic); } else { const categoryId = tactic.style; if (!groupedTactics[categoryId]) { groupedTactics[categoryId] = []; } groupedTactics[categoryId].push(tactic); } }); if (currentFilter === 'all') { for (const categoryId in groupedTactics) { if (groupedTactics[categoryId].length > 0) { addTacticOptionsGroup(dropdown, groupedTactics[categoryId], getCategoryName(categoryId)); } } } else if (currentFilter === OTHER_CATEGORY_ID) { if (groupedTactics[OTHER_CATEGORY_ID] && groupedTactics[OTHER_CATEGORY_ID].length > 0) { addTacticOptionsGroup(dropdown, groupedTactics[OTHER_CATEGORY_ID], "Other"); } } else { if (groupedTactics[currentFilter] && groupedTactics[currentFilter].length > 0) { addTacticOptionsGroup(dropdown, groupedTactics[currentFilter], getCategoryName(currentFilter)); } } if (filteredTactics.length === 0) { const noTacticsOption = document.createElement('option'); noTacticsOption.disabled = true; noTacticsOption.textContent = USERSCRIPT_STRINGS.noTacticsFound; dropdown.appendChild(noTacticsOption); } dropdown.disabled = dropdownMenuTactics.length === 0; } function addTacticOptionsGroup(dropdown, tactics, groupLabel) { if (tactics.length === 0) return; const groupHeader = document.createElement('optgroup'); groupHeader.label = groupLabel; dropdown.appendChild(groupHeader); tactics.sort((a, b) => a.name.localeCompare(b.name)); tactics.forEach(tactic => { const option = document.createElement('option'); option.value = tactic.name; option.dataset.style = tactic.style || OTHER_CATEGORY_ID; option.textContent = tactic.name; dropdown.appendChild(option); }); } // ============================== // 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", 'Edit', renameTactic); } function createUpdateTacticButton() { return createButton("update_tactic_button", 'Update Coords', 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 = "v12"; vText.classList.add("mz-version-text"); mainTitle.appendChild(vText); const tacticsSelector = createTacticsSelector(); 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, tacticsSelector, 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.className = "mz-language-container"; const languageLabel = document.createElement("div"); languageLabel.id = "language_dropdown_menu_label"; languageLabel.className = "mz-language-label"; languageLabel.textContent = USERSCRIPT_STRINGS.languageDropdownMenuLabel; const languageDropdownWrapper = document.createElement("div"); languageDropdownWrapper.className = "mz-language-dropdown"; const languageDropdownMenu = createLanguageDropdownMenu(); languageDropdownWrapper.appendChild(languageDropdownMenu); const flagImage = createFlagImage(); appendChildren(otherRightGroup, [languageLabel, languageDropdownWrapper, 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 createLanguageDropdownMenu() { const dropdown = document.createElement("select"); dropdown.id = "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]]; } } const allFilterTab = document.querySelector('.tactics-filter-tab[data-filter="all"]'); if (allFilterTab) allFilterTab.textContent = "All"; for (const categoryId in categories) { const filterTab = document.querySelector(`.tactics-filter-tab[data-filter="${categoryId}"]`); if (filterTab) filterTab.textContent = categories[categoryId].name; } const otherFilterTab = document.querySelector(`.tactics-filter-tab[data-filter="${OTHER_CATEGORY_ID}"]`); if (otherFilterTab) otherFilterTab.textContent = "Other"; const searchBox = document.querySelector('.tactics-search-box'); if (searchBox) searchBox.placeholder = "Search…"; updateTacticsDropdown(); } 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 = USERSCRIPT_STRINGS.usefulContent; 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"; title.textContent = "MZ Tactics Manager"; 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 = 'X'; button.title = 'Hide panel'; return button; } function createCollapsedIcon() { const icon = document.createElement('div'); icon.id = 'collapsed_icon'; icon.innerHTML = 'MZTM'; 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); loadCategories(); 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(); dropdownMenuTactics = data.tactics; dropdownMenuTactics.forEach(tactic => { if (!tactic.hasOwnProperty('style')) { tactic.style = OTHER_CATEGORY_ID; } }); dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(); updateFilterTabs(); const tacticsSelector = document.getElementById("tactics_selector"); if (tacticsSelector) { tacticsSelector.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); })();