ylOppTacticsPreview (Modified)

Shows the latest tactics used by an upcoming opponent from the scheduled matches page

当前为 2025-06-11 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ylOppTacticsPreview (Modified)
// @namespace    douglaskampl
// @version      5.0.0
// @description  Shows the latest tactics used by an upcoming opponent from the scheduled matches page
// @author       kostrzak16 (feat. Douglas and xente)
// @match        https://www.managerzone.com/?p=match&sub=scheduled
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js
// @resource     oppTacticsPreviewStyles https://br18.org/mz/userscript/other/Slezsko.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('oppTacticsPreviewStyles'));

    const CONSTANTS = {
        MATCH_TYPE_GROUPS: {
            'General': [
                { id: 'no_restriction', label: 'Senior_All' },
                { id: 'u23', label: 'U23_All' },
                { id: 'u21', label: 'U21_All' },
                { id: 'u18', label: 'U18_All' }
            ],
            'World League': [
                { id: 'world_series', label: 'Senior WL' },
                { id: 'u23_world_series', label: 'U23 WL' },
                { id: 'u21_world_series', label: 'U21 WL' },
                { id: 'u18_world_series', label: 'U18 WL' }
            ],
            'Official League': [
                { id: 'series', label: 'Senior League' },
                { id: 'u23_series', label: 'U23 League' },
                { id: 'u21_series', label: 'U21 League' },
                { id: 'u18_series', label: 'U18 League' }
            ]
        },
        MATCH_STATS_URL: (matchId) => `https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=${matchId}&sport=soccer`,
        STORAGE_KEYS: {
            MATCH_LIMIT: 'ylopp_match_limit',
            SAVED_TEAMS: 'ylopp_saved_teams',
            USER_TEAM_ID: 'ylopp_user_team_id'
        },
        DEFAULTS: {
            MATCH_LIMIT: 10,
            MAX_SAVED_TEAMS: 15,
            MAX_MATCH_LIMIT: 100
        },
        SELECTORS: {
            FIXTURES_LIST: '#fixtures-results-list-wrapper',
            STATS_XENTE: '#legendDiv',
            ELO_SCHEDULED: '#eloScheduledSelect',
            HOME_TEAM: '.home-team-column.flex-grow-1',
            SELECT_WRAPPER: 'dd.set-default-wrapper'
        },
    };

    let ourTeamName = null;
    let userTeamId = null;
    let currentOpponentTid = '';
    let spinnerInstance = null;

    const observer = new MutationObserver(() => {
        insertIconsAndListeners();
    });

    function getMatchLimit() {
        return GM_getValue(CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, CONSTANTS.DEFAULTS.MATCH_LIMIT);
    }

    function setMatchLimit(limit) {
        const numericLimit = parseInt(limit, 10);
        if (!isNaN(numericLimit) && numericLimit > 0 && numericLimit <= CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT) {
            GM_setValue(CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, numericLimit);
        }
    }

    function getSavedTeams() {
        return GM_getValue(CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, []);
    }

    function saveTeam(teamId, teamName) {
        if (!teamId || !teamName || teamName.startsWith('Team ')) {
            return;
        }
        let teams = getSavedTeams();
        const existingIndex = teams.findIndex(team => team.id === teamId);
        if (existingIndex > -1) {
            teams.splice(existingIndex, 1);
        }
        teams.unshift({ id: teamId, name: teamName });
        const trimmedTeams = teams.slice(0, CONSTANTS.DEFAULTS.MAX_SAVED_TEAMS);
        GM_setValue(CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, trimmedTeams);
    }

    function removeSavedTeam(teamId) {
        let teams = getSavedTeams();
        const updatedTeams = teams.filter(team => team.id !== teamId);
        GM_setValue(CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, updatedTeams);
    }

    function startObserving() {
        const fixturesList = document.querySelector(CONSTANTS.SELECTORS.FIXTURES_LIST);
        if (fixturesList) {
            observer.observe(fixturesList, { childList: true, subtree: true });
        }
    }

    function showLoadingSpinner() {
        if (spinnerInstance) {
            return;
        }
        const spinnerContainer = document.createElement('div');
        spinnerContainer.id = 'spinjs-overlay';
        document.body.appendChild(spinnerContainer);
        spinnerInstance = new Spinner({ color: '#FFFFFF', lines: 12, top: '50%', left: '50%' }).spin(spinnerContainer);
    }

    function hideLoadingSpinner() {
        if (spinnerInstance) {
            spinnerInstance.stop();
            spinnerInstance = null;
        }
        const spinnerContainer = document.getElementById('spinjs-overlay');
        if (spinnerContainer) {
            spinnerContainer.remove();
        }
    }

    function extractTeamNameFromHtml(htmlDocument, teamId) {
        const nameCounts = new Map();
        const teamLinks = htmlDocument.querySelectorAll('.teams-wrapper a.clippable');
        teamLinks.forEach(link => {
            const linkUrl = new URL(link.href, location.href);
            const linkTid = linkUrl.searchParams.get('tid');
            if (linkTid === teamId) {
                const fullName = link.querySelector('.full-name')?.textContent.trim();
                if (fullName) {
                    nameCounts.set(fullName, (nameCounts.get(fullName) || 0) + 1);
                }
            }
        });

        if (nameCounts.size > 0) {
            let mostCommonName = '';
            let maxCount = 0;
            for (const [name, count] of nameCounts.entries()) {
                if (count > maxCount) {
                    maxCount = count;
                    mostCommonName = name;
                }
            }
            return mostCommonName;
        }

        const boldTeamNameElement = htmlDocument.querySelector('.teams-wrapper a.clippable > strong > .full-name');
        if (boldTeamNameElement) {
            return boldTeamNameElement.textContent.trim();
        }

        return null;
    }

    async function fetchLatestTactics(teamId, matchType) {
        const modal = document.getElementById('interaction-modal');
        if (modal) {
            fadeOutAndRemove(modal);
        }
        showLoadingSpinner();
        try {
            const response = await fetch(
                'https://www.managerzone.com/ajax.php?p=matches&sub=list&sport=soccer',
                {
                    method: 'POST',
                    headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: `type=played&hidescore=false&tid1=${teamId}&offset=&selectType=${matchType}&limit=max`,
                    credentials: 'include'
                }
            );
            if (!response.ok) {
                throw new Error(`Network response was not ok: ${response.statusText}`);
            }

            const data = await response.json();
            const parser = new DOMParser();
            const htmlDocument = parser.parseFromString(data.list, 'text/html');
            const actualTeamName = extractTeamNameFromHtml(htmlDocument, teamId);
            const finalTeamName = actualTeamName || `Team ${teamId}`;

            saveTeam(teamId, finalTeamName);
            currentOpponentTid = teamId;
            processTacticsData(htmlDocument, matchType, finalTeamName);
        } catch (error) {
            console.error('Failed to fetch latest tactics:', error);
        } finally {
            hideLoadingSpinner();
        }
    }

    function processTacticsData(htmlDocument, matchType, opponentName) {
        const matchEntries = htmlDocument.querySelectorAll('dl > dd.odd');
        const container = createTacticsContainer(matchType, opponentName);
        document.body.appendChild(container);
        const listWrapper = container.querySelector('.tactics-list');
        let processedCount = 0;
        const matchLimit = getMatchLimit();

        for (const entry of matchEntries) {
            if (processedCount >= matchLimit) {
                break;
            }

            if (currentOpponentTid !== userTeamId && entry.querySelector('.fa-user-secret')) {
                continue;
            }

            const link = entry.querySelector('a.score-shown');
            if (!link) {
                continue;
            }

            const dl = link.closest('dl');
            const theScore = link.textContent.trim();
            const homeTeamName = dl.querySelector('.home-team-column .full-name')?.textContent.trim() || 'Home';
            const awayTeamName = dl.querySelector('.away-team-column .full-name')?.textContent.trim() || 'Away';
            const mid = new URLSearchParams(new URL(link.href, location.href).search).get('mid');
            if (!mid) {
                continue;
            }

            let [homeGoals, awayGoals] = [0, 0];
            if (theScore.includes('-')) {
                const parts = theScore.split('-').map(x => parseInt(x.trim(), 10));
                if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
                    [homeGoals, awayGoals] = parts;
                }
            }

            const opponentIsHome = (homeTeamName === opponentName);
            const tacticUrl = `https://www.managerzone.com/dynimg/pitch.php?match_id=${mid}`;
            const resultUrl = `https://www.managerzone.com/?p=match&sub=result&mid=${mid}`;
            const canvas = createCanvasWithReplacedColors(tacticUrl, opponentIsHome);
            const item = document.createElement('div');
            item.className = 'tactic-item';

            const opponentGoals = opponentIsHome ? homeGoals : awayGoals;
            const otherGoals = opponentIsHome ? awayGoals : homeGoals;

            if (opponentGoals > otherGoals) {
                item.classList.add('tactic-win');
            } else if (opponentGoals < otherGoals) {
                item.classList.add('tactic-loss');
            } else {
                item.classList.add('tactic-draw');
            }

            const linkA = document.createElement('a');
            linkA.href = resultUrl;
            linkA.target = '_blank';
            linkA.className = 'tactic-link';
            linkA.appendChild(canvas);

            const scoreP = document.createElement('p');
            scoreP.textContent = `${homeTeamName} ${theScore} ${awayTeamName}`;
            linkA.appendChild(scoreP);
            item.appendChild(linkA);

            addPlaystyleHover(mid, canvas, currentOpponentTid);
            listWrapper.appendChild(item);
            processedCount++;
        }
        if (processedCount === 0) {
            const message = document.createElement('div');
            message.className = 'no-tactics-message';
            message.textContent = 'No recent valid tactics found for this team and category.';
            listWrapper.appendChild(message);
        }
        container.classList.add('fade-in');
    }

    function showInteractionModal(teamId, sourceElement) {
        const existingModal = document.getElementById('interaction-modal');
        if (existingModal) {
            fadeOutAndRemove(existingModal);
        }
        const modal = document.createElement('div');
        modal.id = 'interaction-modal';
        modal.classList.add('fade-in');
        const header = document.createElement('div');
        header.className = 'interaction-modal-header';
        const title = document.createElement('span');
        title.textContent = '';
        header.appendChild(title);
        const settingsIcon = document.createElement('span');
        settingsIcon.className = 'settings-icon';
        settingsIcon.innerHTML = '⚙';
        header.appendChild(settingsIcon);
        modal.appendChild(header);
        const teamInputSection = createTeamInputSection(modal, teamId);
        createTabbedButtons(modal, teamInputSection.teamIdInput);
        const settingsPanel = createSettingsPanel(modal);
        settingsIcon.onclick = () => {
            settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block';
        };
        document.body.appendChild(modal);
        const rect = sourceElement.getBoundingClientRect();
        modal.style.position = 'absolute';
        modal.style.top = `${window.scrollY + rect.bottom + 5}px`;
        modal.style.left = `${window.scrollX + rect.left}px`;
    }

    function createTeamInputSection(container, initialTeamId) {
        const section = document.createElement('div');
        section.className = 'interaction-section team-input-section';
        const label = document.createElement('label');
        label.textContent = 'Team ID:';
        label.htmlFor = 'team-id-input';
        section.appendChild(label);
        const teamIdInput = document.createElement('input');
        teamIdInput.type = 'text';
        teamIdInput.id = 'team-id-input';
        teamIdInput.value = initialTeamId;
        section.appendChild(teamIdInput);
        const select = createRecentsDropdown(teamIdInput);
        section.appendChild(select);
        container.appendChild(section);
        return { teamIdInput, recentsSelect: select };
    }

    function createRecentsDropdown(teamIdInput) {
        const select = document.createElement('select');
        select.className = 'recents-select';
        const defaultOption = document.createElement('option');
        defaultOption.textContent = 'Recent Teams';
        defaultOption.value = '';
        select.appendChild(defaultOption);
        getSavedTeams().forEach(team => {
            const option = document.createElement('option');
            option.value = team.id;
            option.textContent = `${team.name} (${team.id})`;
            select.appendChild(option);
        });
        select.onchange = () => {
            if (select.value) {
                teamIdInput.value = select.value;
            }
        };
        return select;
    }

    function createTabbedButtons(container, teamIdInput) {
        const tabContainer = document.createElement('div');
        tabContainer.className = 'tab-container';
        const tabHeaders = document.createElement('div');
        tabHeaders.className = 'tab-headers';
        const tabContents = document.createElement('div');
        tabContents.className = 'tab-contents';
        Object.entries(CONSTANTS.MATCH_TYPE_GROUPS).forEach(([groupName, types], index) => {
            const header = document.createElement('button');
            header.className = 'tab-header';
            header.textContent = groupName;
            const content = document.createElement('div');
            content.className = 'tab-content';
            types.forEach(type => {
                const button = document.createElement('button');
                button.textContent = type.label;
                button.onclick = () => {
                    const teamId = teamIdInput.value.trim();
                    if (teamId) {
                        fetchLatestTactics(teamId, type.id);
                    }
                };
                content.appendChild(button);
            });
            header.onclick = () => {
                tabContainer.querySelectorAll('.tab-header').forEach(h => h.classList.remove('active'));
                tabContainer.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
                header.classList.add('active');
                content.style.display = 'flex';
            };
            tabHeaders.appendChild(header);
            tabContents.appendChild(content);
            if (index === 0) {
                header.classList.add('active');
                content.style.display = 'flex';
            } else {
                content.style.display = 'none';
            }
        });
        tabContainer.appendChild(tabHeaders);
        tabContainer.appendChild(tabContents);
        container.appendChild(tabContainer);
    }

    function createSettingsPanel(modalContainer) {
        const panel = document.createElement('div');
        panel.className = 'settings-panel';
        panel.style.display = 'none';
        const limitLabel = document.createElement('label');
        limitLabel.textContent = `Match Limit (1-${CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT}):`;
        panel.appendChild(limitLabel);
        const limitInput = document.createElement('input');
        limitInput.type = 'text';
        limitInput.inputMode = 'numeric';
        limitInput.pattern = '[0-9]*';
        limitInput.value = getMatchLimit();
        limitInput.oninput = () => {
            limitInput.value = limitInput.value.replace(/\D/g, '');
        };
        limitInput.onchange = () => setMatchLimit(limitInput.value);
        panel.appendChild(limitInput);
        modalContainer.appendChild(panel);
        return panel;
    }

    function createTacticsContainer(matchType, opponent) {
        const existingContainer = document.getElementById('tactics-container');
        if (existingContainer) {
            fadeOutAndRemove(existingContainer);
        }
        const container = document.createElement('div');
        container.id = 'tactics-container';
        container.className = 'tactics-container';
        const header = document.createElement('div');
        header.className = 'tactics-header';
        const title = document.createElement('div');
        title.className = 'match-info-text';
        let matchTypeLabel = matchType;
        for (const group in CONSTANTS.MATCH_TYPE_GROUPS) {
            const found = CONSTANTS.MATCH_TYPE_GROUPS[group].find(t => t.id === matchType);
            if (found) {
                matchTypeLabel = found.label;
                break;
            }
        }
        title.innerHTML = `<div class="title-main">${opponent} (${matchTypeLabel})</div>`;
        header.appendChild(title);
        const closeButton = document.createElement('button');
        closeButton.className = 'close-button';
        closeButton.textContent = '×';
        closeButton.onclick = () => fadeOutAndRemove(container);
        header.appendChild(closeButton);
        container.appendChild(header);
        const listWrapper = document.createElement('div');
        listWrapper.className = 'tactics-list';
        container.appendChild(listWrapper);
        return container;
    }

    function fadeOutAndRemove(el) {
        if (!el) {
            return;
        }
        el.classList.remove('fade-in');
        el.classList.add('fade-out');
        setTimeout(() => el.remove(), 200);
    }

    function identifyUserTeamName() {
        const ddRows = document.querySelectorAll('#fixtures-results-list > dd.odd');
        if (ddRows.length === 0) {
            return null;
        }
        const countMap = new Map();
        ddRows.forEach(dd => {
            const homeName = dd.querySelector('.home-team-column .full-name')?.textContent.trim();
            const awayName = dd.querySelector('.away-team-column .full-name')?.textContent.trim();
            if (homeName) {
                countMap.set(homeName, (countMap.get(homeName) || 0) + 1);
            }
            if (awayName) {
                countMap.set(awayName, (countMap.get(awayName) || 0) + 1);
            }
        });
        for (const [name, count] of countMap.entries()) {
            if (count === ddRows.length) {
                return name;
            }
        }
        return null;
    }

    function insertIconsAndListeners() {
        if (!ourTeamName) {
            ourTeamName = identifyUserTeamName();
        }
        if (!ourTeamName) {
            return;
        }

        document.querySelectorAll('dd.odd').forEach(dd => {
            const selectWrapper = dd.querySelector(CONSTANTS.SELECTORS.SELECT_WRAPPER);
            if (selectWrapper && !selectWrapper.querySelector('.magnifier-icon')) {
                const homeTeamName = dd.querySelector('.home-team-column .full-name')?.textContent.trim();
                const awayTeamName = dd.querySelector('.away-team-column .full-name')?.textContent.trim();
                const homeTeamLink = dd.querySelector('.home-team-column a.clippable');
                const awayTeamLink = dd.querySelector('.away-team-column a.clippable');
                let opponentName = null, opponentTid = null;
                if (homeTeamName === ourTeamName && awayTeamName && awayTeamLink) {
                    opponentName = awayTeamName;
                    const awayHref = awayTeamLink.href;
                    if (awayHref) {
                        opponentTid = new URLSearchParams(new URL(awayHref, location.href).search).get('tid');
                    }
                } else if (awayTeamName === ourTeamName && homeTeamName && homeTeamLink) {
                    opponentName = homeTeamName;
                    const homeHref = homeTeamLink.href;
                    if (homeHref) {
                        opponentTid = new URLSearchParams(new URL(homeHref, location.href).search).get('tid');
                    }
                }
                if (opponentName && opponentTid) {
                    const iconWrapper = document.createElement('span');
                    iconWrapper.className = 'magnifier-icon';
                    iconWrapper.dataset.tid = opponentTid;
                    iconWrapper.dataset.opponent = opponentName;
                    iconWrapper.title = 'Check opponent latest tactics';
                    iconWrapper.textContent = '🔍';
                    const select = selectWrapper.querySelector('select');
                    if (select) {
                        select.insertAdjacentElement('afterend', iconWrapper);
                    }
                }
            }
        });
    }

    function createCanvasWithReplacedColors(imageUrl, opponentIsHome) {
        const canvas = document.createElement('canvas');
        canvas.width = 150;
        canvas.height = 200;
        const context = canvas.getContext('2d');
        const image = new Image();
        image.crossOrigin = 'Anonymous';
        image.onload = () => {
            if (opponentIsHome) {
                context.translate(canvas.width / 2, canvas.height / 2);
                context.rotate(Math.PI);
                context.translate(-canvas.width / 2, -canvas.height / 2);
            }
            context.drawImage(image, 0, 0, canvas.width, canvas.height);
            const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;
            const darkGreen = { r: 0, g: 100, b: 0 };
            for (let i = 0; i < data.length; i += 4) {
                const r = data[i], g = data[i + 1], b = data[i + 2];
                const isBlack = r < 30 && g < 30 && b < 30;
                const isYellow = r > 200 && g > 200 && b < 100;
                if (opponentIsHome) {
                    if (isYellow) {
                        data[i] = 0;
                        data[i + 1] = 0;
                        data[i + 2] = 0;
                    } else if (isBlack) {
                        data[i] = darkGreen.r;
                        data[i + 1] = darkGreen.g;
                        data[i + 2] = darkGreen.b;
                    }
                } else {
                    if (isBlack) {
                        data[i] = 0;
                        data[i + 1] = 0;
                        data[i + 2] = 0;
                    } else if (isYellow) {
                        data[i] = darkGreen.r;
                        data[i + 1] = darkGreen.g;
                        data[i + 2] = darkGreen.b;
                    }
                }
            }
            const tempData = new Uint8ClampedArray(data);
            for (let y = 1; y < canvas.height - 1; y++) {
                for (let x = 1; x < canvas.width - 1; x++) {
                    const i = (y * canvas.width + x) * 4;
                    if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) {
                        for (let dy = -1; dy <= 1; dy++) {
                            for (let dx = -1; dx <= 1; dx++) {
                                if (dx === 0 && dy === 0) {
                                    continue;
                                }
                                const ni = ((y + dy) * canvas.width + (x + dx)) * 4;
                                if (!(data[ni] === 0 && data[ni + 1] === 0 && data[ni + 2] === 0)) {
                                    tempData[i] = 255;
                                    tempData[i + 1] = 255;
                                    tempData[i + 2] = 255;
                                }
                            }
                        }
                    }
                }
            }
            context.putImageData(new ImageData(tempData, canvas.width, canvas.height), 0, 0);
        };
        image.src = imageUrl;
        return canvas;
    }

    async function fetchPlaystyleChanges(mid, opponentTid) {
        try {
            const res = await fetch(CONSTANTS.MATCH_STATS_URL(mid));
            const txt = await res.text();
            const xml = new DOMParser().parseFromString(txt, 'text/xml');
            const changes = Array.from(xml.querySelectorAll('Events Tactic'))
                .filter(n => n.getAttribute('teamId') === opponentTid)
                .map(n => {
                    const tType = n.getAttribute('type');
                    if (['playstyle', 'aggression', 'tactic'].includes(tType)) {
                        return `Min ${n.getAttribute('time')}: ${tType} → ${n.getAttribute('new_setting')}`;
                    }
                    return null;
                }).filter(Boolean);
            return changes.length ? changes.join('<br>') : 'No relevant changes detected';
        } catch (e) {
            return 'Could not fetch playstyle data.';
        }
    }

    function addPlaystyleHover(mid, canvas, opponentTid) {
        const tooltip = document.createElement('div');
        tooltip.className = 'playstyle-tooltip';
        document.body.appendChild(tooltip);
        canvas.addEventListener('mouseover', async (ev) => {
            tooltip.style.display = 'block';
            tooltip.innerHTML = 'Loading...';
            const info = await fetchPlaystyleChanges(mid, opponentTid);
            tooltip.innerHTML = info;
            tooltip.style.top = `${ev.pageY + 15}px`;
            tooltip.style.left = `${ev.pageX + 5}px`;
        });
        canvas.addEventListener('mousemove', (ev) => {
            tooltip.style.top = `${ev.pageY + 15}px`;
            tooltip.style.left = `${ev.pageX + 5}px`;
        });
        canvas.addEventListener('mouseout', () => {
            tooltip.style.display = 'none';
        });
    }

    function waitForEloValues() {
        const interval = setInterval(() => {
            const elements = document.querySelectorAll(CONSTANTS.SELECTORS.HOME_TEAM);
            if (elements.length > 0 && elements[elements.length - 1]?.innerHTML.includes('br')) {
                clearInterval(interval);
                insertIconsAndListeners();
            }
        }, 100);
        setTimeout(() => clearInterval(interval), 1500);
    }

    function handleClickEvents(e) {
        const clickedMagnifier = e.target.closest('.magnifier-icon');
        if (clickedMagnifier) {
            e.preventDefault();
            e.stopPropagation();
            const tid = clickedMagnifier.dataset.tid;
            const name = clickedMagnifier.dataset.opponent;
            if (!tid) {
                return;
            }
            saveTeam(tid, name);
            showInteractionModal(tid, clickedMagnifier);
            return;
        }
        const interactionModal = document.getElementById('interaction-modal');
        const tacticsContainer = document.getElementById('tactics-container');
        if (interactionModal && !interactionModal.contains(e.target)) {
            fadeOutAndRemove(interactionModal);
        }
        if (tacticsContainer && !tacticsContainer.contains(e.target)) {
            fadeOutAndRemove(tacticsContainer);
        }
    }

    async function initializeUserTeamId() {
        let storedId = GM_getValue(CONSTANTS.STORAGE_KEYS.USER_TEAM_ID, null);
        if (storedId) {
            userTeamId = storedId;
            return;
        }

        try {
            const response = await fetch('https://www.managerzone.com/?p=clubhouse');
            const text = await response.text();
            const match = text.match(/dynimg\/badge\.php\?team_id=(\d+)/);
            if (match && match[1]) {
                userTeamId = match[1];
                GM_setValue(CONSTANTS.STORAGE_KEYS.USER_TEAM_ID, userTeamId);
            }
        } catch (error) {
            console.error('Could not fetch user team ID.', error);
        }
    }


    async function run() {
        await initializeUserTeamId();
        const statsXenteRunning = document.querySelector(CONSTANTS.SELECTORS.STATS_XENTE);
        const eloScheduledSelected = document.querySelector(CONSTANTS.SELECTORS.ELO_SCHEDULED)?.checked;
        if (statsXenteRunning && eloScheduledSelected) {
            waitForEloValues();
        } else {
            insertIconsAndListeners();
        }
        startObserving();
        document.body.addEventListener('click', handleClickEvents, true);
    }

    run();
})();