Greasy Fork is available in English.

ylOppTactsPreview (Modified)

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

// ==UserScript==
// @name         ylOppTactsPreview (Modified)
// @namespace    douglaskampl
// @version      4.4
// @description  Shows the latest tactics used by an 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
// @require      https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js
// @resource     tactsPreviewStyles https://u18mz.vercel.app/mz/userscript/other/tactsPreview.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('tactsPreviewStyles'));

    const CONSTANTS = {
        MAX_OPPONENT_TACTICS: 10,
        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'
        },
        MATCH_TYPES: ['u18', 'u21', 'u23', 'no_restriction'],
        MATCH_STATS_URL: (matchId) => 'https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=' + matchId + '&sport=soccer'
    };

    let ourTeamName = null;
    let selectedMatchTypeG = '';
    let currentTidValue = '';
    let currentOpponent = '';
    let currentOpponentTid = '';
    let lastMagnifierRect = null;

    let spinnerInstance = null;

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

    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';
        spinnerContainer.style.position = 'fixed';
        spinnerContainer.style.top = '0';
        spinnerContainer.style.left = '0';
        spinnerContainer.style.width = '100vw';
        spinnerContainer.style.height = '100vh';
        spinnerContainer.style.background = 'rgba(0, 0, 0, 0.4)';
        spinnerContainer.style.zIndex = '999999';
        document.body.appendChild(spinnerContainer);

        const opts = {
            lines: 12,
            length: 16,
            width: 6,
            radius: 20,
            scale: 1,
            corners: 1,
            color: '#FFC0CB',
            opacity: 0.25,
            rotate: 0,
            direction: 1,
            speed: 1,
            trail: 60,
            fps: 20,
            zIndex: 2e9,
            className: 'spinner',
            top: '50%',
            left: '50%',
            shadow: false,
            hwaccel: false,
            position: 'absolute'
        };

        spinnerInstance = new Spinner(opts).spin(spinnerContainer);
    }

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

    async function fetchLatestTactics(tidValue, opponent, matchType, opponentTid) {
        selectedMatchTypeG = matchType;
        currentTidValue = tidValue;
        currentOpponent = opponent;
        currentOpponentTid = opponentTid;
        try {
            showLoadingSpinner();

            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=' + tidValue + '&offset=&selectType=' + matchType + '&limit=default',
                    credentials: 'include'
                }
            );

            if (!response.ok) throw new Error('Network response was not ok');
            const data = await response.json();
            processTacticsData(data);
        } catch (_) {
        } finally {
            hideLoadingSpinner();
        }
    }

    function processTacticsData(data) {
        const parser = new DOMParser();
        const htmlDocument = parser.parseFromString(data.list, 'text/html');
        const scoreShownLinks = htmlDocument.querySelectorAll('a.score-shown');
        const container = createTacticsContainer(selectedMatchTypeG, currentOpponent);
        document.body.appendChild(container);
        const listWrapper = container.querySelector('.tactics-list');
        if (scoreShownLinks.length === 0) {
            const message = document.createElement('div');
            message.style.textAlign = 'center';
            message.style.color = '#555';
            message.style.fontSize = '12px';
            message.style.padding = '10px';
            message.textContent = 'No recent tactics found for the selected match type.';
            listWrapper.appendChild(message);
            container.classList.add('fade-in');
            return;
        }
        scoreShownLinks.forEach((link, index) => {
            if (index >= CONSTANTS.MAX_OPPONENT_TACTICS) return;
            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 homeTeamLink = dl.querySelector('.home-team-column a.clippable');
            const awayTeamLink = dl.querySelector('.away-team-column a.clippable');
            let homeTid = null, awayTid = null;
            if (homeTeamLink) {
                homeTid = new URLSearchParams(new URL(homeTeamLink.href, location.href).search).get('tid');
            }
            if (awayTeamLink) {
                awayTid = new URLSearchParams(new URL(awayTeamLink.href, location.href).search).get('tid');
            }
            let homeGoals = 0;
            let awayGoals = 0;
            if (theScore.includes('-')) {
                const parts = theScore.split('-').map(x => x.trim());
                if (parts.length === 2) {
                    homeGoals = parseInt(parts[0]) || 0;
                    awayGoals = parseInt(parts[1]) || 0;
                }
            }
            const mid = extractMidFromUrl(link.href);
            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 opponentIsHome = (homeTid === currentTidValue);
            const canvas = createCanvasWithReplacedColors(tacticUrl, opponentIsHome);
            const item = document.createElement('div');
            item.className = 'tactic-item';
            let opponentGoals = opponentIsHome ? homeGoals : awayGoals;
            let otherGoals = opponentIsHome ? awayGoals : homeGoals;
            if (opponentGoals > otherGoals) {
                item.style.backgroundColor = '#daf8da';
            } else if (opponentGoals < otherGoals) {
                item.style.backgroundColor = '#f8dada';
            } else {
                item.style.backgroundColor = '#f0f0f0';
            }
            const linkA = document.createElement('a');
            linkA.href = resultUrl;
            linkA.target = '_blank';
            linkA.className = 'tactic-link';
            linkA.style.color = '#333';
            linkA.style.textDecoration = 'none';
            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);
        });
        container.classList.add('fade-in');
    }

    function showMatchTypeModal(tidValue, opponent, event, opponentTid) {
        const existingModal = document.getElementById('match-type-modal');
        if (existingModal) {
            fadeOutAndRemove(existingModal);
        }
        const modal = document.createElement('div');
        modal.id = 'match-type-modal';
        modal.classList.add('fade-in');
        const label = document.createElement('label');
        label.textContent = 'Select match type:';
        modal.appendChild(label);
        const select = document.createElement('select');
        CONSTANTS.MATCH_TYPES.forEach(type => {
            const option = document.createElement('option');
            option.value = type;
            let labelText;
            if (type === 'no_restriction') {
                labelText = 'Senior';
            } else {
                labelText = type.replace('_', ' ').toUpperCase();
            }
            option.textContent = labelText;
            select.appendChild(option);
        });
        modal.appendChild(select);
        const btnGroup = document.createElement('div');
        btnGroup.className = 'btn-group';
        const okButton = document.createElement('button');
        okButton.textContent = 'OK';
        okButton.onclick = () => {
            fadeOutAndRemove(modal);
            fetchLatestTactics(tidValue, opponent, select.value, opponentTid);
        };
        const cancelButton = document.createElement('button');
        cancelButton.textContent = 'Cancel';
        cancelButton.onclick = () => fadeOutAndRemove(modal);
        btnGroup.append(okButton, cancelButton);
        modal.appendChild(btnGroup);
        document.body.appendChild(modal);
        const rect = event.target.getBoundingClientRect();
        lastMagnifierRect = {
            left: window.scrollX + rect.left,
            top: window.scrollY + rect.top,
            bottom: window.scrollY + rect.bottom,
            width: rect.width,
            height: rect.height
        };
        modal.style.position = 'absolute';
        modal.style.top = (lastMagnifierRect.bottom + 5) + 'px';
        modal.style.left = lastMagnifierRect.left + 'px';
    }

    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';
        const modalTitleMatchType = matchType === 'no_restriction' ? 'Senior' : matchType.replace('_', ' ').toUpperCase();
        title.innerHTML = '<div class="title-main">' + (opponent ? opponent : '') + ' (' + modalTitleMatchType + ')</div><div class="title-subtitle">' + opponent + '\'s tactics are represented by black dots with white outlines <span style="display:inline-block;width:6px;height:6px;background:#000;border:1px solid #fff;margin-left:2px;vertical-align:middle;"></span></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);
        document.body.appendChild(container);
        if (lastMagnifierRect) {
            const modalWidth = 420;
            const leftPos = lastMagnifierRect.left + (lastMagnifierRect.width / 2) - (modalWidth / 2);
            const topPos = lastMagnifierRect.bottom - 350;
            container.style.position = 'absolute';
            container.style.top = topPos + 'px';
            container.style.left = leftPos + 'px';
            container.style.transform = 'none';
        }
        return container;
    }

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

    function identifyUserTeamName() {
        const ddRows = document.querySelectorAll('dd.odd');
        const countMap = new Map();
        let totalMatches = 0;
        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 && awayName) {
                totalMatches++;
                countMap.set(homeName, (countMap.get(homeName) || 0) + 1);
                countMap.set(awayName, (countMap.get(awayName) || 0) + 1);
            }
        });
        for (const [name, count] of countMap.entries()) {
            if (count === totalMatches) {
                return name;
            }
        }
        return null;
    }

    function insertIconsAndListeners() {
        ourTeamName = ourTeamName || identifyUserTeamName();
        if (!ourTeamName) return;
        document.querySelectorAll('dd.odd').forEach(dd => {
            const selectWrapper = dd.querySelector(CONSTANTS.SELECTORS.SELECT_WRAPPER);
            if (selectWrapper) {
                const select = selectWrapper.querySelector('select');
                if (select && !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();
                    let opponentName = null;
                    let opponentTid = null;
                    const homeTeamLink = dd.querySelector('.home-team-column a.clippable');
                    const awayTeamLink = dd.querySelector('.away-team-column a.clippable');
                    let homeTid = null, awayTid = null;
                    if (homeTeamLink) {
                        homeTid = new URLSearchParams(new URL(homeTeamLink.href, location.href).search).get('tid');
                    }
                    if (awayTeamLink) {
                        awayTid = new URLSearchParams(new URL(awayTeamLink.href, location.href).search).get('tid');
                    }
                    if (homeTeamName === ourTeamName && awayTeamName && awayTid) {
                        opponentName = awayTeamName;
                        opponentTid = awayTid;
                    } else if (awayTeamName === ourTeamName && homeTeamName && homeTid) {
                        opponentName = homeTeamName;
                        opponentTid = homeTid;
                    } else {
                        return;
                    }
                    if (!opponentTid) return;
                    const icon = document.createElement('span');
                    icon.className = 'magnifier-icon';
                    icon.dataset.tid = opponentTid;
                    icon.dataset.opponent = opponentName;
                    icon.textContent = '🔍';
                    icon.title = 'Click to check latest tactics for this opponent';
                    select.insertAdjacentElement('afterend', icon);
                }
            }
        });
    }

    function extractMidFromUrl(url) {
        return new URLSearchParams(new URL(url, location.href).search).get('mid');
    }

    function processImage(context, canvas, image, opponentIsHome) {
        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];
            const g = data[i + 1];
            const 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 = 0; y < canvas.height; y++) {
            for (let x = 0; x < canvas.width; 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 nx = x + dx;
                            const ny = y + dy;
                            if (nx >= 0 && nx < canvas.width && ny >= 0 && ny < canvas.height) {
                                const ni = (ny * canvas.width + nx) * 4;
                                if (!(data[ni] === 0 && data[ni + 1] === 0 && data[ni + 2] === 0)) {
                                    tempData[ni] = 255; tempData[ni + 1] = 255; tempData[ni + 2] = 255;
                                }
                            }
                        }
                    }
                }
            }
        }
        context.putImageData(new ImageData(tempData, canvas.width, canvas.height), 0, 0);
    }

    function createCanvas(width, height) {
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        canvas.style.pointerEvents = 'auto';
        return canvas;
    }

    function createCanvasWithReplacedColors(imageUrl, opponentIsHome) {
        const canvas = createCanvas(150, 200);
        const context = canvas.getContext('2d');
        const image = new Image();
        image.crossOrigin = 'Anonymous';
        image.onload = () => processImage(context, canvas, image, opponentIsHome);
        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 parser = new DOMParser();
            const xml = parser.parseFromString(txt, 'text/xml');
            const tactics = xml.querySelectorAll('Events Tactic');
            const out = [];
            tactics.forEach(n => {
                if (n.getAttribute('teamId') !== opponentTid) {
                    return;
                }
                const tType = n.getAttribute('type');
                if (tType === 'playstyle' || tType === 'aggression' || tType === 'tactic') {
                    const time = n.getAttribute('time');
                    const setting = n.getAttribute('new_setting');
                    out.push('Minute ' + time + ': ' + tType + ' -> ' + setting);
                }
            });
            return out.length ? out.join('<br>') : 'No playstyle, mentality or pressing changes detected';
        } catch (_) {
            return 'No info';
        }
    }

    function addPlaystyleHover(mid, canvas, opponentTid) {
        const tooltip = document.createElement('div');
        tooltip.style.position = 'absolute';
        tooltip.style.background = '#333';
        tooltip.style.color = '#fff';
        tooltip.style.padding = '5px';
        tooltip.style.borderRadius = '3px';
        tooltip.style.fontSize = '12px';
        tooltip.style.display = 'none';
        tooltip.style.zIndex = '9999';
        document.body.appendChild(tooltip);

        canvas.addEventListener('mouseover', async (ev) => {
            tooltip.style.display = 'block';
            tooltip.style.top = ev.pageY + 15 + 'px';
            tooltip.style.left = ev.pageX + 5 + 'px';
            tooltip.innerHTML = 'Loading...';
            const info = await fetchPlaystyleChanges(mid, opponentTid);
            tooltip.innerHTML = info;
        });
        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);
            insertIconsAndListeners();
        }, 1500);
    }

    function handleClickEvents(e) {
        if (e.target?.classList.contains('magnifier-icon')) {
            e.preventDefault();
            e.stopPropagation();
            const tidValue = e.target.dataset.tid;
            const opponent = e.target.dataset.opponent;
            if (!tidValue) return;
            showMatchTypeModal(ourTeamName === opponent ? ourTeamName : tidValue, opponent, e, tidValue);
            return;
        }
        const tacticsContainer = document.getElementById('tactics-container');
        const matchTypeModal = document.getElementById('match-type-modal');
        const isOutsideClick = !e.target.classList.contains('magnifier-icon');
        if (tacticsContainer && !tacticsContainer.contains(e.target) && isOutsideClick) {
            fadeOutAndRemove(tacticsContainer);
        }
        if (matchTypeModal && !matchTypeModal.contains(e.target)) {
            fadeOutAndRemove(matchTypeModal);
        }
    }

    function run() {
        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);
    run();
})();