// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 12.1.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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(`@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600&display=swap");@import url("https://fonts.googleapis.com/css2?family=Pacifico&display=swap");:root{--bg-color:#2a2d40;--text-color:#dcdde1;--highlight-color:#a37acc;--accent-color:#6c70d1;--shadow-color-dark:rgba(0,0,0,0.3);--shadow-color-light:rgba(255,255,255,0.05);--border-radius:12px;--shadow-base:3px 3px 6px var(--shadow-color-dark),-3px -3px 6px var(--shadow-color-light);--shadow-inset:inset 2px 2px 4px var(--shadow-color-dark),inset -2px -2px 4px var(--shadow-color-light);--shadow-concave:4px 4px 8px var(--shadow-color-dark),-4px -4px 8px var(--shadow-color-light),inset 1px 1px 2px var(--shadow-color-light),inset -1px -1px 2px var(--shadow-color-dark);--short-passing-color:#54a0ff;--wing-play-color:#5dd39e;--other-style-color:#ffcb77;--uncategorized-color:#8395a7;}#mz_tactics_panel{font-family:"Space Grotesk",-apple-system,sans-serif;background-color:var(--bg-color);border-radius:var(--border-radius);padding:20px;margin:12px;box-shadow:var(--shadow-base);border:1px solid rgba(255,255,255,0.05);transition:max-height 0.4s ease-out, padding 0.4s ease-out, margin 0.4s ease-out, opacity 0.3s ease-out;max-height:1000px;opacity:1;color:var(--text-color);overflow:hidden;}#mz_tactics_panel.collapsed{max-height:0 !important; padding-top: 0 !important; padding-bottom: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; opacity:0 !important; border:none !important; overflow: hidden !important;}.mz-group{background-color:rgba(0,0,0,0.1);border-radius:var(--border-radius);padding:16px;margin:10px 0;box-shadow:none;border:1px solid rgba(255,255,255,0.05);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 12px 0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,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:'Pacifico',cursive;font-size:1.1em;font-weight:400;margin-left:8px;transform:rotate(-5deg);}.mz-divider{width:50px;height:2px;background:var(--text-color);margin:10px auto 0;opacity:0.2;}#toggle_panel_btn{background:transparent;border:none;color:var(--text-color);cursor:pointer;padding:8px;width:32px;height:32px;border-radius:50%;margin-left:auto;font-size:18px;transition:all 0.3s ease;display:inline-flex;align-items:center;justify-content:center;}#toggle_panel_btn:hover{background:rgba(255,255,255,0.1);}#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(--shadow-base);z-index:1000;color:var(--text-color);font-size:16px; font-weight: bold; border: 1px solid rgba(255,255,255,0.1);}#collapsed_icon.visible{opacity:1;transform:scale(1);}#collapsed_icon:hover{transform:scale(1.05);box-shadow:0 0 15px rgba(163, 122, 204, 0.5);}#mz_tactics_panel .mzbtn{display:inline-flex;align-items:center;justify-content:center;padding:8px 14px;margin:4px;font-family:"Space Grotesk",sans-serif;font-size:13px;font-weight:500;color:var(--text-color);background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;cursor:pointer;transition:all 0.2s ease;min-height:36px;box-shadow: 0 1px 2px rgba(0,0,0,0.1);}#mz_tactics_panel .mzbtn:hover{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.2);transform:translateY(-1px);box-shadow: 0 3px 6px rgba(0,0,0,0.15);}#mz_tactics_panel .mzbtn:active{background:rgba(0,0,0,0.1);transform:translateY(0);box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);}#mz_tactics_panel select{font-family:"Space Grotesk",sans-serif;font-size:14px;color:var(--text-color);padding:8px 14px;border:1px solid rgba(255,255,255,0.1);border-radius:8px;background-color:rgba(255,255,255,0.05);cursor:pointer;margin:0 4px;transition:all 0.2s ease;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml;utf8,");background-repeat:no-repeat;background-position:right 10px top 50%;padding-right:30px;height:36px;box-sizing:border-box;}#mz_tactics_panel select:hover{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.2);}#mz_tactics_panel select:focus{outline:none;border-color:var(--accent-color);box-shadow:0 0 0 2px rgba(108, 112, 209, 0.3);}#mz_tactics_panel select option{background-color:var(--bg-color);color:var(--text-color);padding:5px 10px;}.tactics-selector-section{margin-bottom:12px;}.tactics-selector-label{display:none;}#language_flag{height:12px;width:16px;margin:6px;border:none;border-radius:4px;}#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(--shadow-base); border: 1px solid rgba(255,255,255,0.1);}#info_modal a,#useful_links_modal a{color:var(--accent-color);text-decoration:none;transition:color 0.3s ease;}#info_modal a:hover,#useful_links_modal a:hover{color:var(--highlight-color); text-decoration: underline;}#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:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);transition:all 0.3s ease;}#info_modal ul li:hover,#useful_links_modal ul li:hover{background:rgba(255,255,255,0.1);}#mz-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(42, 45, 64, 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:0 10px 25px rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);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(255,255,255,0.1);padding-bottom:12px;}#mz-modal-title{font-size:20px;font-weight:500;margin:0;}#mz-modal-close{background:transparent;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%;}#mz-modal-close:hover{background:rgba(255,255,255,0.1);color:var(--highlight-color);}#mz-modal-content{margin-bottom:24px;white-space:pre-line;line-height:1.5;}#mz-modal-input{width:calc(100% - 32px);background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:var(--text-color);padding:14px 16px;border-radius:8px;font-family:"Space Grotesk",sans-serif;font-size:15px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;}#mz-modal-input:focus{outline:none;border-color:var(--accent-color);box-shadow:0 0 0 2px rgba(108, 112, 209, 0.3);background:rgba(255,255,255,0.08);}#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:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;cursor:pointer;transition:all 0.2s ease;min-width:90px;box-shadow: 0 1px 2px rgba(0,0,0,0.1);}.mz-modal-btn:hover{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.2);transform:translateY(-1px);box-shadow: 0 3px 6px rgba(0,0,0,0.15);}.mz-modal-btn:active{background:rgba(0,0,0,0.1);transform:translateY(0);box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);}.mz-modal-btn.primary{background:var(--accent-color);color:white;font-weight:600; border: none;}.mz-modal-btn.primary:hover{background:var(--highlight-color);}.mz-modal-btn.cancel{background:transparent;color:#aaa;border:1px solid rgba(255,255,255,0.1);}.mz-modal-btn.cancel:hover{background:rgba(255,255,255,0.05);color:var(--text-color); border-color:rgba(255,255,255,0.2);}.mz-modal-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;margin-right:14px;background:rgba(0,0,0,0.1);}.mz-modal-icon.success{color:#5dd39e; background: rgba(93, 211, 158, 0.1);}.mz-modal-icon.error{color:#ff6b6b; background: rgba(255, 107, 107, 0.1);}.mz-modal-icon.info{color:#54a0ff; background: rgba(84, 160, 255, 0.1);}.mz-modal-title-with-icon{display:flex;align-items:center;}.tactics-selector-container{position:relative;width:100%; display: flex; align-items: center; gap: 8px;}.tactics-dropdown-container{display:flex;flex-wrap:nowrap;gap:8px;margin-top:0; flex-grow: 1; align-items: center;}.tactics-search-box{width:160px !important;padding:8px 12px;margin-bottom:0 !important;border:1px solid rgba(255,255,255,0.1);border-radius:8px;background-color:rgba(255,255,255,0.05);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-sizing:border-box;height:36px;transition:all 0.2s ease;position:relative; flex-shrink: 0;}.tactics-search-box:focus{outline:none;border-color:var(--accent-color);box-shadow:0 0 0 2px rgba(108, 112, 209, 0.3);background:rgba(255,255,255,0.08);}.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;margin:0;padding-bottom:0; overflow-x: auto; flex-shrink: 1; min-width: 100px; max-width: 300px; align-items: center; height: 36px; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.2) transparent;}.tactics-filter-tab{padding:4px 10px;margin-right:6px;border:1px solid transparent;border-radius:12px; background-color:transparent;color:var(--text-color);opacity:0.7;font-family:"Space Grotesk",sans-serif;font-size:11px; cursor:pointer;white-space:nowrap;transition:all 0.2s ease; flex-shrink: 0; height: 24px; line-height: 16px;}.tactics-filter-tab:hover{background-color:rgba(255,255,255,0.08);opacity:1;}.tactics-filter-tab.active{background-color: var(--category-color, rgba(255,255,255,0.15)); border-color:transparent;font-weight:500;opacity:1; color: #fff; text-shadow: 0 0 3px rgba(0,0,0,0.5);}.tactics-dropdown-wrapper{flex:1;min-width:180px;position:relative; flex-grow: 1; flex-shrink: 1;}.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(220, 221, 225, 0.7);font-size:12px;font-weight:600;padding:4px 10px;background:rgba(0,0,0,0.2);margin-top:4px;border-radius:4px;}#category-selector{width:100%;margin-top:10px;padding:10px 12px;border:1px solid rgba(255,255,255,0.1);border-radius:8px;background-color:rgba(255,255,255,0.05);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-sizing:border-box;}#category-selector option{padding:8px; background-color: var(--bg-color);}.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:1px solid rgba(255,255,255,0.1);border-radius:8px;background-color:rgba(255,255,255,0.05);color:var(--text-color);font-family:"Space Grotesk",sans-serif;font-size:14px;box-sizing:border-box;}#new-category-input:focus{outline:none;border-color:var(--accent-color);box-shadow:0 0 0 2px rgba(108, 112, 209, 0.3);background:rgba(255,255,255,0.08);}#tactics_selector{height:36px;box-sizing:border-box;max-height:300px;overflow-y:auto; width: 100%;}#tactics_selector option{animation:fadeIn 0.3s ease;background-color:var(--bg-color);padding:8px 12px;margin:2px 0; color: var(--text-color);}#tactics_selector optgroup{background-color:#950606;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:13px;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;}}.action-buttons-section{display: flex; flex-wrap: wrap; margin-top: 10px; justify-content: flex-start; gap: 4px;}.action-dropdown-menu{position:absolute;background-color:var(--bg-color);border-radius:8px;box-shadow:0 5px 15px rgba(0,0,0,0.3);padding:8px;z-index:100;display:none;min-width:150px; border: 1px solid rgba(255,255,255,0.1);}.action-dropdown-menu button{display:block;width:100%;margin:4px 0;text-align:left; background: transparent; border: none; box-shadow: none;}.action-dropdown-menu button:hover{background:rgba(255,255,255,0.1); color: var(--highlight-color); transform: none; box-shadow: none;}.footer-actions{display:flex;align-items:center;gap:10px;}#combined_info_modal_content > div { margin-bottom: 20px; } #combined_info_modal_content h3 { font-size: 18px; margin-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px; color: var(--highlight-color); }`); 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 SCRIPT_VERSION = '12.1.0'; const DISPLAY_VERSION = '12'; const VERSION_KEY = 'mz_tactics_version'; const COLLAPSED_KEY = 'mz_tactics_collapsed'; 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: '#54a0ff' }, 'wing_play': { id: 'wing_play', name: 'Wing Play', color: '#5dd39e' } }; const NEW_CATEGORY_ID = 'new_category'; const OTHER_CATEGORY_ID = 'other'; const USERSCRIPT_STRINGS = { addButton: 'Add', addCurrentTactic: 'Add Current', addWithXmlButton: 'Add via XML', manageButton: 'Manage', deleteButton: 'Delete', renameButton: 'Edit', updateButton: 'Save Positions', clearButton: 'Clear All', resetButton: 'Default', importButton: 'Import', exportButton: 'Export', infoButton: '❔', 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: 'Default tactics loaded.', 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 all saved tactics?', resetConfirmation: 'Reset to default tactics? This will remove all your custom tactics.', invalidTacticError: 'Invalid tactic. Ensure 11 players are on the pitch.', noTacticNameProvidedError: 'No tactic name provided.', alreadyExistingTacticNameError: 'Tactic name already exists.', tacticNameMaxLengthError: 'Tactic name is too long (max 50 chars).', noTacticSelectedError: 'No tactic selected.', duplicateTacticError: 'This formation already exists.', noChangesMadeError: 'No changes detected in player positions.', invalidImportError: 'Invalid import data. Please provide valid JSON.', modalContentInfoText: 'Manage your tactics efficiently.', modalContentFeedbackText: 'For feedback or suggestions, contact douglaskampl via GB/Chat.', usefulContent: 'Community Resources:', tacticsDropdownMenuLabel: 'Select a tactic:', languageDropdownMenuLabel: 'Language:', errorTitle: 'Error', doneTitle: 'Success', confirmationTitle: 'Confirmation', deleteTacticConfirmButton: 'Delete', cancelConfirmButton: 'Cancel', updateConfirmButton: 'Update', clearTacticsConfirmButton: 'Clear All', resetTacticsConfirmButton: 'Reset', addConfirmButton: 'Add', xmlValidationError: 'Invalid XML format.', xmlParsingError: 'Error parsing XML.', xmlPlaceholder: 'Paste XML here', tacticNamePlaceholder: 'Tactic name', managerTitle: 'MZ Tactics Manager', tacticActionsTitle: 'Actions', otherActionsTitle: 'Other', searchPlaceholder: 'Search...', allTacticsFilter: 'All', selectTacticButton: 'Select', openTacticsSelector: 'Browse Tactics', noTacticsFound: 'No tactics found', welcomeMessage: `Welcome to MZ Tactics Manager v${DISPLAY_VERSION}!\n\nThis version includes:\n• New UI for better space usage.\n\nEnjoy managing your tactics!`, welcomeGotIt: 'Got it!' }; const ELEMENT_STRING_KEYS = { delete_tactic_button: 'deleteButton', rename_tactic_button: 'renameButton', update_tactic_button: 'updateButton', tactics_dropdown_menu_label: 'tacticsDropdownMenuLabel', language_dropdown_menu_label: 'languageDropdownMenuLabel', info_modal_info_text: 'modalContentInfoText', info_modal_feedback_text: 'modalContentFeedbackText', }; 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 currentFilter = 'all'; let searchTerm = ''; let categories = {}; let activeDropdownMenu = null; 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); const errorContainer = document.getElementById(errorContainerId) || document.createElement('div'); errorContainer.id = errorContainerId; errorContainer.style.color = '#ff6b6b'; errorContainer.style.marginTop = '-10px'; errorContainer.style.marginBottom = '10px'; errorContainer.style.fontSize = '13px'; const existingError = document.getElementById(errorContainerId); if (existingError) existingError.remove(); if (!validationError) { return null; } errorContainer.textContent = validationError; if (!existingError) { input.parentNode.insertBefore(errorContainer, input.nextSibling); } return validationError; } function closeModal(overlay, callback) { overlay.classList.remove('active'); setTimeout(() => { if (overlay.parentNode === document.body) { 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(); const categoryErrorContainer = document.getElementById('new-category-error'); if (categoryErrorContainer) categoryErrorContainer.remove(); if (!newCategoryName) { const errorText = document.createElement('div'); errorText.style.color = '#ff6b6b'; errorText.style.marginTop = '5px'; errorText.style.fontSize = '13px'; errorText.textContent = 'Category name cannot be empty'; errorText.id = 'new-category-error'; newCategoryInput.parentNode.appendChild(errorText); return; } const existingCategory = Object.values(categories).find( cat => cat.name.toLowerCase() === newCategoryName.toLowerCase() ); if (existingCategory) { const errorText = document.createElement('div'); errorText.style.color = '#ff6b6b'; errorText.style.marginTop = '5px'; errorText.style.fontSize = '13px'; errorText.textContent = 'This category already exists'; errorText.id = 'new-category-error'; newCategoryInput.parentNode.appendChild(errorText); 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] || { id: OTHER_CATEGORY_ID, name: 'Other', color: '#ffcb77' }; } } 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)) { if (input && input.tagName === 'TEXTAREA') {} else { 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'; if (options.htmlContent) { content.appendChild(options.htmlContent); } else { 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 usedCategoryIds = new Set(dropdownMenuTactics.map(t => t.style).filter(Boolean)); if (options.currentCategory) { usedCategoryIds.add(options.currentCategory); } const availableCategories = Object.values(categories).filter(cat => cat.id === 'short_passing' || cat.id === 'wing_play' || cat.id === OTHER_CATEGORY_ID || usedCategoryIds.has(cat.id) ); availableCategories.sort((a, b) => { if (a.id === OTHER_CATEGORY_ID) return 1; if (b.id === OTHER_CATEGORY_ID) return -1; return a.name.localeCompare(b.name); }); availableCategories.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 otherOption = document.createElement('option'); otherOption.value = OTHER_CATEGORY_ID; otherOption.textContent = getCategoryName(OTHER_CATEGORY_ID); categorySelector.appendChild(otherOption); const addNewOption = document.createElement('option'); addNewOption.value = NEW_CATEGORY_ID; addNewOption.textContent = '+ New category'; categorySelector.appendChild(addNewOption); if (options.currentCategory && categories[options.currentCategory]) { categorySelector.value = options.currentCategory; } else { categorySelector.value = OTHER_CATEGORY_ID; } 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'); const categoryErrorContainer = document.getElementById('new-category-error'); if (categoryErrorContainer) categoryErrorContainer.remove(); } }); 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 = 'New category name'; newCategoryContainer.appendChild(newCategoryInput); categoryContainer.appendChild(newCategoryContainer); if (content.textContent || content.hasChildNodes()) { categoryContainer.style.marginTop = '15px'; } 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(); if (categorySelector && categorySelector.value === NEW_CATEGORY_ID) { newCategoryInput.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, confirmButtonText: USERSCRIPT_STRINGS.welcomeGotIt }); } 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(); } return await response.json(); } catch (_e) { 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 generateUniqueId(coordinates) { coordinates.sort((a, b) => { if (a[1] !== b[1]) { return a[1] - b[1]; } else { return a[0] - b[0]; } }); const coordString = coordinates.map(coord => `${coord[0]},${coord[1]}`).join(';'); return sha256Hash(coordString); } 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 = 50 + (parseInt(hash.substring(6, 8), 16) % 30); const lightness = 55 + (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 && typeof storedCategories === 'object') { categories = storedCategories; if (!categories['short_passing']) categories['short_passing'] = DEFAULT_CATEGORIES['short_passing']; if (!categories['wing_play']) categories['wing_play'] = DEFAULT_CATEGORIES['wing_play']; } else { categories = { ...DEFAULT_CATEGORIES }; saveCategories(); } if (!categories[OTHER_CATEGORY_ID]) { categories[OTHER_CATEGORY_ID] = { id: OTHER_CATEGORY_ID, name: 'Other', color: '#ffcb77' }; } } function loadCategoryColor(categoryId) { if (categories[categoryId]) { return categories[categoryId].color; } else if (categoryId === 'short_passing') { return DEFAULT_CATEGORIES.short_passing.color; } else if (categoryId === 'wing_play') { return DEFAULT_CATEGORIES.wing_play.color; } else if (categoryId === 'other' || !categoryId) { return '#ffcb77'; } else { return '#8395a7'; } } 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_CATEGORY_ID || !categoryId) { return 'Other'; } else { return categoryId || 'Uncategorized'; } } function insertAfterElement(something, element) { element.parentNode.insertBefore(something, element.nextSibling); } function appendChildren(parent, children) { children.forEach((ch) => { if (ch) parent.appendChild(ch); }); } function handleTacticsSelection(tacticName) { if (!tacticName) return; const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = dropdownMenuTactics.find((tacticData) => tacticData.name === tacticName); if (selectedTactic) { if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS) { const hiddenTriggerButton = document.getElementById('hidden_trigger_button'); if(hiddenTriggerButton) 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) { if (coordinates[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); if (formationTextElement) { const defs = formationTextElement.querySelector('.defs'); const mids = formationTextElement.querySelector('.mids'); const atts = formationTextElement.querySelector('.atts'); if (defs) defs.textContent = formation.defenders; if (mids) mids.textContent = formation.midfielders; if (atts) atts.textContent = formation.strikers; } } function getFormation(coordinates) { let strikers = 0; let midfielders = 0; let defenders = 0; for (const coo of coordinates) { const y = coo[1]; if (y < 103) { strikers++; } else if (y <= 204) { midfielders++; } else { defenders++; } } return { strikers, midfielders, defenders }; } function validateTacticPlayerCount(outfieldPlayers) { const isGoalkeeper = document.querySelector(GOALKEEPER_SELECTOR); outfieldPlayers = outfieldPlayers.filter((player) => !player.classList.contains('fieldpos-collision')); if (outfieldPlayers.length < MIN_OUTFIELD_PLAYERS || !isGoalkeeper) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError); return false; } return true; } async function addNewTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const 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; return null; }, 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); dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); 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; return null; }, 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); dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); 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) { console.error('XMLError:', e); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError + (e.message ? `: ${e.message}` : '')); } } 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); const categoryStillHasTactics = dropdownMenuTactics.some(tactic => tactic.style === deletedCategoryId); if (!categoryStillHasTactics && deletedCategoryId !== 'short_passing' && deletedCategoryId !== 'wing_play' && deletedCategoryId !== OTHER_CATEGORY_ID) { delete categories[deletedCategoryId]; saveCategories(); if (currentFilter === deletedCategoryId) { 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: 'Edit Tactic', 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; return null; }, showCategorySelector: true, currentCategory: selectedTactic.style, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || (!result.value && !result.category) ) { return; } const newName = result.value || oldName; const newCategory = result.category?.id || oldCategory; if (newName === oldName && newCategory === oldCategory) { return; } 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); dropdownMenuTactics = dropdownMenuTactics.map((tactic) => { if (tactic.id === selectedTactic.id) { tactic.name = newName; tactic.style = newCategory; } return tactic; }); if (categoryChanged) { const oldCategoryStillHasTactics = dropdownMenuTactics.some(tactic => tactic.style === oldCategory); if (!oldCategoryStillHasTactics && oldCategory !== 'short_passing' && oldCategory !== 'wing_play' && oldCategory !== OTHER_CATEGORY_ID) { delete categories[oldCategory]; saveCategories(); if (currentFilter === oldCategory) { currentFilter = 'all'; } } } dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(); updateFilterTabs(); tacticsSelector.value = newName; await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace('{}', newName)); } 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; } if (!validateTacticPlayerCount(outfieldPlayers)) { 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; } } const memoryTactic = dropdownMenuTactics.find(t => t.id === selectedTactic.id); if (memoryTactic) { memoryTactic.coordinates = updatedCoordinates; memoryTactic.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, type: 'error' }); if (!confirmResult.isConfirmed) { return; } await GM_deleteValue(TACTICS_STORAGE_KEY); dropdownMenuTactics = []; currentFilter = 'all'; categories = { ...DEFAULT_CATEGORIES }; if (!categories[OTHER_CATEGORY_ID]) { categories[OTHER_CATEGORY_ID] = { id: OTHER_CATEGORY_ID, name: 'Other', color: '#ffcb77' }; } saveCategories(); 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, type: 'error' }); if (!confirmResult.isConfirmed) { return; } await GM_deleteValue(TACTICS_STORAGE_KEY); currentFilter = 'all'; categories = { ...DEFAULT_CATEGORIES }; if (!categories[OTHER_CATEGORY_ID]) { categories[OTHER_CATEGORY_ID] = { id: OTHER_CATEGORY_ID, name: 'Other', color: '#ffcb77' }; } saveCategories(); try { const response = await fetch(defaultTacticsDataUrl); if (!response.ok) { throw new Error(); } 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; dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); } catch (error) { console.log(); try { const fallbackURL = (defaultTacticsDataUrl === CDN_URLS.default.tactics) ? CDN_URLS.china.tactics : CDN_URLS.default.tactics; const fallbackResponse = await fetch(fallbackURL); if (!fallbackResponse.ok) { throw new Error(); } 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; dropdownMenuTactics.sort((a, b) => a.name.localeCompare(b.name)); } catch (e) { console.error(e); dropdownMenuTactics = []; await GM_setValue(TACTICS_STORAGE_KEY, { tactics: [] }); await showErrorMessage('Error', 'Could not load default tactics.'); return; } } updateTacticsDropdown(); updateFilterTabs(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert); } async function importTactics() { try { const result = await showAlert({ title: 'Import Tactics', input: 'text', inputValue: '', placeholder: 'Paste Tactics JSON here', 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; } if (!tactic.name || !tactic.id || !Array.isArray(tactic.coordinates)) { throw new Error('Invalid tactic structure in imported data.'); } if (tactic.style && !categories[tactic.style] && tactic.style !== OTHER_CATEGORY_ID && tactic.style !== 'short_passing' && tactic.style !== 'wing_play') { addCategory({ id: tactic.style, name: tactic.style, color: generateCategoryColor(tactic.style) }); } }); let existingTacticsData = await GM_getValue(TACTICS_STORAGE_KEY, { tactics: [] }); let existingTactics = existingTacticsData.tactics || []; const mergedTactics = [...existingTactics]; let addedCount = 0; for (const importedTactic of importedTactics) { if (!existingTactics.some((tactic) => tactic.id === importedTactic.id)) { mergedTactics.push(importedTactic); addedCount++; } } 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 + (addedCount > 0 ? ` (${addedCount} new tactics added)`: '')); } catch (error) { console.error('ImportError:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError + (error.message ? `: ${error.message}`: '')); } } 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('Clipboard write failed, falling back to modal.', clipboardError); } } const textarea = document.createElement('textarea'); textarea.value = tacticsJson; textarea.style.width = '100%'; textarea.style.minHeight = '150px'; textarea.style.marginTop = '10px'; textarea.style.backgroundColor = 'rgba(0,0,0,0.2)'; textarea.style.color = 'var(--text-color)'; textarea.style.border = '1px solid rgba(255,255,255,0.1)'; textarea.style.borderRadius = '4px'; textarea.readOnly = true; const container = document.createElement('div'); container.appendChild(document.createTextNode('Copy the JSON data below:')); container.appendChild(textarea); await showAlert({ title: 'Export Tactics', htmlContent: container, confirmButtonText: 'Done' }); textarea.select(); textarea.setSelectionRange(0, 99999); } catch (error) { console.error('Export error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export tactics.'); } } 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) { console.error('XMLError:', parserError[0].textContent); throw new Error(USERSCRIPT_STRINGS.xmlValidationError); } const posElements = Array.from(xmlDoc.getElementsByTagName('Pos')); const normalPosElements = posElements.filter(el => el.getAttribute('pos') === 'normal'); if (normalPosElements.length !== MIN_OUTFIELD_PLAYERS) { throw new Error(`XML must contain exactly ${MIN_OUTFIELD_PLAYERS} outfield players ('Pos' elements with pos="normal"). Found ${normalPosElements.length}.`); } const coordinates = normalPosElements.map(el => { const x = parseInt(el.getAttribute('x')); const y = parseInt(el.getAttribute('y')); if (isNaN(x) || isNaN(y)) { throw new Error('Invalid coordinates found in XML.'); } const htmlLeft = x - 7; const htmlTop = y - 9; return [htmlLeft, htmlTop]; }); return { name: tacticName, coordinates: coordinates }; } function createTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; 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 = USERSCRIPT_STRINGS.searchPlaceholder; 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', USERSCRIPT_STRINGS.allTacticsFilter, 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; const categoryColor = loadCategoryColor(filter); tab.style.setProperty('--category-color', categoryColor); 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', USERSCRIPT_STRINGS.allTacticsFilter, currentFilter === 'all'); allFilter.style.setProperty('--category-color', 'rgba(255,255,255,0.15)'); filterTabsContainer.appendChild(allFilter); const usedCategories = new Set(); dropdownMenuTactics.forEach(tactic => { if (tactic.style) { usedCategories.add(tactic.style); } else { usedCategories.add(OTHER_CATEGORY_ID); } }); const availableCategoryIds = Object.keys(categories).filter(id => id === 'short_passing' || id === 'wing_play' || usedCategories.has(id) ); const sortedCategoryIds = availableCategoryIds.sort((a, b) => { if (a === OTHER_CATEGORY_ID) return 1; if (b === OTHER_CATEGORY_ID) return -1; const nameA = categories[a]?.name || ''; const nameB = categories[b]?.name || ''; return nameA.localeCompare(nameB); }); for (const categoryId of sortedCategoryIds) { if (categoryId !== OTHER_CATEGORY_ID && categories[categoryId]) { const categoryFilter = createFilterTab(categoryId, categories[categoryId].name, currentFilter === categoryId); filterTabsContainer.appendChild(categoryFilter); } } if (currentFilter !== 'all' && currentFilter !== OTHER_CATEGORY_ID && !usedCategories.has(currentFilter) && !DEFAULT_CATEGORIES[currentFilter]) { currentFilter = 'all'; } if (usedCategories.has(OTHER_CATEGORY_ID)) { const otherFilter = createFilterTab(OTHER_CATEGORY_ID, getCategoryName(OTHER_CATEGORY_ID), currentFilter === OTHER_CATEGORY_ID); filterTabsContainer.appendChild(otherFilter); } else if (currentFilter === OTHER_CATEGORY_ID) { currentFilter = 'all'; } let activeTabFound = false; document.querySelectorAll('.tactics-filter-tab').forEach(tab => { if(tab.dataset.filter === currentFilter) { tab.classList.add('active'); activeTabFound = true; } else { tab.classList.remove('active'); } }); if (!activeTabFound) { currentFilter = 'all'; const allTab = document.querySelector('.tactics-filter-tab[data-filter="all"]'); if (allTab) allTab.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; const previouslySelectedValue = dropdown.value; 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 = 'Select a Tactic'; placeholderOption.disabled = true; placeholderOption.selected = true; dropdown.appendChild(placeholderOption); const filteredTactics = dropdownMenuTactics.filter(tactic => { const nameMatch = searchTerm === '' || tactic.name.toLowerCase().includes(searchTerm); const categoryMatch = currentFilter === 'all' || (currentFilter === OTHER_CATEGORY_ID && (tactic.style === OTHER_CATEGORY_ID || !tactic.style)) || tactic.style === currentFilter; return nameMatch && categoryMatch; }); const groupedTactics = {}; Object.keys(categories).forEach(id => { groupedTactics[id] = []; }); if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = []; filteredTactics.forEach(tactic => { const categoryId = tactic.style || OTHER_CATEGORY_ID; if (!groupedTactics[categoryId]) { groupedTactics[OTHER_CATEGORY_ID].push(tactic); } else { groupedTactics[categoryId].push(tactic); } }); const categoryOrder = Object.keys(groupedTactics) .filter(id => groupedTactics[id].length > 0) .sort((a, b) => { if (a === currentFilter) return -1; if (b === currentFilter) return 1; if (a === OTHER_CATEGORY_ID) return 1; if (b === OTHER_CATEGORY_ID) return -1; return (getCategoryName(a) || '').localeCompare(getCategoryName(b) || ''); }); categoryOrder.forEach(categoryId => { if (groupedTactics[categoryId].length > 0) { addTacticOptionsGroup(dropdown, groupedTactics[categoryId], getCategoryName(categoryId)); } }); if (filteredTactics.length === 0 && dropdownMenuTactics.length > 0) { const noTacticsOption = document.createElement('option'); noTacticsOption.disabled = true; noTacticsOption.textContent = USERSCRIPT_STRINGS.noTacticsFound; dropdown.appendChild(noTacticsOption); placeholderOption.selected = false; } else if (filteredTactics.length === 0 && dropdownMenuTactics.length === 0) { placeholderOption.textContent = 'No tactics saved'; } let foundPrevious = false; for (let i = 0; i < dropdown.options.length; i++) { if (dropdown.options[i].value === previouslySelectedValue) { dropdown.selectedIndex = i; foundPrevious = true; break; } } if (!foundPrevious) { if (filteredTactics.length === 1 && categoryOrder.length === 1 && groupedTactics[categoryOrder[0]].length === 1) { for (let i = 0; i < dropdown.options.length; i++) { if (!dropdown.options[i].disabled) { dropdown.selectedIndex = i; break; } } } else { dropdown.selectedIndex = 0; } } 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); }); } function createButton(id, text, clickHandler) { const button = document.createElement('button'); setUpButton(button, id, text); if (clickHandler) { button.addEventListener('click', function (e) { e.stopPropagation(); hideActiveDropdownMenu(); clickHandler().catch((error) => { console.error('Button click handler failed:', error); showErrorMessage('Action Failed', `${error}`); }); }); } return button; } function createActionDropdownButton(id, text, menuItems) { const wrapper = document.createElement('div'); wrapper.style.position = 'relative'; wrapper.style.display = 'inline-block'; const button = createButton(id, text + ' ▼'); const menu = document.createElement('div'); menu.className = 'action-dropdown-menu'; menu.id = id + '_menu'; menuItems.forEach(item => { if (item && item.id && item.text && item.handler) { const menuItem = createButton(item.id, item.text, item.handler); menu.appendChild(menuItem); } }); wrapper.appendChild(button); wrapper.appendChild(menu); button.addEventListener('click', (e) => { e.stopPropagation(); toggleDropdownMenu(menu); }); return wrapper; } function toggleDropdownMenu(menu) { if (activeDropdownMenu && activeDropdownMenu !== menu) { activeDropdownMenu.style.display = 'none'; } if (menu.style.display === 'block') { menu.style.display = 'none'; activeDropdownMenu = null; } else { menu.style.display = 'block'; activeDropdownMenu = menu; menu.style.top = menu.previousElementSibling.offsetHeight + 4 + 'px'; menu.style.left = '0'; } } function hideActiveDropdownMenu() { if (activeDropdownMenu) { activeDropdownMenu.style.display = 'none'; activeDropdownMenu = null; } } function createRenameTacticButton() { return createButton('rename_tactic_button', USERSCRIPT_STRINGS.renameButton, renameTactic); } function createUpdateTacticButton() { return createButton('update_tactic_button', USERSCRIPT_STRINGS.updateButton, updateTactic); } function createDeleteTacticButton() { return createButton('delete_tactic_button', USERSCRIPT_STRINGS.deleteButton, deleteTactic); } function createAddActionButton() { const menuItems = [ { id: 'add_current_tactic_item', text: USERSCRIPT_STRINGS.addCurrentTactic, handler: addNewTactic }, { id: 'add_xml_tactic_item', text: USERSCRIPT_STRINGS.addWithXmlButton, handler: addNewTacticWithXml } ]; return createActionDropdownButton('add_action_dropdown', USERSCRIPT_STRINGS.addButton, menuItems); } function createManageActionButton() { const menuItems = [ { id: 'import_tactics_item', text: USERSCRIPT_STRINGS.importButton, handler: importTactics }, { id: 'export_tactics_item', text: USERSCRIPT_STRINGS.exportButton, handler: exportTactics }, { id: 'reset_tactics_item', text: USERSCRIPT_STRINGS.resetButton, handler: resetTactics }, { id: 'clear_tactics_item', text: USERSCRIPT_STRINGS.clearButton, handler: clearTactics } ]; return createActionDropdownButton('manage_action_dropdown', USERSCRIPT_STRINGS.manageButton, menuItems); } async function checkVersion() { const storedVersion = GM_getValue(VERSION_KEY, null); if (!storedVersion || storedVersion !== SCRIPT_VERSION) { await showWelcomeMessage(); GM_setValue(VERSION_KEY, SCRIPT_VERSION); } } 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().catch(e => console.error('Cannot play 猫 シ Corp.', e)); 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', '🔊'); button.style.background = 'transparent'; button.style.border = 'none'; button.style.boxShadow = 'none'; 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%AF%20Plaza.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' ]; let audios = []; let isPlaying = false; let currentAudio = null; let audioInitialized = false; const initializeAudio = () => { if (!audioInitialized) { audios = audioUrls.map(url => { const audio = new Audio(); audio.src = url; audio.preload = 'metadata'; return audio; }); audioInitialized = true; } }; button.addEventListener('click', function () { initializeAudio(); if (!isPlaying) { const playableAudios = audios.map(a => a.cloneNode()); currentAudio = playRandomAudio(playableAudios); isPlaying = true; } else { pauseAudio(currentAudio); isPlaying = false; } updateAudioIcon(button, isPlaying); }); return button; } 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 = 'v' + DISPLAY_VERSION; vText.classList.add('mz-version-text'); mainTitle.appendChild(vText); const tacticsSelectorSection = createTacticsSelector(); const buttonsSection = document.createElement('div'); buttonsSection.className = 'action-buttons-section'; const addActionBtn = createAddActionButton(); const renameTacticBtn = createRenameTacticButton(); const updateTacticBtn = createUpdateTacticButton(); const deleteTacticBtn = createDeleteTacticButton(); const manageActionBtn = createManageActionButton(); appendChildren(buttonsSection, [ addActionBtn, renameTacticBtn, updateTacticBtn, deleteTacticBtn, manageActionBtn ]); appendChildren(tacticGroup, [ mainTitle, tacticsSelectorSection, buttonsSection, createHiddenTriggerButton() ]); const otherGroup = document.createElement('div'); otherGroup.classList.add('mz-group'); otherGroup.style.padding = '10px 16px'; 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.className = 'footer-actions'; const infoBtn = createCombinedInfoButton(); const audioBtn = createAudioButton(); appendChildren(otherLeftGroup, [infoBtn, 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.position = 'absolute'; button.style.opacity = '0'; button.style.pointerEvents = 'none'; button.style.width = '0'; button.style.height = '0'; button.style.padding = '0'; button.style.margin = '0'; button.style.border = '0'; button.addEventListener('click', function () { const tacticsPresetInfo = { elem: document.getElementById('tactics_preset'), resetValue: '5-3-2' }; if (tacticsPresetInfo.elem) { tacticsPresetInfo.elem.value = tacticsPresetInfo.resetValue; tacticsPresetInfo.elem.dispatchEvent(new Event('change')); } }); return button; } 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; } 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, { defaultValue: USERSCRIPT_STRINGS[key] }); } const renameBtn = document.getElementById('rename_tactic_button'); if(renameBtn) renameBtn.textContent = 'Edit'; const updateBtn = document.getElementById('update_tactic_button'); if(updateBtn) updateBtn.textContent = 'Update Coords'; const deleteBtn = document.getElementById('delete_tactic_button'); if(deleteBtn) deleteBtn.textContent = USERSCRIPT_STRINGS.deleteButton; const addActionBtn = document.getElementById('add_action_dropdown'); if (addActionBtn) addActionBtn.textContent = 'Add' + ' ▼'; const manageActionBtn = document.getElementById('manage_action_dropdown'); if (manageActionBtn) manageActionBtn.textContent = USERSCRIPT_STRINGS.manageButton + ' ▼'; const addCurrentItem = document.getElementById('add_current_tactic_item'); if (addCurrentItem) addCurrentItem.textContent = USERSCRIPT_STRINGS.addCurrentTactic; const addXmlItem = document.getElementById('add_xml_tactic_item'); if (addXmlItem) addXmlItem.textContent = USERSCRIPT_STRINGS.addWithXmlButton; const importItem = document.getElementById('import_tactics_item'); if (importItem) importItem.textContent = USERSCRIPT_STRINGS.importButton; const exportItem = document.getElementById('export_tactics_item'); if (exportItem) exportItem.textContent = USERSCRIPT_STRINGS.exportButton; const resetItem = document.getElementById('reset_tactics_item'); if (resetItem) resetItem.textContent = USERSCRIPT_STRINGS.resetButton; const clearItem = document.getElementById('clear_tactics_item'); if (clearItem) clearItem.textContent = USERSCRIPT_STRINGS.clearButton; const infoBtn = document.getElementById('info_button'); if (infoBtn) infoBtn.textContent = USERSCRIPT_STRINGS.infoButton; const langLabel = document.getElementById('language_dropdown_menu_label'); if(langLabel) langLabel.textContent = USERSCRIPT_STRINGS.languageDropdownMenuLabel; const allFilterTab = document.querySelector('.tactics-filter-tab[data-filter="all"]'); if (allFilterTab) allFilterTab.textContent = i18next.t('allTacticsFilter', { defaultValue: 'All'}); for (const categoryId in categories) { if (categoryId === OTHER_CATEGORY_ID) continue; 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 = getCategoryName(OTHER_CATEGORY_ID); const searchBox = document.querySelector('.tactics-search-box'); if (searchBox) searchBox.placeholder = i18next.t('searchPlaceholder', { defaultValue: 'Search...'}); updateTacticsDropdown(); updateFilterTabs(); } 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 (${languageCode}) failed, trying fallback URL`); const fallbackBaseUrl = (langDataBaseUrl === CDN_URLS.default.lang) ? CDN_URLS.china.lang : CDN_URLS.default.lang; const fallbackUrl = fallbackBaseUrl + languageCode + '.json'; try { const fallbackResponse = await fetch(fallbackUrl); if (!fallbackResponse.ok) throw new Error('Fallback language URL failed'); translations = await fallbackResponse.json(); } catch (fallbackError) { console.error(`Failed to load language ${languageCode} from primary and fallback sources. Using defaults.`); translations = {}; } } await i18next.changeLanguage(languageCode); i18next.addResourceBundle(languageCode, 'translation', translations, true, true); GM_setValue('language', languageCode); activeLanguage = 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); } } function createCombinedInfoModalContent() { const wrapper = document.createElement('div'); wrapper.id = 'combined_info_modal_content'; const aboutSection = document.createElement('div'); const aboutTitle = document.createElement('h3'); aboutTitle.textContent = i18next.t('aboutButton'); const aboutInfoText = document.createElement('p'); aboutInfoText.id = 'info_modal_info_text'; aboutInfoText.innerHTML = i18next.t('modalContentInfoText'); const aboutFeedbackText = document.createElement('p'); aboutFeedbackText.id = 'info_modal_feedback_text'; aboutFeedbackText.innerHTML = i18next.t('modalContentFeedbackText'); appendChildren(aboutSection, [aboutTitle, aboutInfoText, aboutFeedbackText]); const linksSection = document.createElement('div'); const linksTitle = document.createElement('h3'); linksTitle.textContent = i18next.t('usefulLinksButton'); const linksContent = 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); appendChildren(linksSection, [linksTitle, linksContent, usefulLinksList]); appendChildren(wrapper, [aboutSection, linksSection]); return wrapper; } function createUsefulContent() { const usefulContent = document.createElement('p'); usefulContent.id = 'useful_content'; usefulContent.textContent = i18next.t('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.target = '_blank'; link.rel = 'noopener noreferrer'; link.textContent = title; listItem.appendChild(link); list.appendChild(listItem); }); return list; } function createCombinedInfoButton() { const button = document.createElement('button'); setUpButton(button, 'info_button', USERSCRIPT_STRINGS.infoButton); button.style.background = 'transparent'; button.style.border = 'none'; button.style.boxShadow = 'none'; button.addEventListener('click', function (event) { event.stopPropagation(); showAlert({ title: 'Info & Links', htmlContent: createCombinedInfoModalContent(), confirmButtonText: DEFAULT_MODAL_STRINGS.ok }); }); return button; } function createToggleButton() { const button = document.createElement('button'); button.id = 'toggle_panel_btn'; button.innerHTML = '✕'; button.title = 'Hide panel'; return button; } function createCollapsedIcon() { const icon = document.createElement('div'); icon.id = 'collapsed_icon'; icon.innerHTML = 'TM'; icon.title = 'Show MZ Tactics Manager'; document.body.appendChild(icon); return icon; } function isLikelyFromChina() { const lang = navigator.language || navigator.userLanguage || ''; const ua = navigator.userAgent.toLowerCase(); const region = navigator.language?.split('-')[1]?.toUpperCase() || ''; return lang.startsWith('zh-') || ua.includes('micromessenger') || ua.includes('qq') || ua.includes('ucbrowser') || 中国地区.includes(region); } function initializeLanguage() { return new Promise((resolve, reject) => { activeLanguage = getActiveLanguage(); i18next.init({ lng: activeLanguage, fallbackLng: 'en', resources: { [activeLanguage]: { translation: {} } }, interpolation: { escapeValue: false } }) .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 (${activeLanguage}) failed during initialization, trying fallback URL or using defaults.`); const fallbackBaseURL = (langDataBaseUrl === CDN_URLS.default.lang) ? CDN_URLS.china.lang : CDN_URLS.default.lang; const fallbackUrl = fallbackBaseURL + activeLanguage + '.json'; try { const fallbackRes = await fetch(fallbackUrl); if (!fallbackRes.ok) throw new Error('Fallback language URL failed too'); json = await fallbackRes.json(); } catch (fallbackError) { console.error(`Failed to load language ${activeLanguage} from primary and fallback sources. Using defaults.`); } } i18next.addResourceBundle(activeLanguage, 'translation', json, true, true); 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 = GM_getValue(COLLAPSED_KEY, false); const applyCollapsedState = (instant = false) => { if (isCollapsed) { if (instant) { mainContainer.style.transition = 'none'; mainContainer.classList.add('collapsed'); collapsedIcon.classList.add('visible'); toggleBtn.innerHTML = '☰'; toggleBtn.title = 'Show panel'; void mainContainer.offsetHeight; mainContainer.style.transition = ''; } else { mainContainer.classList.add('collapsed'); collapsedIcon.classList.add('visible'); toggleBtn.innerHTML = '☰'; toggleBtn.title = 'Show panel'; } } else { mainContainer.classList.remove('collapsed'); collapsedIcon.classList.remove('visible'); toggleBtn.innerHTML = '✕'; toggleBtn.title = 'Hide panel'; } }; applyCollapsedState(true); function togglePanel() { isCollapsed = !isCollapsed; GM_setValue(COLLAPSED_KEY, isCollapsed); applyCollapsedState(); } 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(); } catch (error) { console.error('ErrorLoadingTactics:', error); showErrorMessage('Load Error', 'Failed to load saved tactics.'); } } function initialize() { const tacticsBox = document.getElementById('tactics_box'); if (!tacticsBox) return; initializeLanguage() .then(() => { const mainContainer = createMainContainer(); setUpTacticsInterface(mainContainer); if (isFootball()) { insertAfterElement(mainContainer, tacticsBox); } updateTranslation(); return loadTacticsData(); }) .catch(error => { console.error('InitializationError:', error); const errorDiv = document.createElement('div'); errorDiv.textContent = 'Error initializing MZ Tactics Manager.'; errorDiv.style.color = 'red'; errorDiv.style.padding = '10px'; errorDiv.style.border = '1px solid red'; errorDiv.style.margin = '10px'; insertAfterElement(errorDiv, tacticsBox); }); } window.addEventListener('load', initialize); document.addEventListener('click', (e) => { if (activeDropdownMenu) { const triggerButton = activeDropdownMenu.previousElementSibling; if (!activeDropdownMenu.contains(e.target) && triggerButton !== e.target && !triggerButton.contains(e.target)) { hideActiveDropdownMenu(); } } }); })();