// ==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();
})();