// ==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
// ==/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 <SoccerTactics>");
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 = `<?xml version="1.0" ?>\n<SoccerTactics>\n`;
const mappedCaptain = playerMapping[tacticData.teamSettings.captainPID] || '0';
xml += `\t<Team tactics="${tacticData.teamSettings.passingStyle || 'shortpass'}" playstyle="${tacticData.teamSettings.mentality || 'normal'}" aggression="${tacticData.teamSettings.aggression || 'normal'}" captain="${mappedCaptain}" />\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<Pos pos="${playerData.pos}" pid="${mappedPid}" x="${initial.x}" y="${initial.y}" x1="${alt1.x}" y1="${alt1.y}" x2="${alt2.x}" y2="${alt2.y}" />\n`;
}
tacticData.substitutes.forEach(s => {
const mappedPid = playerMapping[s.pid];
if (mappedPid) xml += `\t<Sub pos="${s.pos}" pid="${mappedPid}" x="${s.x}" y="${s.y}" />\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<TacticRule';
for (const attr in rule) {
let value = rule[attr];
if (attr === 'out_player') value = mappedOutPlayer;
if (attr === 'in_player_id') value = mappedInPlayer;
xml += ` ${attr}="${value}"`;
}
xml += ' />\n';
}
});
xml += '</SoccerTactics>';
return xml;
}
async function saveCompleteTactic() {
const exportButton = document.getElementById('export_button');
const importExportWindow = document.getElementById('importExportTacticsWindow');
const playerInfoWindow = document.getElementById('playerInfoWindow');
const importExportData = document.getElementById('importExportData');
if (!exportButton || !importExportWindow || !playerInfoWindow || !importExportData) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find required MZ UI elements for export.');
const windowHidden = importExportWindow.style.display === 'none';
if (windowHidden) {
const toggleButton = document.getElementById('import_export_button');
if (toggleButton) toggleButton.click();
else return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find button to toggle XML view.');
}
importExportData.value = '';
exportButton.click();
await new Promise(r => 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 = `
<div class="mztm-preview-formation"><strong>${USERSCRIPT_STRINGS.previewFormationLabel}</strong> ${formationString}</div>
${description ? `<div class="mztm-preview-desc">${description.replace(/\n/g, '<br>')}</div>` : '<div class="mztm-preview-no-desc">No description available.</div>'}
`;
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: "<code>Add Current</code> 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: "<code>Add via XML</code> 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 & <code>⚙️</code> Button (Formations Mode)", a: "Use the dropdown to filter formations by category. Click the gear icon (⚙️) to open the Management Modal (Formations & Categories)." },
{ q: "<code>Edit</code> Button (Formations Mode)", a: "Allows renaming the selected formation, changing its assigned category, and editing its description via a popup." },
{ q: "<code>Update Coords</code> 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: "<code>Delete</code> Button (Formations Mode)", a: "Permanently removes the selected formation from the storage." },
{ q: "<code>Import</code> Button (Formations Mode)", a: "Imports multiple formations from a JSON text format. Merges with existing formations (updates name/category/description if ID matches)." },
{ q: "<code>Export</code> Button (Formations Mode)", a: "Exports all saved formations (including descriptions) into a JSON text format (copied to clipboard)." },
{ q: "<code>Reset</code> Button (Formations Mode)", a: "Deletes all saved formations and custom categories, restoring defaults." },
{ q: "<code>Clear</code> 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: "<code>Save Current</code> 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: "<code>Load</code> 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: "<code>Rename</code> Button (Tactics Mode)", a: "Allows renaming the selected complete tactic and editing its description via a popup." },
{ q: "<code>Update with Current</code> 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: "<code>Delete</code> Button (Tactics Mode)", a: "Permanently removes the selected complete tactic." },
{ q: "<code>Import</code> 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: "<code>Export</code> 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);
})();