// ==UserScript== // @name ylOppTactsPreview (Modified) // @namespace douglaskampl // @version 3.0 // @description Shows the latest tactics used by an opponent from the scheduled matches page // @author Douglas // @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 // @resource tactsPreviewStyles https://u18mz.vercel.app/mz/userscript/other/tactsPreview.css // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function () { "use strict"; GM_addStyle(GM_getResourceText("tactsPreviewStyles")); const CONSTANTS = { MAX_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; const observer = new MutationObserver(() => { insertIconsAndListeners(); }); function startObserving() { const fixturesList = document.querySelector(CONSTANTS.SELECTORS.FIXTURES_LIST); if (fixturesList) { observer.observe(fixturesList, { childList: true, subtree: true }); } } async function fetchLatestTactics(tidValue, opponent, matchType, opponentTid) { selectedMatchTypeG = matchType; currentTidValue = tidValue; currentOpponent = opponent; currentOpponentTid = opponentTid; 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=' + 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 (_) { } } 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. Talvez esse cara seja preguiçoso, sei lá..."; listWrapper.appendChild(message); container.classList.add('fade-in'); return; } scoreShownLinks.forEach((link, index) => { if (index >= CONSTANTS.MAX_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; option.textContent = type.replace('_', ' ').toUpperCase(); 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'; title.innerHTML = '
' + (opponent ? opponent : '') + ' – ' + matchType.toUpperCase() + '
' + opponent + '\'s tactics are represented by black dots with white outlines:
'; 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) { // Invertendo as cores do time do oponente (e.g. se era amarelo, vira preto etc.) 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('
') : 'No playstyle/aggression changes found'; } 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; statsXenteRunning && eloScheduledSelected ? waitForEloValues() : insertIconsAndListeners(); startObserving(); } document.body.addEventListener('click', handleClickEvents); run(); })();