// ==UserScript==
// @name MZ Tactics Manager
// @namespace douglaskampl
// @version 12.0.0
// @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.0";
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.0.0!\n\nWhat's new:\n• Categories for organizing tactics (existing tactics have no category but 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 we're deleting a tactic from the category we're currently filtering on,
// check if there are any tactics left in that category
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;
// Check if the category is changing
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 we're changing category and currently filtering by the old category,
// check if any tactics remain in that category
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 = '';
// Add "All" filter tab
const allFilter = createFilterTab('all', "All", currentFilter === 'all');
filterTabsContainer.appendChild(allFilter);
// Find all categories that have at least one tactic
const usedCategories = new Set();
dropdownMenuTactics.forEach(tactic => {
if (tactic.style && tactic.style !== OTHER_CATEGORY_ID) {
usedCategories.add(tactic.style);
}
});
// Add filter tabs for each used category
for (const categoryId of usedCategories) {
if (categories[categoryId]) {
const categoryFilter = createFilterTab(categoryId, categories[categoryId].name, currentFilter === categoryId);
filterTabsContainer.appendChild(categoryFilter);
}
}
// Check if the current filter still exists in categories
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');
}
});
}
}
// Add "Other" filter tab if there are uncategorized tactics
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) {
// If there are no uncategorized tactics but currentFilter is "other",
// switch to "all"
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);
})();