// ==UserScript== // @name MZ Tactics Manager // @namespace douglaskampl // @version 13.2.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 // @grant GM_getResourceText // @require https://cdnjs.cloudflare.com/ajax/libs/jsSHA/3.3.1/sha256.js // @resource mztmStyles https://br18.org/mz/userscript/tactics/mirassol.css // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(GM_getResourceText('mztmStyles')); 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_PLAYERS_ON_PITCH = 11; const MAX_TACTIC_NAME_LENGTH = 50; const MAX_CATEGORY_NAME_LENGTH = 30; const MAX_DESCRIPTION_LENGTH = 250; const SCRIPT_VERSION = '13.2.0'; const DISPLAY_VERSION = '13.2'; const SCRIPT_NAME = 'MZ Tactics Manager'; const VERSION_KEY = 'mz_tactics_version'; const COLLAPSED_KEY = 'mz_tactics_collapsed'; const VIEW_MODE_KEY = 'mztm_view_mode'; const CATEGORIES_STORAGE_KEY = 'mz_tactics_categories'; const FORMATIONS_STORAGE_KEY = 'mztm_formations'; const OLD_FORMATIONS_STORAGE_KEY = 'ls_tactics'; const COMPLETE_TACTICS_STORAGE_KEY = 'mztm_complete_tactics'; const ROSTER_CACHE_KEY = 'mztm_roster_cache'; const USER_INFO_CACHE_KEY = 'mztm_user_info_cache'; const ROSTER_CACHE_DURATION_MS = 3600000; const USER_INFO_CACHE_DURATION_MS = 86400000; 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: 'Update Coords', clearButton: 'Clear', resetButton: 'Reset', importButton: 'Import', exportButton: 'Export', infoButton: 'FAQ', saveButton: 'Save', tacticNamePrompt: 'Please enter a name and a category', addAlert: 'Formation {} added successfully.', deleteAlert: 'Item {} deleted successfully.', renameAlert: 'Item {} successfully edited.', updateAlert: 'Formation {} updated successfully.', clearAlert: 'Formations cleared successfully.', resetAlert: 'Default formations reset successfully.', importAlert: 'Formations imported successfully.', exportAlert: 'Formations JSON copied to clipboard.', deleteConfirmation: 'Do you really want to delete {}?', updateConfirmation: 'Do you really want to update {} coords?', clearConfirmation: 'Do you really want to clear all saved formations?', resetConfirmation: 'Reset to default formations? This will remove all your custom formations.', invalidTacticError: 'Invalid formation. Ensure 11 players are on the pitch.', noTacticNameProvidedError: 'No name provided.', alreadyExistingTacticNameError: 'Name already exists.', tacticNameMaxLengthError: `Name is too long (max ${MAX_TACTIC_NAME_LENGTH} chars).`, noTacticSelectedError: 'No item selected.', duplicateTacticError: 'This formation already exists.', noChangesMadeError: 'No changes detected in player positions.', invalidImportError: 'Invalid import data. Please provide valid JSON.', modalContentInfoText: 'MZ Tactics Manager by douglaskampl.', modalContentFeedbackText: 'For feedback or suggestions, contact via GB/Chat.', usefulContent: '', tacticsDropdownMenuLabel: 'Select a Formation', completeTacticsDropdownMenuLabel: 'Select a Tactic', errorTitle: 'Error', doneTitle: 'Success', confirmationTitle: 'Confirmation', deleteTacticConfirmButton: 'Delete', cancelConfirmButton: 'Cancel', updateConfirmButton: 'Update', clearTacticsConfirmButton: 'Clear', resetTacticsConfirmButton: 'Reset', addConfirmButton: 'Add', xmlValidationError: 'Invalid XML format', xmlParsingError: 'Error parsing XML', xmlPlaceholder: 'Paste Formation XML here', tacticNamePlaceholder: 'Formation name', managerTitle: SCRIPT_NAME, searchPlaceholder: 'Search...', allTacticsFilter: 'All', noTacticsFound: 'No formations found', welcomeMessage: `Welcome to ${SCRIPT_NAME} v${DISPLAY_VERSION}!\n\nNew features:\n• Category filter is now a dropdown menu.\n• New Management Modal: Edit/remove formations & add/remove categories via the gear icon (⚙️).\n• Preview on Hover: See formation details by hovering over names in the dropdown.\n\nEnjoy!`, welcomeGotIt: 'Got it!', removeCategoryConfirmation: 'Remove category "{}"? (All formations in this category will be moved to "Other").', removeCategoryAlert: 'Category "{}" removed successfully.', removeCategoryButton: 'Remove', completeTacticsTitle: 'Tactics Management', saveCompleteTacticButton: 'Save Current', loadCompleteTacticButton: 'Load', deleteCompleteTacticButton: 'Delete', renameCompleteTacticButton: 'Rename', updateCompleteTacticButton: 'Update with Current', importCompleteTacticsButton: 'Import', exportCompleteTacticsButton: 'Export', completeTacticNamePrompt: 'Please enter a name for the tactic', renameCompleteTacticPrompt: 'Enter a new name for the tactic:', updateCompleteTacticConfirmation: 'Overwrite tactic "{}" with the current setup (positions, rules, settings) from the pitch?', completeTacticSaveSuccess: 'Tactic {} saved successfully.', completeTacticLoadSuccess: 'Tactic {} loaded successfully.', completeTacticDeleteSuccess: 'Tactic {} deleted successfully.', completeTacticRenameSuccess: 'Tactic renamed to {} successfully.', completeTacticUpdateSuccess: 'Tactic {} updated successfully.', importCompleteTacticsTitle: 'Import Tactics (JSON)', exportCompleteTacticsTitle: 'Export Tactics (JSON)', importCompleteTacticsPlaceholder: 'Paste Tactics JSON here', importCompleteTacticsAlert: 'Tactics imported successfully.', exportCompleteTacticsAlert: 'Tactics JSON copied to clipboard.', invalidCompleteImportError: 'Invalid import data. Please provide valid JSON (object map).', errorFetchingRoster: 'Error fetching team roster. Cannot load Tactic.', errorInsufficientPlayers: 'Not enough available players in roster to fill required positions.', errorXmlExportParse: 'Error parsing XML from native export.', errorXmlGenerate: 'Error generating XML for import.', errorImportFailed: 'Native import failed. Check XML validity or player availability.', warningPlayersSubstituted: 'Warning: roster mismatch. Some were players replaced at random. Tactic updated!', invalidXmlForImport: 'MZ rejected the generated XML. It might be invalid or player assignments failed.', completeTacticNamePlaceholder: 'Tactic name', normalModeLabel: 'Formations', completeModeLabel: 'Tactics', modeLabel: '', manageCategoriesTitle: 'Manage Categories', noCustomCategories: 'No custom categories to manage.', manageCategoriesDoneButton: 'Done', managementModalTitle: 'Manage Formations & Categories', formationsTabTitle: 'Formations', categoriesTabTitle: 'Categories', addCategoryPlaceholder: 'New category name...', addCategoryButton: '+ Add', categoryNameMaxLengthError: `Category name too long (max ${MAX_CATEGORY_NAME_LENGTH} chars).`, saveChangesButton: 'Save Changes', changesSavedSuccess: 'Changes saved successfully.', noChangesToSave: 'No changes to save.', descriptionLabel: 'Description (optional):', descriptionPlaceholder: `Enter a short description (max ${MAX_DESCRIPTION_LENGTH} chars)...`, descriptionMaxLengthError: `Description too long (max ${MAX_DESCRIPTION_LENGTH} chars).`, previewFormationLabel: 'Formation:', xmlRequiredError: 'Please paste the XML data first.', invalidXmlFormatError: 'The provided text does not appear to be valid XML.', noTacticsSaved: 'No formations saved', noCompleteTacticsSaved: 'No tactics saved' }; const DEFAULT_MODAL_STRINGS = { ok: 'OK', cancel: 'Cancel', error: 'Error', close: '×' }; let tactics = []; let completeTactics = {}; let currentFilter = 'all'; let searchTerm = ''; let categories = {}; let rosterCache = { data: null, timestamp: 0, teamId: null }; let userInfoCache = { teamId: null, username: null, timestamp: 0 }; let teamId = null; let username = null; let loadingOverlay = null; let currentViewMode = 'normal'; let collapsedIconElement = null; let previewElement = null; let previewHideTimeout = null; let currentOpenDropdown = null; let selectedFormationTacticId = null; let selectedCompleteTacticName = null; function createModalIcon(type) { if (!type) return null; const i = document.createElement('div'); i.classList.add('mz-modal-icon'); if (type === 'success') { i.classList.add('success'); i.innerHTML = '✓'; } else if (type === 'error') { i.classList.add('error'); i.innerHTML = '✗'; } else if (type === 'info') { i.classList.add('info'); i.innerHTML = 'ℹ'; } return i; } function validateModalInput(inputElement, validatorFn, errorElementId) { if (!validatorFn || !inputElement || !inputElement.parentNode) return null; const validationError = validatorFn(inputElement.value); const existingError = document.getElementById(errorElementId); if (existingError) existingError.remove(); if (!validationError) return null; const errorContainer = document.createElement('div'); errorContainer.id = errorElementId; errorContainer.style.color = '#ff6b6b'; errorContainer.style.marginTop = inputElement.tagName === 'TEXTAREA' ? '5px' : '-10px'; errorContainer.style.marginBottom = '10px'; errorContainer.style.fontSize = '13px'; errorContainer.textContent = validationError; inputElement.parentNode.insertBefore(errorContainer, inputElement.nextSibling); return validationError; } function closeModal(overlayElement, callback) { if (!overlayElement) return; overlayElement.classList.remove('active'); setTimeout(() => { if (overlayElement && overlayElement.parentNode === document.body) document.body.removeChild(overlayElement); if (callback) callback(); }, 300); } function handleAlertConfirm(options, inputElement, descElement, categorySelect, newCategoryInput, overlayElement, resolve) { if (options.input === 'text' && options.inputValidator && inputElement) { const validationError = validateModalInput(inputElement, options.inputValidator, 'mz-modal-input-error'); if (validationError) return; } if (options.descriptionInput === 'textarea' && options.descriptionValidator && descElement) { const descValidationError = validateModalInput(descElement, options.descriptionValidator, 'mz-modal-desc-error'); if (descValidationError) return; } let selectedCategoryId = null; let newCategoryName = null; if (categorySelect) { selectedCategoryId = categorySelect.value; if (selectedCategoryId === NEW_CATEGORY_ID && newCategoryInput) { newCategoryName = newCategoryInput.value.trim(); const categoryErrorElement = document.getElementById('new-category-error'); if (categoryErrorElement) categoryErrorElement.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 = 'Category name already exists.'; errorText.id = 'new-category-error'; newCategoryInput.parentNode.appendChild(errorText); return; } } } closeModal(overlayElement, () => { let result = { isConfirmed: true }; if (options.input === 'text') { result.value = inputElement ? inputElement.value : null; } if (options.descriptionInput === 'textarea') { result.description = descElement ? descElement.value : null; } if (categorySelect) { if (selectedCategoryId === 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[selectedCategoryId] || categories[OTHER_CATEGORY_ID] || { id: OTHER_CATEGORY_ID, name: 'Other', color: '#8395a7' }; } } resolve(result); }); } function handleAlertCancel(overlayElement, resolve) { closeModal(overlayElement, () => { resolve({ isConfirmed: false, value: null, description: null }); }); } function setUpKeyboardHandler(confirmHandler, cancelHandler, inputElement, descElement) { return function(event) { if (event.key === 'Escape') { cancelHandler(); } else if (event.key === 'Enter') { const activeEl = document.activeElement; if (!(activeEl === descElement && descElement?.tagName === 'TEXTAREA') && !(activeEl === inputElement && inputElement?.tagName === 'TEXTAREA')) { confirmHandler(); } else if (activeEl === inputElement && inputElement?.tagName === 'INPUT') { confirmHandler(); } } }; } 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'; if (options.modalClass) container.classList.add(options.modalClass); 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 closeButton = document.createElement('button'); closeButton.id = 'mz-modal-close'; closeButton.innerHTML = DEFAULT_MODAL_STRINGS.close; header.appendChild(closeButton); const content = document.createElement('div'); content.id = 'mz-modal-content'; if (options.htmlContent) { content.appendChild(options.htmlContent); } else if (options.text) { const textNode = document.createTextNode(options.text); content.appendChild(textNode); } let inputElem = null, descElem = null, descLabel = null, categorySelectElem = null, newCategoryInputElem = null, categoryContainer = null; if (options.input === 'text') { inputElem = document.createElement('input'); inputElem.id = 'mz-modal-input'; inputElem.type = 'text'; inputElem.value = options.inputValue || ''; inputElem.placeholder = options.placeholder || ''; } if (options.descriptionInput === 'textarea') { descLabel = document.createElement('label'); descLabel.className = 'mz-modal-label'; descLabel.textContent = options.descriptionLabel || USERSCRIPT_STRINGS.descriptionLabel; descLabel.htmlFor = 'mz-modal-description'; descElem = document.createElement('textarea'); descElem.id = 'mz-modal-description'; descElem.value = options.descriptionValue || ''; descElem.placeholder = options.descriptionPlaceholder || USERSCRIPT_STRINGS.descriptionPlaceholder; descElem.rows = 3; } if (options.showCategorySelector) { 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); categorySelectElem = document.createElement('select'); categorySelectElem.id = 'category-selector'; const usedCategoryIds = new Set(tactics.map(t => t.style).filter(Boolean)); if (options.currentCategory) usedCategoryIds.add(options.currentCategory); const availableCategories = Object.values(categories).filter(cat => DEFAULT_CATEGORIES[cat.id] || 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(cat => { if (cat.id !== OTHER_CATEGORY_ID) { const opt = document.createElement('option'); opt.value = cat.id; opt.textContent = cat.name; categorySelectElem.appendChild(opt); } }); const otherOption = document.createElement('option'); otherOption.value = OTHER_CATEGORY_ID; otherOption.textContent = getCategoryName(OTHER_CATEGORY_ID); categorySelectElem.appendChild(otherOption); const addNewOption = document.createElement('option'); addNewOption.value = NEW_CATEGORY_ID; addNewOption.textContent = '+ New category'; categorySelectElem.appendChild(addNewOption); categorySelectElem.value = (options.currentCategory && categories[options.currentCategory]) ? options.currentCategory : OTHER_CATEGORY_ID; const newCategoryContainer = document.createElement('div'); newCategoryContainer.className = 'new-category-input-container'; newCategoryInputElem = document.createElement('input'); newCategoryInputElem.id = 'new-category-input'; newCategoryInputElem.type = 'text'; newCategoryInputElem.placeholder = 'New category name'; newCategoryContainer.appendChild(newCategoryInputElem); categorySelectElem.addEventListener('change', function() { const isNew = this.value === NEW_CATEGORY_ID; newCategoryContainer.classList.toggle('visible', isNew); if (isNew) newCategoryInputElem.focus(); const categoryError = document.getElementById('new-category-error'); if (categoryError) categoryError.remove(); }); categoryContainer.appendChild(categorySelectElem); categoryContainer.appendChild(newCategoryContainer); } const buttons = document.createElement('div'); buttons.id = 'mz-modal-buttons'; const confirmHandler = () => handleAlertConfirm(options, inputElem, descElem, categorySelectElem, newCategoryInputElem, overlay, resolve); const cancelHandler = () => handleAlertCancel(overlay, resolve); const confirmButton = document.createElement('button'); confirmButton.classList.add('mz-modal-btn', 'primary'); confirmButton.textContent = options.confirmButtonText || DEFAULT_MODAL_STRINGS.ok; confirmButton.addEventListener('click', confirmHandler); buttons.appendChild(confirmButton); if (options.showCancelButton) { const cancelButton = document.createElement('button'); cancelButton.classList.add('mz-modal-btn', 'cancel'); cancelButton.textContent = options.cancelButtonText || DEFAULT_MODAL_STRINGS.cancel; cancelButton.addEventListener('click', cancelHandler); buttons.appendChild(cancelButton); } closeButton.addEventListener('click', cancelHandler); const keyboardHandler = setUpKeyboardHandler(confirmHandler, cancelHandler, inputElem, descElem); document.addEventListener('keydown', keyboardHandler); container.appendChild(header); container.appendChild(content); if (inputElem) { container.appendChild(inputElem); } if (descLabel && descElem) { container.appendChild(descLabel); container.appendChild(descElem); } if (categoryContainer) { container.appendChild(categoryContainer); } container.appendChild(buttons); overlay.appendChild(container); document.body.appendChild(overlay); setTimeout(() => { overlay.classList.add('active'); if (inputElem) inputElem.focus(); else if (descElem) descElem.focus(); if (categorySelectElem && categorySelectElem.value === NEW_CATEGORY_ID) newCategoryInputElem.focus(); }, 10); overlay.addEventListener('transitionend', () => { if (!overlay.classList.contains('active')) document.removeEventListener('keydown', keyboardHandler); }); }); } 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: 'Hello', text: USERSCRIPT_STRINGS.welcomeMessage, confirmButtonText: USERSCRIPT_STRINGS.welcomeGotIt }); } function showLoadingOverlay() { if (!loadingOverlay) { loadingOverlay = document.createElement('div'); loadingOverlay.id = 'loading-overlay'; const spinner = document.createElement('div'); spinner.id = 'loading-spinner'; loadingOverlay.appendChild(spinner); document.body.appendChild(loadingOverlay); } setTimeout(() => loadingOverlay.classList.add('visible'), 10); } function hideLoadingOverlay() { if (loadingOverlay) loadingOverlay.classList.remove('visible'); } function isFootball() { return !!document.querySelector('div#tactics_box.soccer.clearfix'); } function sha256Hash(s) { const shaObj = new jsSHA('SHA-256', 'TEXT'); shaObj.update(s); return shaObj.getHash('HEX'); } function insertAfterElement(newNode, referenceNode) { if (referenceNode && referenceNode.parentNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } else { console.warn("MZTM: Reference node for insertion not found or has no parent."); } } function appendChildren(parent, children) { children.forEach((child) => { if (child) parent.appendChild(child); }); } function getFormattedDate() { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; } async function fetchTacticsFromGMStorage() { return GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); } function storeTacticsInGMStorage(data) { GM_setValue(FORMATIONS_STORAGE_KEY, data); } async function validateDuplicateTactic(id) { const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; return data.tactics.some(t => t.id === id); } async function saveTacticToStorage(tactic) { const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics.push(tactic); await GM_setValue(FORMATIONS_STORAGE_KEY, data); } async function validateDuplicateTacticWithUpdatedCoord(newId, currentTactic, data) { if (newId === currentTactic.id) return 'unchanged'; else if (data.tactics.some(t => t.id === newId)) return 'duplicate'; else return 'unique'; } function loadCompleteTacticsData() { completeTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {}); updateCompleteTacticsDropdown(); } function saveCompleteTacticsData() { GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, completeTactics); } async function fetchTeamIdAndUsername(forceRefresh = false) { const now = Date.now(); const cachedInfo = GM_getValue(USER_INFO_CACHE_KEY); if (!forceRefresh && cachedInfo && cachedInfo.teamId && cachedInfo.username && (now - cachedInfo.timestamp < USER_INFO_CACHE_DURATION_MS)) { teamId = cachedInfo.teamId; username = cachedInfo.username; return { teamId, username }; } try { const usernameElement = document.getElementById('header-username'); if (!usernameElement) throw new Error('No username element found'); const currentUsername = usernameElement.textContent.trim(); const url = `/xml/manager_data.php?sport_id=1&username=${encodeURIComponent(currentUsername)}`; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const xmlString = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const teamElement = xmlDoc.querySelector('Team[sport="soccer"]'); if (!teamElement) throw new Error('No soccer team data found in XML'); const currentTeamId = teamElement.getAttribute('teamId'); if (!currentTeamId) throw new Error('No team ID found in XML'); teamId = currentTeamId; username = currentUsername; const newUserInfo = { teamId: teamId, username: username, timestamp: now }; GM_setValue(USER_INFO_CACHE_KEY, newUserInfo); return { teamId, username }; } catch (error) { console.error('Error fetching Team ID and Username:', error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not fetch team info. Some features might be limited.'); return { teamId: null, username: null }; } } async function fetchTeamRoster(forceRefresh = false) { const now = Date.now(); if (!teamId) { const ids = await fetchTeamIdAndUsername(); if (!ids.teamId) { console.error("MZTM: Cannot fetch roster without Team ID."); return null; } } const cachedRoster = GM_getValue(ROSTER_CACHE_KEY); const isCacheValid = !forceRefresh && cachedRoster && cachedRoster.data && cachedRoster.teamId === teamId && (now - cachedRoster.timestamp < ROSTER_CACHE_DURATION_MS); if (isCacheValid) { return cachedRoster.data; } try { const url = `/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const xmlString = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const playerElements = Array.from(xmlDoc.querySelectorAll('TeamPlayers Player')); const roster = playerElements.map(p => p.getAttribute('id')).filter(id => id); if (roster.length === 0) { console.warn("MZTM: Fetched roster is empty for team", teamId); } rosterCache = { data: roster, timestamp: now, teamId: teamId }; GM_setValue(ROSTER_CACHE_KEY, rosterCache); return roster; } catch (error) { console.error('Error fetching team roster:', error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorFetchingRoster); return null; } } 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 handleTacticSelection(tacticId) { selectedFormationTacticId = tacticId; if (!tacticId) return; const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = tactics.find(td => td.id === tacticId); if (selectedTactic) { if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1) { const hiddenTrigger = document.getElementById('hidden_trigger_button'); if (hiddenTrigger) hiddenTrigger.click(); setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 100); } 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(); updateFormationTextDisplay(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(playerElement) { if (playerElement.classList.contains('fieldpos-collision')) { playerElement.classList.remove('fieldpos-collision'); playerElement.classList.add('fieldpos-ok'); } } function removeTacticSlotInvalidStatus() { const slot = document.querySelector(TACTIC_SLOT_SELECTOR); if (slot) slot.classList.remove('invalid'); } function updateFormationTextDisplay(formation) { const formationTextElement = document.querySelector(FORMATION_TEXT_SELECTOR); if (formationTextElement) { const defs = formationTextElement.querySelector('.defs'), mids = formationTextElement.querySelector('.mids'), 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, midfielders = 0, defenders = 0; for (const coord of coordinates) { const y = coord[1]; if (y < 103) strikers++; else if (y <= 204) midfielders++; else defenders++; } return { strikers, midfielders, defenders }; } function getFormationFromCompleteTactic(tacticData) { let strikers = 0, midfielders = 0, defenders = 0; const outfieldCoords = tacticData.initialCoords.filter(p => p.pos === 'normal'); for (const coord of outfieldCoords) { const y = coord.y; const effectiveY = y - 9; if (effectiveY < 103) strikers++; else if (effectiveY <= 204) midfielders++; else defenders++; } if (strikers + midfielders + defenders !== 10) { console.warn("MZTM: Calculated formation from complete tactic doesn't sum to 10 outfield players."); } return { strikers, midfielders, defenders }; } function formatFormationString(formationObj) { if (!formationObj || typeof formationObj.defenders === 'undefined') return 'N/A'; return `${formationObj.defenders}-${formationObj.midfielders}-${formationObj.strikers}`; } function validateTacticPlayerCount(outfieldPlayers) { const isGoalkeeperPresent = document.querySelector(GOALKEEPER_SELECTOR); outfieldPlayers = outfieldPlayers.filter(p => !p.classList.contains('fieldpos-collision')); if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1 || !isGoalkeeperPresent) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError); return false; } return true; } function generateCategoryId(name) { return sha256Hash(name.toLowerCase()).substring(0, 10); } function generateCategoryColor(name) { const hash = sha256Hash(name); 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: '#8395a7' }; } } 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_CATEGORY_ID || !categoryId) return '#8395a7'; 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'; } async function removeCategory(categoryId, sourceModalElement = null) { if (!categoryId || categoryId === 'all' || categoryId === OTHER_CATEGORY_ID || DEFAULT_CATEGORIES[categoryId]) { console.error("Cannot remove this category:", categoryId); return false; } const categoryName = getCategoryName(categoryId); const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.removeCategoryConfirmation.replace('{}', categoryName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.removeCategoryButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return false; const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); let updated = false; data.tactics = data.tactics.map(t => { if (t.style === categoryId) { t.style = OTHER_CATEGORY_ID; updated = true; } return t; }); tactics = tactics.map(t => { if (t.style === categoryId) t.style = OTHER_CATEGORY_ID; return t; }); if (updated) await GM_setValue(FORMATIONS_STORAGE_KEY, data); delete categories[categoryId]; saveCategories(); if (currentFilter === categoryId) currentFilter = 'all'; updateTacticsDropdown(); updateCategoryFilterDropdown(); if (sourceModalElement) { const categoryItem = sourceModalElement.querySelector(`li[data-category-id="${categoryId}"]`); if (categoryItem) { categoryItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; categoryItem.style.opacity = '0'; categoryItem.style.transform = 'translateX(-20px)'; setTimeout(() => categoryItem.remove(), 300); } } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.removeCategoryAlert.replace('{}', categoryName)); return true; } async function showManagementModal() { const modalContent = createManagementModalContent(); await showAlert({ title: USERSCRIPT_STRINGS.managementModalTitle, htmlContent: modalContent, confirmButtonText: USERSCRIPT_STRINGS.manageCategoriesDoneButton, showCancelButton: false, modalClass: 'management-modal' }); updateCategoryFilterDropdown(); updateTacticsDropdown(); } function createManagementModalContent() { const wrapper = document.createElement('div'); wrapper.className = 'management-modal-wrapper'; const tabsConfig = [ { id: 'formations', title: USERSCRIPT_STRINGS.formationsTabTitle, contentGenerator: createFormationsManagementTab }, { id: 'categories', title: USERSCRIPT_STRINGS.categoriesTabTitle, contentGenerator: createCategoriesManagementTab } ]; const tabsContainer = createModalTabs(tabsConfig, wrapper); wrapper.appendChild(tabsContainer); tabsConfig.forEach((tab, index) => { const contentDiv = document.createElement('div'); contentDiv.className = 'management-modal-content'; contentDiv.dataset.tabId = tab.id; if (index === 0) contentDiv.classList.add('active'); tab.contentGenerator(contentDiv); wrapper.appendChild(contentDiv); }); wrapper.addEventListener('click', handleManagementModalClick); wrapper.addEventListener('change', handleManagementModalChange); wrapper.addEventListener('keydown', handleManagementModalKeydown); return wrapper; } function createFormationsManagementTab(container) { container.innerHTML = ''; const list = document.createElement('ul'); list.className = 'formation-management-list'; const sortedTactics = [...tactics].sort((a, b) => a.name.localeCompare(b.name)); if (sortedTactics.length === 0) { const message = document.createElement('p'); message.textContent = 'No formations saved yet.'; message.className = 'no-items-message'; list.appendChild(message); } else { sortedTactics.forEach(tactic => { list.appendChild(createFormationManagementItem(tactic)); }); } container.appendChild(list); } function createFormationManagementItem(tactic) { const listItem = document.createElement('li'); listItem.dataset.tacticId = tactic.id; const nameContainer = document.createElement('div'); nameContainer.className = 'item-name-container'; const nameSpan = document.createElement('span'); nameSpan.className = 'item-name'; nameSpan.textContent = tactic.name; nameContainer.appendChild(nameSpan); const controlsContainer = document.createElement('div'); controlsContainer.className = 'item-controls'; const categorySelect = document.createElement('select'); categorySelect.className = 'item-category-select'; populateCategorySelect(categorySelect, tactic.style); const editBtn = document.createElement('button'); editBtn.className = 'item-action-btn edit-name-btn'; editBtn.innerHTML = '✏️'; editBtn.title = 'Edit name & description'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'item-action-btn delete-item-btn'; deleteBtn.innerHTML = '🗑️'; deleteBtn.title = 'Delete formation'; appendChildren(controlsContainer, [categorySelect, editBtn, deleteBtn]); appendChildren(listItem, [nameContainer, controlsContainer]); return listItem; } function populateCategorySelect(selectElement, currentCategoryId) { selectElement.innerHTML = ''; const availableCategories = Object.values(categories) .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(cat => { const option = document.createElement('option'); option.value = cat.id; option.textContent = cat.name; if (cat.id === (currentCategoryId || OTHER_CATEGORY_ID)) { option.selected = true; } selectElement.appendChild(option); }); } function createCategoriesManagementTab(container) { container.innerHTML = ''; const addCategorySection = document.createElement('div'); addCategorySection.className = 'add-category-section'; const newCategoryInput = document.createElement('input'); newCategoryInput.type = 'text'; newCategoryInput.placeholder = USERSCRIPT_STRINGS.addCategoryPlaceholder; newCategoryInput.className = 'add-category-input'; const addCategoryBtn = document.createElement('button'); addCategoryBtn.className = 'mz-modal-btn add-category-btn'; addCategoryBtn.textContent = USERSCRIPT_STRINGS.addCategoryButton; appendChildren(addCategorySection, [newCategoryInput, addCategoryBtn]); container.appendChild(addCategorySection); const list = document.createElement('ul'); list.className = 'category-management-list'; const customCategories = Object.values(categories) .filter(cat => cat.id !== OTHER_CATEGORY_ID && !DEFAULT_CATEGORIES[cat.id]) .sort((a, b) => a.name.localeCompare(b.name)); const noCatMsg = document.createElement('p'); noCatMsg.textContent = USERSCRIPT_STRINGS.noCustomCategories; noCatMsg.className = 'no-custom-categories-message'; noCatMsg.style.display = customCategories.length === 0 ? 'block' : 'none'; list.appendChild(noCatMsg); customCategories.forEach(cat => { list.appendChild(createCategoryManagementItem(cat)); }); container.appendChild(list); } function createCategoryManagementItem(category) { const listItem = document.createElement('li'); listItem.dataset.categoryId = category.id; const nameSpan = document.createElement('span'); nameSpan.textContent = category.name; nameSpan.style.flexGrow = '1'; nameSpan.style.marginRight = '10px'; const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.className = 'mz-modal-btn category-remove-btn'; removeBtn.title = `Remove category "${category.name}"`; listItem.appendChild(nameSpan); listItem.appendChild(removeBtn); return listItem; } function handleManagementModalClick(event) { const target = event.target; const listItem = target.closest('li'); if (target.classList.contains('edit-name-btn') && listItem) { handleEditFormationInModal(listItem); } else if (target.classList.contains('delete-item-btn') && listItem) { handleDeleteFormationInModal(listItem); } else if (target.classList.contains('add-category-btn')) { handleAddNewCategoryInModal(target.closest('.add-category-section')); } else if (target.classList.contains('category-remove-btn') && listItem) { handleDeleteCategoryInModal(listItem); } } function handleManagementModalChange(event) { const target = event.target; if (target.classList.contains('item-category-select')) { const listItem = target.closest('li'); const tacticId = listItem?.dataset.tacticId; const newCategoryId = target.value; if (tacticId && newCategoryId) { updateFormationCategory(tacticId, newCategoryId); } } } function handleManagementModalKeydown(event) { if (event.key === 'Enter' && !event.shiftKey) { const target = event.target; if (target.classList.contains('add-category-input')) { handleAddNewCategoryInModal(target.closest('.add-category-section')); event.preventDefault(); } } } async function handleEditFormationInModal(listItem) { const tacticId = listItem.dataset.tacticId; const tactic = tactics.find(t => t.id === tacticId); if (!tactic) return; await editTactic(tactic.id, listItem); } async function handleDeleteFormationInModal(listItem) { const tacticId = listItem.dataset.tacticId; const tactic = tactics.find(t => t.id === tacticId); if (!tactic) return; const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', tactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; const deletedCategoryId = tactic.style; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics = data.tactics.filter(t => t.id !== tacticId); await GM_setValue(FORMATIONS_STORAGE_KEY, data); tactics = tactics.filter(t => t.id !== tacticId); listItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; listItem.style.opacity = '0'; listItem.style.transform = 'translateX(-20px)'; setTimeout(() => { listItem.remove(); const list = document.querySelector('.formation-management-list'); if (list && !list.querySelector('li')) { const message = document.createElement('p'); message.textContent = 'No formations saved yet.'; message.className = 'no-items-message'; list.appendChild(message); } }, 300); const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId); if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) { delete categories[deletedCategoryId]; saveCategories(); if (currentFilter === deletedCategoryId) currentFilter = 'all'; const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]'); if (catTab) createCategoriesManagementTab(catTab); } updateTacticsDropdown(); updateCategoryFilterDropdown(); } async function updateFormationCategory(tacticId, newCategoryId) { const tacticIndex = tactics.findIndex(t => t.id === tacticId); if (tacticIndex === -1) return; const originalCategoryId = tactics[tacticIndex].style; if (originalCategoryId === newCategoryId) return; tactics[tacticIndex].style = newCategoryId; const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); const dataIndex = data.tactics.findIndex(t => t.id === tacticId); if (dataIndex !== -1) { data.tactics[dataIndex].style = newCategoryId; await GM_setValue(FORMATIONS_STORAGE_KEY, data); const originalCategoryStillUsed = tactics.some(t => t.style === originalCategoryId); if (!originalCategoryStillUsed && originalCategoryId && !DEFAULT_CATEGORIES[originalCategoryId] && originalCategoryId !== OTHER_CATEGORY_ID) { delete categories[originalCategoryId]; saveCategories(); if (currentFilter === originalCategoryId) currentFilter = 'all'; const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]'); if (catTab) createCategoriesManagementTab(catTab); } updateTacticsDropdown(); updateCategoryFilterDropdown(); } else { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update category in storage."); tactics[tacticIndex].style = originalCategoryId; const selectElement = document.querySelector(`li[data-tactic-id="${tacticId}"] .item-category-select`); if (selectElement) selectElement.value = originalCategoryId || OTHER_CATEGORY_ID; } } async function handleAddNewCategoryInModal(addSection) { const input = addSection.querySelector('.add-category-input'); const list = addSection.nextElementSibling; if (!input || !list) return; const name = input.value.trim(); if (!name) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticNameProvidedError.replace("name", "category name")); input.focus(); return; } if (name.length > MAX_CATEGORY_NAME_LENGTH) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.categoryNameMaxLengthError); input.focus(); return; } const existingCategory = Object.values(categories).find(cat => cat.name.toLowerCase() === name.toLowerCase()); if (existingCategory) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Category name already exists."); input.focus(); return; } const newCategoryId = generateCategoryId(name); const newCategory = { id: newCategoryId, name: name, color: generateCategoryColor(name) }; addCategory(newCategory); input.value = ''; const noCatMsg = list.querySelector('.no-custom-categories-message'); if (noCatMsg) noCatMsg.style.display = 'none'; const newItem = createCategoryManagementItem(newCategory); list.appendChild(newItem); newItem.style.opacity = '0'; newItem.style.transform = 'translateY(-10px)'; setTimeout(() => { newItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; newItem.style.opacity = '1'; newItem.style.transform = 'translateY(0)'; }, 10); updateCategoryFilterDropdown(); document.querySelectorAll('.item-category-select').forEach(select => { const currentTacticId = select.closest('li')?.dataset.tacticId; const currentTactic = tactics.find(t => t.id === currentTacticId); populateCategorySelect(select, currentTactic?.style); }); } async function handleDeleteCategoryInModal(listItem) { const categoryId = listItem.dataset.categoryId; const categoryName = getCategoryName(categoryId); if (!categoryId || !categoryName) return; const success = await removeCategory(categoryId, listItem.closest('.management-modal-content')); if (success) { document.querySelectorAll('.item-category-select').forEach(select => { const currentTacticId = select.closest('li')?.dataset.tacticId; const currentTactic = tactics.find(t => t.id === currentTacticId); populateCategorySelect(select, currentTactic?.style); }); const formationsTab = document.querySelector('.management-modal-content[data-tab-id="formations"]'); if (formationsTab) createFormationsManagementTab(formationsTab); } } async function addNewTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const coordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]); if (!validateTacticPlayerCount(outfieldPlayers)) return; const id = generateUniqueId(coordinates); const isDuplicate = await validateDuplicateTactic(id); 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: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; const name = result.value; const description = result.description || ''; const categoryId = result.category.id; const tactic = { name: name, description: description, coordinates: coordinates, id: id, style: categoryId }; await saveTacticToStorage(tactic); tactics.push(tactic); tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(id); updateCategoryFilterDropdown(); handleTacticSelection(tactic.id); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', tactic.name)); } async function addNewTacticWithXml() { const xmlResult = await showAlert({ title: USERSCRIPT_STRINGS.xmlPlaceholder, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.xmlPlaceholder, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!xmlResult.isConfirmed) return; const xml = xmlResult.value; if (!xml) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlRequiredError); return; } if (!xml.trim().startsWith('<') || !xml.trim().endsWith('>')) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidXmlFormatError); return; } const nameResult = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!nameResult.isConfirmed || !nameResult.value) return; const name = nameResult.value; const description = nameResult.description || ''; const categoryId = nameResult.category.id; try { const newTactic = await convertXmlToSimpleFormationJson(xml, name); newTactic.style = categoryId; newTactic.description = description; const id = generateUniqueId(newTactic.coordinates); const isDuplicate = await validateDuplicateTactic(id); if (isDuplicate) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticError); return; } newTactic.id = id; await saveTacticToStorage(newTactic); tactics.push(newTactic); tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(id); updateCategoryFilterDropdown(); handleTacticSelection(newTactic.id); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', newTactic.name)); } catch (error) { console.error('XMLError:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError + (error.message ? `: ${error.message}` : '')); } } async function deleteTactic() { const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedTactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; const deletedCategoryId = selectedTactic.style; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics = data.tactics.filter(t => t.id !== selectedTactic.id); await GM_setValue(FORMATIONS_STORAGE_KEY, data); tactics = tactics.filter(t => t.id !== selectedTactic.id); selectedFormationTacticId = null; const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId); if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) { delete categories[deletedCategoryId]; saveCategories(); if (currentFilter === deletedCategoryId) currentFilter = 'all'; } updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace('{}', selectedTactic.name)); } async function editTactic(tacticIdToEdit = null, sourceListItem = null) { const idToUse = tacticIdToEdit || selectedFormationTacticId; const selectedTactic = tactics.find(t => t.id === idToUse); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const originalName = selectedTactic.name; const originalCategory = selectedTactic.style; const originalDescription = selectedTactic.description || ''; const result = await showAlert({ title: 'Edit Formation', input: 'text', inputValue: originalName, placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (v !== originalName && tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: originalDescription, descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, currentCategory: selectedTactic.style, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) return; const newName = result.value || originalName; const newDescription = result.description || ''; const newCategory = result.category?.id || originalCategory; if (newName === originalName && newCategory === originalCategory && newDescription === originalDescription) { return; } const categoryChanged = originalCategory !== newCategory; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; let updatedInStorage = false; data.tactics = data.tactics.map(t => { if (t.id === selectedTactic.id) { t.name = newName; t.style = newCategory; t.description = newDescription; updatedInStorage = true; } return t; }); if (!updatedInStorage) { console.error("MZTM: Failed to find tactic in storage for update.", selectedTactic.id); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update tactic in storage."); return; } await GM_setValue(FORMATIONS_STORAGE_KEY, data); const tacticIndex = tactics.findIndex(t => t.id === selectedTactic.id); if (tacticIndex !== -1) { tactics[tacticIndex].name = newName; tactics[tacticIndex].style = newCategory; tactics[tacticIndex].description = newDescription; } else { console.error("MZTM: Failed to find tactic in memory for update.", selectedTactic.id); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Internal error updating tactic data."); await initializeScriptData(); updateTacticsDropdown(); updateCategoryFilterDropdown(); return; } if (categoryChanged) { const originalCategoryStillUsed = tactics.some(t => t.style === originalCategory); if (!originalCategoryStillUsed && originalCategory && !DEFAULT_CATEGORIES[originalCategory] && originalCategory !== OTHER_CATEGORY_ID) { delete categories[originalCategory]; saveCategories(); if (currentFilter === originalCategory) currentFilter = 'all'; } } tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(selectedTactic.id); updateCategoryFilterDropdown(); if (sourceListItem) { const nameSpan = sourceListItem.querySelector('.item-name'); if (nameSpan) nameSpan.textContent = newName; const categorySelect = sourceListItem.querySelector('.item-category-select'); if (categorySelect) categorySelect.value = newCategory; } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace('{}', newName)); } async function updateTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } if (!validateTacticPlayerCount(outfieldPlayers)) return; const updatedCoordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]); const newId = generateUniqueId(updatedCoordinates); const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; const validationOutcome = await validateDuplicateTacticWithUpdatedCoord(newId, selectedTactic, data); 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 confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.updateConfirmation.replace('{}', selectedTactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; for (const tactic of data.tactics) { if (tactic.id === selectedTactic.id) { tactic.coordinates = updatedCoordinates; tactic.id = newId; } } const memoryTactic = tactics.find(t => t.id === selectedTactic.id); if (memoryTactic) { memoryTactic.coordinates = updatedCoordinates; memoryTactic.id = newId; } await GM_setValue(FORMATIONS_STORAGE_KEY, data); selectedFormationTacticId = newId; updateTacticsDropdown(newId); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.updateAlert.replace('{}', selectedTactic.name)); } async function clearTactics() { const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.clearConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; await GM_deleteValue(FORMATIONS_STORAGE_KEY); await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); tactics = []; selectedFormationTacticId = null; currentFilter = 'all'; loadCategories(); updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert); } async function resetTactics() { const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.resetConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; await GM_deleteValue(FORMATIONS_STORAGE_KEY); await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); await GM_deleteValue(CATEGORIES_STORAGE_KEY); tactics = []; selectedFormationTacticId = null; currentFilter = 'all'; loadCategories(); updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert); } async function importTacticsJsonData() { try { const result = await showAlert({ title: 'Import Formations (JSON)', input: 'text', inputValue: '', placeholder: 'Paste Formations 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) || !importedData.tactics.every(t => t.name && t.id && Array.isArray(t.coordinates))) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError); return; } const importedTactics = importedData.tactics; importedTactics.forEach(t => { if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID; if (!t.hasOwnProperty('description')) t.description = ''; if (t.style && !categories[t.style] && !DEFAULT_CATEGORIES[t.style] && t.style !== OTHER_CATEGORY_ID) { addCategory({ id: t.style, name: t.style, color: generateCategoryColor(t.style) }); } }); let existingData = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); let existingTactics = existingData.tactics || []; const mergedTactics = [...existingTactics]; let addedCount = 0; for (const impTactic of importedTactics) { if (!existingTactics.some(t => t.id === impTactic.id)) { mergedTactics.push(impTactic); addedCount++; } else { const existingIndex = mergedTactics.findIndex(t => t.id === impTactic.id); if (existingIndex !== -1) { mergedTactics[existingIndex] = { ...mergedTactics[existingIndex], ...impTactic }; } } } await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: mergedTactics }); mergedTactics.sort((a, b) => a.name.localeCompare(b.name)); tactics = mergedTactics; updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.importAlert + (addedCount > 0 ? ` (${addedCount} new items added)` : '')); } catch (error) { console.error('ImportError:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError + (error.message ? `: ${error.message}` : '')); } } async function exportTacticsJsonData() { try { const data = GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); const jsonString = JSON.stringify(data, null, 2); if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(jsonString); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportAlert); return; } catch (clipError) { console.warn('Clipboard write failed, fallback.', clipError); } } const textArea = document.createElement('textarea'); textArea.value = jsonString; 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:')); container.appendChild(textArea); await showAlert({ title: 'Export Formations (JSON)', 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 formations.'); } } async function convertXmlToSimpleFormationJson(xmlString, tacticName) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const parseErrors = xmlDoc.getElementsByTagName('parsererror'); if (parseErrors.length > 0) throw new Error(USERSCRIPT_STRINGS.xmlValidationError); const positionElements = Array.from(xmlDoc.getElementsByTagName('Pos')).filter(el => el.getAttribute('pos') === 'normal'); if (positionElements.length !== MIN_PLAYERS_ON_PITCH - 1) throw new Error(`XML must contain exactly ${MIN_PLAYERS_ON_PITCH - 1} outfield players. Found ${positionElements.length}.`); const coordinates = positionElements.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.'); return [x - 7, y - 9]; }); return { name: tacticName, coordinates: coordinates }; } function getAttr(element, attributeName, defaultValue = null) { return element ? element.getAttribute(attributeName) || defaultValue : defaultValue; } function parseCompleteTacticXml(xmlString) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); if (xmlDoc.getElementsByTagName("parsererror").length > 0) throw new Error("XML parsing error"); const soccerTactics = xmlDoc.querySelector("SoccerTactics"); if (!soccerTactics) throw new Error("Missing "); const teamElement = soccerTactics.querySelector("Team"); const posElements = Array.from(soccerTactics.querySelectorAll("Pos")); const subElements = Array.from(soccerTactics.querySelectorAll("Sub")); const ruleElements = Array.from(soccerTactics.querySelectorAll("TacticRule")); const data = { initialCoords: [], alt1Coords: [], alt2Coords: [], teamSettings: {}, substitutes: [], tacticRules: [], originalPlayerIDs: new Set(), description: '' }; data.teamSettings = { passingStyle: getAttr(teamElement, 'tactics', 'shortpass'), mentality: getAttr(teamElement, 'playstyle', 'normal'), aggression: getAttr(teamElement, 'aggression', 'normal'), captainPID: getAttr(teamElement, 'captain', '0') }; if (data.teamSettings.captainPID !== '0') data.originalPlayerIDs.add(data.teamSettings.captainPID); posElements.forEach(el => { const pid = getAttr(el, 'pid'); const posType = getAttr(el, 'pos'); if (!pid) return; data.originalPlayerIDs.add(pid); if (posType === 'normal' || posType === 'goalie') { const x = parseInt(getAttr(el, 'x', 0)); const y = parseInt(getAttr(el, 'y', 0)); const x1 = parseInt(getAttr(el, 'x1', x)); const y1 = parseInt(getAttr(el, 'y1', y)); const x2 = parseInt(getAttr(el, 'x2', x1)); const y2 = parseInt(getAttr(el, 'y2', y1)); data.initialCoords.push({ pid: pid, pos: posType, x: x, y: y }); data.alt1Coords.push({ pid: pid, pos: posType, x: x1, y: y1 }); data.alt2Coords.push({ pid: pid, pos: posType, x: x2, y: y2 }); } }); subElements.forEach(el => { const pid = getAttr(el, 'pid'); const posType = getAttr(el, 'pos'); const x = parseInt(getAttr(el, 'x', 0)); const y = parseInt(getAttr(el, 'y', 0)); if (pid) { data.originalPlayerIDs.add(pid); data.substitutes.push({ pid: pid, pos: posType, x: x, y: y }); } }); ruleElements.forEach(el => { const rule = {}; for (const attr of el.attributes) { rule[attr.name] = attr.value; if (attr.name === 'out_player' && attr.value !== 'no_change' && attr.value !== '0') data.originalPlayerIDs.add(attr.value); if (attr.name === 'in_player_id' && attr.value !== 'NULL' && attr.value !== '0') data.originalPlayerIDs.add(attr.value); } data.tacticRules.push(rule); }); data.originalPlayerIDs = Array.from(data.originalPlayerIDs); return data; } function generateCompleteTacticXml(tacticData, playerMapping) { let xml = `\n\n`; const mappedCaptain = playerMapping[tacticData.teamSettings.captainPID] || '0'; xml += `\t\n`; const playerCoords = {}; tacticData.initialCoords.forEach(p => { if (!playerCoords[p.pid]) { playerCoords[p.pid] = { pos: p.pos }; playerCoords[p.pid].initial = { x: p.x, y: p.y }; } }); tacticData.alt1Coords.forEach(p => { if (!playerCoords[p.pid]) return; playerCoords[p.pid].alt1 = { x: p.x, y: p.y }; }); tacticData.alt2Coords.forEach(p => { if (!playerCoords[p.pid]) return; playerCoords[p.pid].alt2 = { x: p.x, y: p.y }; }); for (const originalPid in playerCoords) { const mappedPid = playerMapping[originalPid]; if (!mappedPid) continue; const playerData = playerCoords[originalPid]; const initial = playerData.initial || { x: 0, y: 0 }; const alt1 = playerData.alt1 || initial; const alt2 = playerData.alt2 || alt1; xml += `\t\n`; } tacticData.substitutes.forEach(s => { const mappedPid = playerMapping[s.pid]; if (mappedPid) xml += `\t\n`; }); tacticData.tacticRules.forEach(rule => { const mappedOutPlayer = (rule.out_player && rule.out_player !== 'no_change') ? (playerMapping[rule.out_player] || 'no_change') : 'no_change'; const mappedInPlayer = (rule.in_player_id && rule.in_player_id !== 'NULL') ? (playerMapping[rule.in_player_id] || 'NULL') : 'NULL'; let includeRule = true; if (rule.out_player && rule.out_player !== 'no_change' && mappedOutPlayer === 'no_change') includeRule = false; if (rule.in_player_id && rule.in_player_id !== 'NULL' && mappedInPlayer === 'NULL') includeRule = false; if (includeRule) { xml += '\t setTimeout(r, 200)); const xmlString = importExportData.value; if (!xmlString) { if (windowHidden) document.getElementById('close_button')?.click(); return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Export did not produce XML.'); } let savedData; try { savedData = parseCompleteTacticXml(xmlString); } catch (error) { console.error("XML Parse Error:", error); if (windowHidden) document.getElementById('close_button')?.click(); return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorXmlExportParse); } const result = await showAlert({ title: USERSCRIPT_STRINGS.completeTacticNamePrompt, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, }); if (!result.isConfirmed || !result.value) { if (windowHidden) document.getElementById('close_button')?.click(); return; } const baseName = result.value; const description = result.description || ''; const fullName = `${baseName} (${getFormattedDate()})`; savedData.description = description; completeTactics[fullName] = savedData; saveCompleteTacticsData(); updateCompleteTacticsDropdown(fullName); if (windowHidden) document.getElementById('close_button')?.click(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticSaveSuccess.replace('{}', fullName)); } async function loadCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); showLoadingOverlay(); const originalAlert = window.alert; try { const dataToLoad = completeTactics[selectedName]; const currentRoster = await fetchTeamRoster(); if (!currentRoster) throw new Error(USERSCRIPT_STRINGS.errorFetchingRoster); const rosterSet = new Set(currentRoster); const originalPids = dataToLoad.originalPlayerIDs || []; const mapping = {}; const missingPids = []; const mappedPids = new Set(); originalPids.forEach(pid => { if (rosterSet.has(pid)) { mapping[pid] = pid; mappedPids.add(pid); } else { missingPids.push(pid); } }); const availablePids = currentRoster.filter(pid => !mappedPids.has(pid)); let replacementsFound = 0; missingPids.forEach(missingPid => { if (availablePids.length > 0) { const randomIndex = Math.floor(Math.random() * availablePids.length); const replacementPid = availablePids.splice(randomIndex, 1)[0]; mapping[missingPid] = replacementPid; replacementsFound++; } else { mapping[missingPid] = null; } }); const assignedPids = new Set(); dataToLoad.initialCoords.forEach(p => { if (mapping[p.pid]) assignedPids.add(mapping[p.pid]); }); dataToLoad.substitutes.forEach(s => { if (mapping[s.pid]) assignedPids.add(mapping[s.pid]); }); if (assignedPids.size < MIN_PLAYERS_ON_PITCH) throw new Error(USERSCRIPT_STRINGS.errorInsufficientPlayers); let xmlString; try { xmlString = generateCompleteTacticXml(dataToLoad, mapping); } catch (error) { console.error("XML Gen Error:", error); throw new Error(USERSCRIPT_STRINGS.errorXmlGenerate); } let alertContent = null; window.alert = (msg) => { console.warn("Native alert captured:", msg); alertContent = msg; }; const importButton = document.getElementById('import_button'); const importExportWindow = document.getElementById('importExportTacticsWindow'); const importExportData = document.getElementById('importExportData'); if (!importButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for import.'); const windowHidden = importExportWindow.style.display === 'none'; if (windowHidden) { document.getElementById('import_export_button')?.click(); await new Promise(r => setTimeout(r, 50)); } importExportData.value = xmlString; importButton.click(); await new Promise(r => setTimeout(r, 300)); window.alert = originalAlert; if (alertContent) throw new Error(USERSCRIPT_STRINGS.invalidXmlForImport + (alertContent.length < 100 ? ` MZ Message: ${alertContent}` : '')); const observer = new MutationObserver((mutationsList, obs) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const errorBox = document.getElementById('lightbox_tactics_rule_error'); if (errorBox && errorBox.style.display !== 'none') { const okButton = errorBox.querySelector('#powerbox_confirm_ok_button'); if (okButton) { okButton.click(); obs.disconnect(); break; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 3000); if (replacementsFound > 0) { showAlert({ title: 'Warning', text: USERSCRIPT_STRINGS.warningPlayersSubstituted, type: 'info' }); } else showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticLoadSuccess.replace('{}', selectedName)); } catch (error) { console.error("Load Complete Tactic Error:", error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during load.'); if (window.alert !== originalAlert) window.alert = originalAlert; } finally { hideLoadingOverlay(); } } async function deleteCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; delete completeTactics[selectedName]; selectedCompleteTacticName = null; saveCompleteTacticsData(); updateCompleteTacticsDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticDeleteSuccess.replace('{}', selectedName)); } async function editCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) { return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); } const originalTacticData = completeTactics[selectedName]; const originalDescription = originalTacticData.description || ''; const result = await showAlert({ title: USERSCRIPT_STRINGS.renameCompleteTacticPrompt, input: 'text', inputValue: selectedName, placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (v !== selectedName && completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: originalDescription, descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; const newName = result.value; const newDescription = result.description || ''; if (newName === selectedName && newDescription === originalDescription) return; const tacticData = completeTactics[selectedName]; tacticData.description = newDescription; delete completeTactics[selectedName]; completeTactics[newName] = tacticData; saveCompleteTacticsData(); selectedCompleteTacticName = newName; updateCompleteTacticsDropdown(newName); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticRenameSuccess.replace('{}', newName)); } async function updateCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) { return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); } const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.updateCompleteTacticConfirmation.replace('{}', selectedName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; const originalAlert = window.alert; try { const exportButton = document.getElementById('export_button'); const importExportWindow = document.getElementById('importExportTacticsWindow'); const importExportData = document.getElementById('importExportData'); if (!exportButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for export.'); const windowHidden = importExportWindow.style.display === 'none'; if (windowHidden) { document.getElementById('import_export_button')?.click(); await new Promise(r => setTimeout(r, 50)); } importExportData.value = ''; exportButton.click(); await new Promise(r => setTimeout(r, 200)); const xmlString = importExportData.value; if (windowHidden) document.getElementById('close_button')?.click(); if (!xmlString) throw new Error('Export did not produce XML.'); let updatedData; try { updatedData = parseCompleteTacticXml(xmlString); } catch (error) { console.error("XML Parse Error on Update:", error); throw new Error(USERSCRIPT_STRINGS.errorXmlExportParse); } updatedData.description = completeTactics[selectedName]?.description || ''; completeTactics[selectedName] = updatedData; saveCompleteTacticsData(); updateCompleteTacticsDropdown(selectedName); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticUpdateSuccess.replace('{}', selectedName)); } catch (error) { console.error("Update Complete Tactic Error:", error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during update.'); if (window.alert !== originalAlert) window.alert = originalAlert; const importExportWindow = document.getElementById('importExportTacticsWindow'); if (importExportWindow && importExportWindow.style.display !== 'none'){ document.getElementById('close_button')?.click(); } } } async function importCompleteTactics() { try { const result = await showAlert({ title: USERSCRIPT_STRINGS.importCompleteTacticsTitle, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.importCompleteTacticsPlaceholder, 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.invalidCompleteImportError); return; } if (typeof importedData !== 'object' || importedData === null || Array.isArray(importedData)) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError); return; } let addedCount = 0; let updatedCount = 0; for (const name in importedData) { if (importedData.hasOwnProperty(name)) { if (typeof importedData[name] === 'object' && importedData[name] !== null) { if (!importedData[name].hasOwnProperty('description')) importedData[name].description = ''; if (!completeTactics.hasOwnProperty(name)) { addedCount++; } else { updatedCount++; } completeTactics[name] = importedData[name]; } else { console.warn(`MZTM: Skipping invalid tactic data during import for key: ${name}`); } } } saveCompleteTacticsData(); updateCompleteTacticsDropdown(); let message = USERSCRIPT_STRINGS.importCompleteTacticsAlert; if(addedCount > 0 || updatedCount > 0) { message += ` (${addedCount > 0 ? `${addedCount} new` : ''}${addedCount > 0 && updatedCount > 0 ? ', ' : ''}${updatedCount > 0 ? `${updatedCount} updated` : ''} items)`; } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, message); } catch (error) { console.error('Import Complete Tactics Error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError + (error.message ? `: ${error.message}` : '')); } } async function exportCompleteTactics() { try { const jsonString = JSON.stringify(completeTactics, null, 2); if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(jsonString); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportCompleteTacticsAlert); return; } catch (clipError) { console.warn('Clipboard write failed, fallback.', clipError); } } const textArea = document.createElement('textarea'); textArea.value = jsonString; 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:')); container.appendChild(textArea); await showAlert({ title: USERSCRIPT_STRINGS.exportCompleteTacticsTitle, htmlContent: container, confirmButtonText: 'Done' }); textArea.select(); textArea.setSelectionRange(0, 99999); } catch (error) { console.error('Export Complete Tactics error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export tactics.'); } } function createTacticPreviewElement() { if (previewElement) return previewElement; previewElement = document.createElement('div'); previewElement.id = 'mztm-tactic-preview'; previewElement.style.display = 'none'; previewElement.style.opacity = '0'; previewElement.addEventListener('mouseenter', () => { if(previewHideTimeout) clearTimeout(previewHideTimeout); }); previewElement.addEventListener('mouseleave', hideTacticPreview); document.body.appendChild(previewElement); return previewElement; } function updatePreviewPosition(event) { if (!previewElement || previewElement.style.display === 'none') return; const xOffset = 15; const yOffset = 10; let x = event.clientX + xOffset; let y = event.clientY + yOffset; const previewRect = previewElement.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (x + previewRect.width > viewportWidth - 10) { x = event.clientX - previewRect.width - xOffset; } if (y + previewRect.height > viewportHeight - 10) { y = event.clientY - previewRect.height - yOffset; } if (x < 10) x = 10; if (y < 10) y = 10; previewElement.style.left = `${x}px`; previewElement.style.top = `${y}px`; } function showTacticPreview(event, listItem) { if (!listItem || listItem.classList.contains('mztm-custom-select-category') || listItem.classList.contains('mztm-custom-select-no-results')) { hideTacticPreview(); return; } if(previewHideTimeout) clearTimeout(previewHideTimeout); const tacticId = listItem.dataset.tacticId; const tacticName = listItem.dataset.tacticName; const description = listItem.dataset.description || ''; const formationString = listItem.dataset.formationString || 'N/A'; const isCompleteTactic = listItem.closest('#complete_tactics_selector_list'); if (!tacticId && !tacticName) { hideTacticPreview(); return; } const previewDiv = createTacticPreviewElement(); previewDiv.innerHTML = `
${USERSCRIPT_STRINGS.previewFormationLabel} ${formationString}
${description ? `
${description.replace(/\n/g, '
')}
` : '
No description available.
'} `; previewDiv.style.display = 'block'; requestAnimationFrame(() => { updatePreviewPosition(event); previewDiv.style.opacity = '1'; }); document.addEventListener('mousemove', updatePreviewPosition); } function hideTacticPreview() { if(previewHideTimeout) clearTimeout(previewHideTimeout); previewHideTimeout = setTimeout(() => { if (previewElement) { previewElement.style.opacity = '0'; setTimeout(() => { if (previewElement && previewElement.style.opacity === '0') { previewElement.style.display = 'none'; } }, 200); document.removeEventListener('mousemove', updatePreviewPosition); } previewHideTimeout = null; }, 100); } function addPreviewListenersToList(listElement) { if (!listElement) return; listElement.addEventListener('mouseover', (event) => { const listItem = event.target.closest('.mztm-custom-select-item'); if (listItem && !listItem.classList.contains('disabled')) { showTacticPreview(event, listItem); } else if (!listItem) { hideTacticPreview(); } }); listElement.addEventListener('mouseout', (event) => { const listItem = event.target.closest('.mztm-custom-select-item'); if (listItem) { const related = event.relatedTarget; if (!listItem.contains(related) && related !== previewElement) { hideTacticPreview(); } } else if (!listElement.contains(event.relatedTarget) && (!previewElement || event.relatedTarget !== previewElement)) { hideTacticPreview(); } }); window.addEventListener('scroll', hideTacticPreview, true); } function closeAllCustomDropdowns(exceptElement = null) { document.querySelectorAll('.mztm-custom-select-list-container.open').forEach(container => { const wrapper = container.closest('.mztm-custom-select-wrapper'); if (wrapper !== exceptElement?.closest('.mztm-custom-select-wrapper')) { container.classList.remove('open'); const trigger = wrapper?.querySelector('.mztm-custom-select-trigger'); trigger?.classList.remove('open'); } }); currentOpenDropdown = exceptElement?.closest('.mztm-custom-select-wrapper') || null; } document.addEventListener('click', (event) => { if (currentOpenDropdown && !currentOpenDropdown.contains(event.target)) { closeAllCustomDropdowns(); } }); function createCustomSelect(id, placeholderText) { const wrapper = document.createElement('div'); wrapper.className = 'mztm-custom-select-wrapper'; wrapper.id = `${id}_wrapper`; const trigger = document.createElement('div'); trigger.className = 'mztm-custom-select-trigger'; trigger.id = `${id}_trigger`; trigger.tabIndex = 0; const triggerText = document.createElement('span'); triggerText.className = 'mztm-custom-select-text mztm-custom-select-placeholder'; triggerText.textContent = placeholderText; trigger.appendChild(triggerText); const listContainer = document.createElement('div'); listContainer.className = 'mztm-custom-select-list-container'; listContainer.id = `${id}_list_container`; const list = document.createElement('ul'); list.className = 'mztm-custom-select-list'; list.id = `${id}_list`; listContainer.appendChild(list); addPreviewListenersToList(list); trigger.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = listContainer.classList.contains('open'); closeAllCustomDropdowns(wrapper); if (!isOpen && !trigger.classList.contains('disabled')) { listContainer.classList.add('open'); trigger.classList.add('open'); currentOpenDropdown = wrapper; } }); trigger.addEventListener('keydown', (e) => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); trigger.click(); } }); list.addEventListener('click', (e) => { const item = e.target.closest('.mztm-custom-select-item'); if (item && !item.classList.contains('disabled')) { const value = item.dataset.value || item.dataset.tacticId || item.dataset.tacticName; const text = item.textContent; triggerText.textContent = text; triggerText.classList.remove('mztm-custom-select-placeholder'); trigger.dataset.selectedValue = value; if (id === 'tactics_selector') { handleTacticSelection(value); } else if (id === 'complete_tactics_selector') { selectedCompleteTacticName = value; } closeAllCustomDropdowns(); const changeEvent = new Event('change', { bubbles: true }); trigger.dispatchEvent(changeEvent); } }); wrapper.appendChild(trigger); wrapper.appendChild(listContainer); return wrapper; } function createTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; const controlsContainer = document.createElement('div'); controlsContainer.className = 'formations-controls-container'; const dropdownWrapper = createCustomSelect('tactics_selector', USERSCRIPT_STRINGS.tacticsDropdownMenuLabel); 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(selectedFormationTacticId); }); const filterDropdownWrapper = document.createElement('div'); filterDropdownWrapper.className = 'category-filter-wrapper'; const filterSelect = document.createElement('select'); filterSelect.id = 'category_filter_selector'; filterSelect.addEventListener('change', (e) => { currentFilter = e.target.value; updateTacticsDropdown(selectedFormationTacticId); }); const manageBtn = document.createElement('button'); manageBtn.id = 'manage_items_btn'; manageBtn.className = 'mzbtn manage-items-btn'; manageBtn.innerHTML = '⚙️'; manageBtn.title = USERSCRIPT_STRINGS.managementModalTitle; manageBtn.addEventListener('click', showManagementModal); filterDropdownWrapper.appendChild(filterSelect); filterDropdownWrapper.appendChild(manageBtn); appendChildren(controlsContainer, [dropdownWrapper, searchBox, filterDropdownWrapper]); container.appendChild(controlsContainer); return container; } function updateCategoryFilterDropdown() { const filterSelect = document.getElementById('category_filter_selector'); if (!filterSelect) return; const previousValue = filterSelect.value; filterSelect.innerHTML = ''; const usedCategoryIds = new Set(tactics.map(t => t.style || OTHER_CATEGORY_ID)); let categoriesToShow = [{ id: 'all', name: USERSCRIPT_STRINGS.allTacticsFilter }]; Object.values(categories) .filter(cat => cat.id !== 'all' && (usedCategoryIds.has(cat.id) || Object.keys(DEFAULT_CATEGORIES).includes(cat.id) || cat.id === OTHER_CATEGORY_ID)) .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); }) .forEach(cat => categoriesToShow.push({ id: cat.id, name: getCategoryName(cat.id) })); categoriesToShow.forEach(categoryInfo => { const option = document.createElement('option'); option.value = categoryInfo.id; option.textContent = categoryInfo.name; filterSelect.appendChild(option); }); if (categoriesToShow.some(cat => cat.id === previousValue)) { filterSelect.value = previousValue; } else { filterSelect.value = 'all'; currentFilter = 'all'; } filterSelect.disabled = categoriesToShow.length <= 1; } function updateTacticsDropdown(currentSelectedId = null) { const listElement = document.getElementById('tactics_selector_list'); const triggerElement = document.getElementById('tactics_selector_trigger'); const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text'); const wrapper = document.getElementById('tactics_selector_wrapper'); const searchBox = document.querySelector('.tactics-search-box'); if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return; listElement.innerHTML = ''; if (searchTerm.length > 0) { wrapper.classList.add('filtering'); searchBox?.classList.add('filtering'); } else { wrapper.classList.remove('filtering'); searchBox?.classList.remove('filtering'); } const filteredTactics = tactics.filter(t => { const nameMatch = searchTerm === '' || t.name.toLowerCase().includes(searchTerm); const categoryMatch = currentFilter === 'all' || (currentFilter === OTHER_CATEGORY_ID && (!t.style || t.style === OTHER_CATEGORY_ID)) || t.style === currentFilter; return nameMatch && categoryMatch; }); const groupedTactics = {}; Object.keys(categories).forEach(id => { if (id !== 'all') groupedTactics[id] = []; }); if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = []; filteredTactics.forEach(t => { const categoryId = t.style || OTHER_CATEGORY_ID; if (!groupedTactics[categoryId]) { if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = []; groupedTactics[OTHER_CATEGORY_ID].push(t); } else groupedTactics[categoryId].push(t); }); 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 (DEFAULT_CATEGORIES[a] && !DEFAULT_CATEGORIES[b]) return -1; if (!DEFAULT_CATEGORIES[a] && DEFAULT_CATEGORIES[b]) return 1; if (a === OTHER_CATEGORY_ID) return 1; if (b === OTHER_CATEGORY_ID) return -1; return (getCategoryName(a) || '').localeCompare(getCategoryName(b) || ''); }); let itemsAdded = 0; categoryOrder.forEach(categoryId => { if (groupedTactics[categoryId].length > 0) { addTacticItemsGroup(listElement, groupedTactics[categoryId], getCategoryName(categoryId), categoryId); itemsAdded += groupedTactics[categoryId].length; } }); if (itemsAdded === 0) { const noResultsItem = document.createElement('li'); noResultsItem.className = 'mztm-custom-select-no-results'; noResultsItem.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.noTacticsFound; listElement.appendChild(noResultsItem); triggerElement.classList.add('disabled'); triggerTextElement.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.tacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedFormationTacticId = null; } else { triggerElement.classList.remove('disabled'); const currentSelection = tactics.find(t => t.id === currentSelectedId); if (currentSelection) { triggerTextElement.textContent = currentSelection.name; triggerTextElement.classList.remove('mztm-custom-select-placeholder'); triggerElement.dataset.selectedValue = currentSelection.id; selectedFormationTacticId = currentSelection.id; } else { triggerTextElement.textContent = USERSCRIPT_STRINGS.tacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedFormationTacticId = null; } } } function addTacticItemsGroup(listElement, tacticsList, groupLabel, categoryId) { if (tacticsList.length === 0) return; const categoryHeader = document.createElement('li'); categoryHeader.className = 'mztm-custom-select-category'; categoryHeader.textContent = groupLabel; listElement.appendChild(categoryHeader); tacticsList.sort((a, b) => a.name.localeCompare(b.name)); tacticsList.forEach(tactic => { const item = document.createElement('li'); item.className = 'mztm-custom-select-item'; item.textContent = tactic.name; item.dataset.tacticId = tactic.id; item.dataset.value = tactic.id; item.dataset.description = tactic.description || ''; item.dataset.style = tactic.style || OTHER_CATEGORY_ID; item.dataset.formationString = formatFormationString(getFormation(tactic.coordinates)); listElement.appendChild(item); }); } function createCompleteTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; const label = document.createElement('label'); label.textContent = ''; label.className = 'tactics-selector-label'; const dropdownWrapper = createCustomSelect('complete_tactics_selector', USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel); container.appendChild(label); container.appendChild(dropdownWrapper); return container; } function updateCompleteTacticsDropdown(currentSelectedName = null) { const listElement = document.getElementById('complete_tactics_selector_list'); const triggerElement = document.getElementById('complete_tactics_selector_trigger'); const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text'); const wrapper = document.getElementById('complete_tactics_selector_wrapper'); if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return; listElement.innerHTML = ''; const names = Object.keys(completeTactics).sort((a, b) => a.localeCompare(b)); if (names.length === 0) { const noResultsItem = document.createElement('li'); noResultsItem.className = 'mztm-custom-select-no-results'; noResultsItem.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved; listElement.appendChild(noResultsItem); triggerElement.classList.add('disabled'); triggerTextElement.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedCompleteTacticName = null; } else { triggerElement.classList.remove('disabled'); names.forEach(name => { const tactic = completeTactics[name]; const item = document.createElement('li'); item.className = 'mztm-custom-select-item'; item.textContent = name; item.dataset.tacticName = name; item.dataset.value = name; item.dataset.description = tactic.description || ''; item.dataset.formationString = formatFormationString(getFormationFromCompleteTactic(tactic)); listElement.appendChild(item); }); const currentSelection = currentSelectedName && completeTactics[currentSelectedName] ? currentSelectedName : null; if (currentSelection) { triggerTextElement.textContent = currentSelection; triggerTextElement.classList.remove('mztm-custom-select-placeholder'); triggerElement.dataset.selectedValue = currentSelection; selectedCompleteTacticName = currentSelection; } else { triggerTextElement.textContent = USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedCompleteTacticName = null; } } } function createButton(id, text, clickHandler) { const button = document.createElement('button'); setUpButton(button, id, text); if (clickHandler) { button.addEventListener('click', async (e) => { e.stopPropagation(); try { await clickHandler(); } catch (err) { console.error('Button click failed:', err); showErrorMessage('Action Failed', `${err.message || err}`); } }); } return button; } async function checkVersion() { const savedVersion = GM_getValue(VERSION_KEY, null); if (!savedVersion || savedVersion !== SCRIPT_VERSION) { await showWelcomeMessage(); GM_setValue(VERSION_KEY, SCRIPT_VERSION); } } function createModeToggleSwitch() { const label = document.createElement('label'); label.className = 'mode-toggle-switch'; const input = document.createElement('input'); input.type = 'checkbox'; input.id = 'view-mode-toggle'; input.addEventListener('change', (e) => setViewMode(e.target.checked ? 'complete' : 'normal')); const slider = document.createElement('span'); slider.className = 'mode-toggle-slider'; label.appendChild(input); label.appendChild(slider); return label; } function createModeLabel(mode, isPrefix = false) { const span = document.createElement('span'); span.className = 'mode-toggle-label'; span.textContent = isPrefix ? USERSCRIPT_STRINGS.modeLabel : (mode === 'normal' ? USERSCRIPT_STRINGS.normalModeLabel : USERSCRIPT_STRINGS.completeModeLabel); span.id = `mode-label-${mode}`; return span; } function setViewMode(mode) { currentViewMode = mode; GM_setValue(VIEW_MODE_KEY, mode); const normalContent = document.getElementById('normal-tactics-content'); const completeContent = document.getElementById('complete-tactics-content'); const toggleInput = document.getElementById('view-mode-toggle'); const normalLabel = document.getElementById('mode-label-normal'); const completeLabel = document.getElementById('mode-label-complete'); const isNormal = mode === 'normal'; if (normalContent) normalContent.style.display = isNormal ? 'block' : 'none'; if (completeContent) completeContent.style.display = isNormal ? 'none' : 'block'; if (toggleInput) toggleInput.checked = !isNormal; if (normalLabel) normalLabel.classList.toggle('active', isNormal); if (completeLabel) completeLabel.classList.toggle('active', !isNormal); } function createMainContainer() { const container = document.createElement('div'); container.id = 'mz_tactics_panel'; container.classList.add('mz-panel'); const header = document.createElement('div'); header.classList.add('mz-group-main-title'); const titleContainer = document.createElement('div'); titleContainer.className = 'mz-title-container'; const titleText = document.createElement('span'); titleText.textContent = USERSCRIPT_STRINGS.managerTitle; titleText.classList.add('mz-main-title'); const versionText = document.createElement('span'); versionText.textContent = 'v' + DISPLAY_VERSION; versionText.classList.add('mz-version-text'); const modeToggleContainer = document.createElement('div'); modeToggleContainer.className = 'mode-toggle-container'; const prefixLabel = createModeLabel('', true); const modeLabelNormal = createModeLabel('normal'); const toggleSwitch = createModeToggleSwitch(); const modeLabelComplete = createModeLabel('complete'); appendChildren(modeToggleContainer, [prefixLabel, modeLabelNormal, toggleSwitch, modeLabelComplete]); appendChildren(titleContainer, [titleText, versionText, modeToggleContainer]); header.appendChild(titleContainer); const toggleButton = createToggleButton(); header.appendChild(toggleButton); container.appendChild(header); const group = document.createElement('div'); group.classList.add('mz-group'); container.appendChild(group); const normalContent = document.createElement('div'); normalContent.id = 'normal-tactics-content'; normalContent.className = 'section-content'; const tacticsSelectorSection = createTacticsSelector(); const normalButtonsSection = document.createElement('div'); normalButtonsSection.className = 'action-buttons-section'; const addCurrentBtn = createButton('add_current_tactic_btn', USERSCRIPT_STRINGS.addCurrentTactic, addNewTactic); const addXmlBtn = createButton('add_xml_tactic_btn', USERSCRIPT_STRINGS.addWithXmlButton, addNewTacticWithXml); const editBtn = createButton('edit_tactic_button', USERSCRIPT_STRINGS.renameButton, () => editTactic()); const updateBtn = createButton('update_tactic_button', USERSCRIPT_STRINGS.updateButton, updateTactic); const deleteBtn = createButton('delete_tactic_button', USERSCRIPT_STRINGS.deleteButton, deleteTactic); const importBtn = createButton('import_tactics_btn', USERSCRIPT_STRINGS.importButton, importTacticsJsonData); const exportBtn = createButton('export_tactics_btn', USERSCRIPT_STRINGS.exportButton, exportTacticsJsonData); const resetBtn = createButton('reset_tactics_btn', USERSCRIPT_STRINGS.resetButton, resetTactics); const clearBtn = createButton('clear_tactics_btn', USERSCRIPT_STRINGS.clearButton, clearTactics); const normalButtonsRow1 = document.createElement('div'); normalButtonsRow1.className = 'action-buttons-row'; appendChildren(normalButtonsRow1, [addCurrentBtn, addXmlBtn, editBtn, updateBtn, deleteBtn]); const normalButtonsRow2 = document.createElement('div'); normalButtonsRow2.className = 'action-buttons-row'; appendChildren(normalButtonsRow2, [importBtn, exportBtn, resetBtn, clearBtn]); appendChildren(normalButtonsSection, [normalButtonsRow1, normalButtonsRow2]); appendChildren(normalContent, [tacticsSelectorSection, normalButtonsSection, createHiddenTriggerButton(), createCombinedInfoButton()]); group.appendChild(normalContent); const completeContent = document.createElement('div'); completeContent.id = 'complete-tactics-content'; completeContent.className = 'section-content'; completeContent.style.display = 'none'; const completeTacticsSelectorSection = createCompleteTacticsSelector(); const completeButtonsSection = document.createElement('div'); completeButtonsSection.className = 'action-buttons-section'; const completeButtonsRow1 = document.createElement('div'); completeButtonsRow1.className = 'action-buttons-row'; const saveCompleteBtn = createButton('save_complete_tactic_button', USERSCRIPT_STRINGS.saveCompleteTacticButton, saveCompleteTactic); const loadCompleteBtn = createButton('load_complete_tactic_button', USERSCRIPT_STRINGS.loadCompleteTacticButton, loadCompleteTactic); const renameCompleteBtn = createButton('rename_complete_tactic_button', USERSCRIPT_STRINGS.renameCompleteTacticButton, editCompleteTactic); const updateCompleteBtn = createButton('update_complete_tactic_button', USERSCRIPT_STRINGS.updateCompleteTacticButton, updateCompleteTactic); const deleteCompleteBtn = createButton('delete_complete_tactic_button', USERSCRIPT_STRINGS.deleteCompleteTacticButton, deleteCompleteTactic); appendChildren(completeButtonsRow1, [saveCompleteBtn, loadCompleteBtn, renameCompleteBtn, updateCompleteBtn, deleteCompleteBtn]); const completeButtonsRow2 = document.createElement('div'); completeButtonsRow2.className = 'action-buttons-row'; const importCompleteBtn = createButton('import_complete_tactics_btn', USERSCRIPT_STRINGS.importCompleteTacticsButton, importCompleteTactics); const exportCompleteBtn = createButton('export_complete_tactics_btn', USERSCRIPT_STRINGS.exportCompleteTacticsButton, exportCompleteTactics); appendChildren(completeButtonsRow2, [importCompleteBtn, exportCompleteBtn]); appendChildren(completeButtonsSection, [completeButtonsRow1, completeButtonsRow2]); appendChildren(completeContent, [completeTacticsSelectorSection, completeButtonsSection, createCombinedInfoButton()]); group.appendChild(completeContent); return container; } function createHiddenTriggerButton() { const button = document.createElement('button'); button.id = 'hidden_trigger_button'; button.textContent = ''; button.style.cssText = 'position:absolute; opacity:0; pointer-events:none; width:0; height:0; padding:0; margin:0; border:0;'; button.addEventListener('click', function() { const presetSelect = document.getElementById('tactics_preset'); if (presetSelect) { presetSelect.value = '5-3-2'; presetSelect.dispatchEvent(new Event('change')); } }); return button; } function setUpButton(button, id, text) { button.id = id; button.classList.add('mzbtn'); button.textContent = text; } function createModalTabs(tabsConfig, modalBody) { const tabsContainer = document.createElement('div'); tabsContainer.className = 'modal-tabs'; tabsConfig.forEach((tab, index) => { const tabButton = document.createElement('button'); tabButton.className = 'modal-tab'; tabButton.textContent = tab.title; tabButton.dataset.tabId = tab.id; if (index === 0) tabButton.classList.add('active'); tabButton.addEventListener('click', () => { modalBody.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active')); modalBody.querySelectorAll('.management-modal-content, .modal-tab-content').forEach(c => c.classList.remove('active')); tabButton.classList.add('active'); const content = modalBody.querySelector(`.management-modal-content[data-tab-id="${tab.id}"], .modal-tab-content[data-tab-id="${tab.id}"]`); if (content) content.classList.add('active'); }); tabsContainer.appendChild(tabButton); }); return tabsContainer; } function createTabbedModalContent(tabsConfig) { const wrapper = document.createElement('div'); wrapper.className = 'modal-info-wrapper'; const tabs = createModalTabs(tabsConfig, wrapper); wrapper.appendChild(tabs); tabsConfig.forEach((tab, index) => { const contentDiv = document.createElement('div'); contentDiv.className = 'modal-tab-content'; contentDiv.dataset.tabId = tab.id; if (index === 0) contentDiv.classList.add('active'); const content = tab.contentGenerator(); contentDiv.appendChild(content); wrapper.appendChild(contentDiv); }); return wrapper; } function createAboutTabContent() { const content = document.createElement('div'); const aboutSection = document.createElement('div'); const aboutTitle = document.createElement('h3'); aboutTitle.textContent = 'About'; const infoText = document.createElement('p'); infoText.id = 'info_modal_info_text'; infoText.innerHTML = USERSCRIPT_STRINGS.modalContentInfoText; const feedbackText = document.createElement('p'); feedbackText.id = 'info_modal_feedback_text'; feedbackText.innerHTML = USERSCRIPT_STRINGS.modalContentFeedbackText; appendChildren(aboutSection, [aboutTitle, infoText, feedbackText]); content.appendChild(aboutSection); const faqSection = document.createElement('div'); faqSection.className = 'faq-section'; const faqTitle = document.createElement('h3'); faqTitle.textContent = 'FAQ/Function Explanations'; faqSection.appendChild(faqTitle); const formationItems = [ { q: "Add Current Button (Formations Mode)", a: "Saves the player positions currently visible on the pitch as a new formation. You'll be prompted for a name, category, and an optional description." }, { q: "Add via XML Button (Formations Mode)", a: "Allows pasting XML to add a new formation. Only player positions are saved from the XML. Prompted for name, category, and description." }, { q: "Category Filter Dropdown & ⚙️ Button (Formations Mode)", a: "Use the dropdown to filter formations by category. Click the gear icon (⚙️) to open the Management Modal (Formations & Categories)." }, { q: "Edit Button (Formations Mode)", a: "Allows renaming the selected formation, changing its assigned category, and editing its description via a popup." }, { q: "Update Coords Button (Formations Mode)", a: "Updates the coordinates of the selected formation to match the current player positions on the pitch (description and category remain unchanged)." }, { q: "Delete Button (Formations Mode)", a: "Permanently removes the selected formation from the storage." }, { q: "Import Button (Formations Mode)", a: "Imports multiple formations from a JSON text format. Merges with existing formations (updates name/category/description if ID matches)." }, { q: "Export Button (Formations Mode)", a: "Exports all saved formations (including descriptions) into a JSON text format (copied to clipboard)." }, { q: "Reset Button (Formations Mode)", a: "Deletes all saved formations and custom categories, restoring defaults." }, { q: "Clear Button (Formations Mode)", a: "Deletes all saved formations." }, { q: "Management Modal (Gear Icon ⚙️)", a: "Opens a dedicated window to manage formations (edit name/description/category, delete) and categories (add, remove) in bulk." }, { q: "Preview on Hover (Formations Mode)", a: "Hover your mouse over a formation name in the dropdown list to see its numerical formation (e.g., 4-4-2) and its description in a small pop-up." } ]; const tacticItems = [ { q: "Save Current Button (Tactics Mode)", a: "Exports the entire current tactic setup (positions, alts, rules, settings) using MZ's native export, parses it, prompts for a name and description, then saves it as a new complete tactic." }, { q: "Load Button (Tactics Mode)", a: "Loads a saved complete tactic using MZ's native import. Shows a spinner during load. Matches players or substitutes if needed. Updates everything on the pitch." }, { q: "Rename Button (Tactics Mode)", a: "Allows renaming the selected complete tactic and editing its description via a popup." }, { q: "Update with Current Button (Tactics Mode)", a: "Overwrites the selected complete tactic's positions, rules, and settings with the setup currently on the pitch (using native export). The existing description is kept." }, { q: "Delete Button (Tactics Mode)", a: "Permanently removes the selected complete tactic." }, { q: "Import Button (Tactics Mode)", a: "Imports multiple complete tactics from a JSON text format. Merges with existing tactics, overwriting any with the same name (including description)." }, { q: "Export Button (Tactics Mode)", a: "Exports all saved complete tactics (including descriptions) into a JSON text format (copied to clipboard)." }, { q: "Preview on Hover (Tactics Mode)", a: "Hover your mouse over a tactic name in the dropdown list to see its numerical formation (e.g., 5-3-2, based on initial positions) and its description in a small pop-up." } ]; const combinedItems = [...formationItems, ...tacticItems].sort((a,b) => { const modeA = a.q.includes("Formations Mode") || a.q.includes("Category Filter") || a.q.includes("Management Modal") ? 0 : (a.q.includes("Tactics Mode") ? 1 : 2); const modeB = b.q.includes("Formations Mode") || b.q.includes("Category Filter") || b.q.includes("Management Modal") ? 0 : (b.q.includes("Tactics Mode") ? 1 : 2); if (modeA !== modeB) return modeA - modeB; return a.q.localeCompare(b.q); }); combinedItems.forEach(item => { const faqItemDiv = document.createElement('div'); faqItemDiv.className = 'faq-item'; const question = document.createElement('h4'); question.innerHTML = item.q; const answer = document.createElement('p'); answer.textContent = item.a; appendChildren(faqItemDiv, [question, answer]); faqSection.appendChild(faqItemDiv); }); content.appendChild(faqSection); return content; } function createLinksTabContent() { const content = document.createElement('div'); const linksSection = document.createElement('div'); const linksTitle = document.createElement('h3'); linksTitle.textContent = 'Useful Links'; const resourcesText = createUsefulContent(); const linksMap = 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/en/scripts/373382-van-mz-playeradvanced'], ['Mazyar Userscript', 'https://greasyfork.org/en/scripts/476290-mazyar'], ['Stats Xente Userscript', 'https://greasyfork.org/en/scripts/491442-stats-xente-script'], ['More userscripts', 'https://greasyfork.org/en/users/1088808-douglasdotv'] ]); const linksList = createLinksList(linksMap); appendChildren(linksSection, [linksTitle, resourcesText, linksList]); content.appendChild(linksSection); return content; } function createCombinedInfoButton() { const button = createButton('info_button', USERSCRIPT_STRINGS.infoButton, null); button.classList.add('footer-actions'); button.style.background = 'transparent'; button.style.border = 'none'; button.style.boxShadow = 'none'; button.style.fontFamily = '"Quicksand", sans-serif'; button.style.color = 'gold'; button.addEventListener('click', (e) => { e.stopPropagation(); const tabsConfig = [{ id: 'about', title: 'About & FAQ', contentGenerator: createAboutTabContent }, { id: 'links', title: 'Useful Links', contentGenerator: createLinksTabContent }]; const modalContent = createTabbedModalContent(tabsConfig); showAlert({ title: 'MZ Tactics Manager Info', htmlContent: modalContent, confirmButtonText: DEFAULT_MODAL_STRINGS.ok }); }); return button; } function createUsefulContent() { const p = document.createElement('p'); p.id = 'useful_content'; p.textContent = USERSCRIPT_STRINGS.usefulContent; return p; } function createLinksList(linksMap) { const list = document.createElement('ul'); linksMap.forEach((href, text) => { const listItem = document.createElement('li'); const anchor = document.createElement('a'); anchor.href = href; anchor.target = '_blank'; anchor.rel = 'noopener noreferrer'; anchor.textContent = text; listItem.appendChild(anchor); list.appendChild(listItem); }); return list; } 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'; collapsedIconElement = icon; return icon; } async function initializeScriptData() { loadCategories(); await checkVersion(); const ids = await fetchTeamIdAndUsername(); if (!ids.teamId) { console.warn("MZTM: Failed to get Team ID."); } let tacticData = GM_getValue(FORMATIONS_STORAGE_KEY); const oldTacticData = GM_getValue(OLD_FORMATIONS_STORAGE_KEY); if (!tacticData && oldTacticData && oldTacticData.tactics && Array.isArray(oldTacticData.tactics)) { console.log(`MZTM: Migrating tactics from old storage key '${OLD_FORMATIONS_STORAGE_KEY}' to '${FORMATIONS_STORAGE_KEY}'.`); tacticData = oldTacticData; tacticData.tactics = tacticData.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates)); tacticData.tactics.forEach(t => { if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID; if (!t.hasOwnProperty('description')) t.description = ''; }); GM_setValue(FORMATIONS_STORAGE_KEY, tacticData); GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); console.log(`MZTM: Migration complete. Deleted old key '${OLD_FORMATIONS_STORAGE_KEY}'.`); } else if (!tacticData) { console.log("MZTM: No existing formations data found. Initializing empty store."); tacticData = { tactics: [] }; GM_setValue(FORMATIONS_STORAGE_KEY, tacticData); } else { if (!tacticData.tactics || !Array.isArray(tacticData.tactics)) tacticData.tactics = []; tacticData.tactics = tacticData.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates)); let dataChanged = false; tacticData.tactics.forEach(t => { if (!t.hasOwnProperty('style')) { t.style = OTHER_CATEGORY_ID; dataChanged = true; } if (!t.hasOwnProperty('description')) { t.description = ''; dataChanged = true; } }); if(dataChanged) GM_setValue(FORMATIONS_STORAGE_KEY, tacticData); } tactics = tacticData.tactics || []; tactics.sort((a, b) => a.name.localeCompare(b.name)); loadCompleteTacticsData(); const storedCompleteTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {}); let completeTacticsChanged = false; for (const name in storedCompleteTactics) { if (storedCompleteTactics.hasOwnProperty(name)) { if (!storedCompleteTactics[name].hasOwnProperty('description')) { storedCompleteTactics[name].description = ''; completeTacticsChanged = true; } } } if (completeTacticsChanged) GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, storedCompleteTactics); completeTactics = storedCompleteTactics; } function setUpTacticsInterface(mainContainer) { const toggleButton = mainContainer.querySelector('#toggle_panel_btn'); const collapsedIcon = collapsedIconElement || createCollapsedIcon(); let isCollapsed = GM_getValue(COLLAPSED_KEY, false); const anchorButtonId = 'replace-player-btn'; const applyCollapseState = (instant = false) => { const anchorButton = document.getElementById(anchorButtonId); if (collapsedIcon && collapsedIcon.parentNode) { collapsedIcon.parentNode.removeChild(collapsedIcon); } if (isCollapsed) { if (instant) { mainContainer.style.transition = 'none'; mainContainer.classList.add('collapsed'); void mainContainer.offsetHeight; mainContainer.style.transition = ''; } else { mainContainer.classList.add('collapsed'); } toggleButton.innerHTML = '☰'; toggleButton.title = 'Show panel'; if (anchorButton) { insertAfterElement(collapsedIcon, anchorButton); collapsedIcon.classList.add('visible'); } else { console.warn(`MZTM: Anchor button #${anchorButtonId} not found for collapsed icon.`); collapsedIcon.classList.remove('visible'); } } else { mainContainer.classList.remove('collapsed'); toggleButton.innerHTML = '✕'; toggleButton.title = 'Hide panel'; collapsedIcon.classList.remove('visible'); } }; applyCollapseState(true); function togglePanel() { isCollapsed = !isCollapsed; GM_setValue(COLLAPSED_KEY, isCollapsed); applyCollapseState(); } toggleButton.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); }); collapsedIcon.addEventListener('click', () => { togglePanel(); }); } async function initialize() { const tacticsBox = document.getElementById('tactics_box'); if (!tacticsBox || !isFootball()) { console.log("MZTM: Not on valid page or tactics box not found."); return; } const cachedUserInfo = GM_getValue(USER_INFO_CACHE_KEY); if (cachedUserInfo && typeof cachedUserInfo === 'object' && cachedUserInfo.teamId && cachedUserInfo.username && cachedUserInfo.timestamp) { userInfoCache = cachedUserInfo; if (Date.now() - userInfoCache.timestamp < USER_INFO_CACHE_DURATION_MS) { teamId = userInfoCache.teamId; username = userInfoCache.username; } } const cachedRoster = GM_getValue(ROSTER_CACHE_KEY); if (cachedRoster && typeof cachedRoster === 'object' && cachedRoster.data && cachedRoster.timestamp) { rosterCache = cachedRoster; } try { collapsedIconElement = createCollapsedIcon(); createTacticPreviewElement(); await initializeScriptData(); const mainContainer = createMainContainer(); setUpTacticsInterface(mainContainer); insertAfterElement(mainContainer, tacticsBox); updateTacticsDropdown(); updateCategoryFilterDropdown(); updateCompleteTacticsDropdown(); const savedMode = GM_getValue(VIEW_MODE_KEY, 'normal'); setViewMode(savedMode); } catch (error) { console.error('MZTM Initialization Error:', error); const errorDiv = document.createElement('div'); errorDiv.textContent = 'Error initializing MZ Tactics Manager. Check console for details.'; errorDiv.style.cssText = 'color:red; padding:10px; border:1px solid red; margin:10px;'; insertAfterElement(errorDiv, tacticsBox); } } window.addEventListener('load', initialize); })();