// ==UserScript==
// @name ylOppTactsPreview (Modified)
// @namespace douglaskampl
// @version 2.0
// @description Shows latest 10 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
// @license MIT
// ==/UserScript==
GM_addStyle(`
.fade-in {
animation: fade-in 0.2s ease forwards;
}
.fade-out {
animation: fade-out 0.2s ease forwards;
}
@keyframes fade-in {
from {opacity:0; transform:translateY(-5px);}
to {opacity:1; transform:translateY(0);}
}
@keyframes fade-out {
from {opacity:1; transform:translateY(0);}
to {opacity:0; transform:translateY(-5px);}
}
.magnifier-icon {
cursor: pointer !important;
font-size: 14px !important;
margin-left: 5px !important;
z-index: 100 !important;
pointer-events: auto !important;
color: #444;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.magnifier-icon:hover {
transform: scale(1.2);
opacity: 0.8;
}
.tactics-container {
position: absolute;
top: 150px;
left: 50%;
transform: translateX(-50%);
background: #fafafa;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 420px;
max-height: 80vh;
overflow: hidden;
font-family: sans-serif;
color: #333;
font-size: 13px;
display: flex;
flex-direction: column;
z-index: 9999;
}
.tactics-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: #cdd;
border-bottom: 1px solid #ccc;
font-size: 12px;
}
.tactics-header .match-info-text {
margin: 0;
font-weight: normal;
font-size: 12px;
color: #333;
}
.tactics-header .close-button {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: #333;
line-height: 1;
padding: 0;
margin: 0;
transition: transform 0.2s ease, color 0.2s ease;
}
.tactics-header .close-button:hover {
transform: scale(1.1);
color: #000;
}
.tactics-header .title-main {
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.tactics-header .title-subtitle {
font-size: 11px;
color: #666;
font-style: italic;
}
.tactics-list {
padding: 10px;
overflow-y: auto;
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
align-content: flex-start;
background: #fff;
}
.tactic-item {
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
text-align: center;
padding: 6px;
font-size: 11px;
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.tactic-item:hover {
transform: scale(1.03);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.tactic-item p {
margin: 5px 0 0 0;
color: #333;
}
.tactics-container canvas {
border-radius: 4px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
margin-bottom: 5px;
background: #f9f9f9;
border: 1px solid #ddd;
}
.tactics-container canvas:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
#match-type-modal {
position: absolute;
top: 180px;
left: 50%;
transform: translateX(-50%);
background: #fafafa;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 15px;
font-family: sans-serif;
font-size: 13px;
color: #333;
z-index: 10000;
width: 220px;
}
#match-type-modal label {
display: block;
margin-bottom: 8px;
font-weight: bold;
font-size: 13px;
}
#match-type-modal select {
padding: 5px;
font-size: 13px;
width: 100%;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
}
#match-type-modal .btn-group {
display: flex;
gap: 8px;
justify-content: flex-end;
}
#match-type-modal button {
padding: 5px 12px;
font-size: 12px;
cursor: pointer;
background: #e0e0e0;
border: 1px solid #aaa;
border-radius: 4px;
transition: background 0.2s ease;
}
#match-type-modal button:hover {
background: #d0d0d0;
}
`);
(function () {
"use strict";
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']
};
let ourTeamName = null;
let selectedMatchTypeG = '';
let currentTidValue = '';
let currentOpponent = '';
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) {
selectedMatchTypeG = matchType;
currentTidValue = tidValue;
currentOpponent = opponent;
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 (_error) {}
}
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. Your opponent clearly doesn't care.";
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);
listWrapper.appendChild(item);
});
container.classList.add('fade-in');
}
function showMatchTypeModal(tidValue, opponent, event) {
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);
};
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 = `
<div class="title-main">${opponent ? opponent : ''} – ${matchType.toUpperCase()}</div>
<div class="title-subtitle">
(Opponent's tactics are represented by black dots with white outlines)
<span style="display:inline-block;width:8px;height:8px;background:#000;border:1px solid #fff;margin-left:5px;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 + 5;
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() {
if (!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 = '🔍';
select.insertAdjacentElement('afterend', icon);
}
}
});
}
function extractMidFromUrl(url) {
return new URLSearchParams(new URL(url, location.href).search).get('mid');
}
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 = function () {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
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 ? isYellow : isBlack) {
data[i] = 0;
data[i+1] = 0;
data[i+2] = 0;
} else if (opponentIsHome ? isBlack : isYellow) {
const nearbyGreen = context.getImageData(Math.floor(i/4) % canvas.width, Math.floor(i/4/canvas.width), 1, 1).data;
data[i] = nearbyGreen[0];
data[i+1] = nearbyGreen[1];
data[i+2] = nearbyGreen[2];
}
}
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);
};
image.src = imageUrl;
return canvas;
}
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 initialize() {
const statsXenteRunning = document.querySelector(CONSTANTS.SELECTORS.STATS_XENTE);
const eloScheduledSelected = document.querySelector(CONSTANTS.SELECTORS.ELO_SCHEDULED)?.checked;
statsXenteRunning && eloScheduledSelected ? waitForEloValues() : insertIconsAndListeners();
startObserving();
}
setTimeout(initialize, 500);
document.body.addEventListener('click', (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(tidValue, opponent, e);
} else {
const tacticsContainer = document.getElementById('tactics-container');
const matchTypeModal = document.getElementById('match-type-modal');
if (tacticsContainer && !tacticsContainer.contains(e.target) && !e.target.classList.contains('magnifier-icon')) {
fadeOutAndRemove(tacticsContainer);
}
if (matchTypeModal && !matchTypeModal.contains(e.target)) {
fadeOutAndRemove(matchTypeModal);
}
}
});
})();