// ==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);
})();