// ==UserScript== // @name MZ - Player Ratings from MZLive // @namespace douglaskampl // @version 1.7 // @description Displays player ratings on transfer and player pages // @author Douglas // @match https://www.managerzone.com/?p=transfer* // @match https://www.managerzone.com/?p=players* // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @grant GM_addStyle // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.cloud/scripts/528082/MZ%20-%20Player%20Ratings%20from%20MZLive.user.js // @updateURL https://update.greasyfork.cloud/scripts/528082/MZ%20-%20Player%20Ratings%20from%20MZLive.meta.js // ==/UserScript== (function () { 'use strict'; const RATINGS = { "SPEED": { "K": 0.09, "D": 0.25, "A": 0.25, "M": 0.15, "W": 0.25, "F": 0.23 }, "STAMINA": { "K": 0.09, "D": 0.16, "A": 0.18, "M": 0.15, "W": 0.20, "F": 0.15 }, "PLAYINT": { "K": 0.09, "D": 0.07, "A": 0.05, "M": 0.10, "W": 0.06, "F": 0.05 }, "PASSING": { "K": 0.02, "D": 0.02, "A": 0.05, "M": 0.15, "W": 0.04, "F": 0.04 }, "SHOOTING": { "K": 0.00, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.05, "F": 0.28 }, "HEADING": { "K": 0.00, "D": 0.00, "A": 0.02, "M": 0.00, "W": 0.00, "F": 0.03 }, "GOALKEEPING": { "K": 0.55, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.00, "F": 0.00 }, "BALLCONTROL": { "K": 0.09, "D": 0.08, "A": 0.10, "M": 0.12, "W": 0.15, "F": 0.15 }, "TACKLING": { "K": 0.00, "D": 0.30, "A": 0.25, "M": 0.20, "W": 0.05, "F": 0.02 }, "CROSSING": { "K": 0.02, "D": 0.07, "A": 0.05, "M": 0.08, "W": 0.15, "F": 0.00 }, "SETPLAYS": { "K": 0.00, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.00, "F": 0.00 }, "EXPERIENCE": { "K": 0.05, "D": 0.05, "A": 0.05, "M": 0.05, "W": 0.05, "F": 0.05 } }; const SKILLS = [ "SPEED", "STAMINA", "PLAYINT", "PASSING", "SHOOTING", "HEADING", "GOALKEEPING", "BALLCONTROL", "TACKLING", "CROSSING", "SETPLAYS", "EXPERIENCE", ]; function calculateRatings(skills) { const player = { K: 0, D: 0, A: 0, M: 0, W: 0, F: 0, B: 0, top: 0 }; SKILLS.forEach(skillName => { if (!skills[skillName] || !RATINGS[skillName]) return; const value = parseInt(skills[skillName], 10); if (isNaN(value)) return; if (skillName !== "EXPERIENCE") { player.B += value; } Object.keys(player).forEach(pos => { if (pos !== 'B' && pos !== 'top') { const weight = RATINGS[skillName][pos]; if (typeof weight === 'number') { player[pos] += value * weight; if (player[pos] > player.top) { player.top = player[pos]; } } } }); }); return { K: player.K.toFixed(2), D: player.D.toFixed(2), A: player.A.toFixed(2), M: player.M.toFixed(2), W: player.W.toFixed(2), F: player.F.toFixed(2), B: player.B, top: player.top.toFixed(2) }; } function extractSkillsFromTable(skillsTable) { const skills = {}; if (!skillsTable) return skills; const skillRows = skillsTable.querySelectorAll('tbody > tr'); skillRows.forEach((row, index) => { if (index >= SKILLS.length) return; const valueElem = row.querySelector('td.skillval > span'); if (valueElem) { const skillType = SKILLS[index]; const value = valueElem.textContent.trim().replace(/[()]/g, ''); if (skillType && value !== null && value !== '' && !isNaN(parseInt(value, 10))) { skills[skillType] = value; } } }); return skills; } function extractPlayerSkillsDirectly(playerElement) { const skillsTable = playerElement.querySelector('.player_skills'); if (skillsTable) { return extractSkillsFromTable(skillsTable); } return {}; } function decodeHtmlEntities(text) { if (!text) return ''; const textarea = document.createElement('textarea'); textarea.innerHTML = text; return textarea.value; } function fetchSkillsFromTransfer(playerId) { return new Promise((resolve, reject) => { const url = `https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=${playerId}&nationality=all_nationalities&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=19&ageb=37&birth_season_low=56&birth_season_high=74&tot_low=0&tot_high=110&s0a=0&s0b=10&s1a=0&s1b=10&s2a=0&s2b=10&s3a=0&s3b=10&s4a=0&s4b=10&s5a=0&s5b=10&s6a=0&s6b=10&s7a=0&s7b=10&s8a=0&s8b=10&s9a=0&s9b=10&s10a=0&s10b=10&s11a=0&s11b=10&s12a=0&s12b=10&o=0`; fetch(url, { credentials: 'include' }) .then(response => { if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } return response.json(); }) .then(data => { if (data && data.players) { try { const decodedHtml = decodeHtmlEntities(data.players); const parser = new DOMParser(); const ajaxDoc = parser.parseFromString(decodedHtml, 'text/html'); const skillsTable = ajaxDoc.querySelector('.player_skills'); if (skillsTable) { const skills = extractSkillsFromTable(skillsTable); if (Object.keys(skills).length > 0) { resolve(skills); } else { reject("Could not extract skills from the AJAX response table."); } } else { reject("Skills table not found in AJAX response."); } } catch (e) { console.error("Error parsing AJAX response:", e); reject("Error parsing AJAX response: " + e.message); } } else { reject("No player data found in AJAX response."); } }) .catch(error => { console.error("Error during fetch request:", error); reject("Error during fetch request: " + error.message); }); }); } function createRatingDisplay(ratingsData) { const positions = [ { code: 'K', name: 'Goalkeeper', value: ratingsData.K }, { code: 'D', name: 'Defender', value: ratingsData.D }, { code: 'A', name: 'Anchorman', value: ratingsData.A }, { code: 'M', name: 'Midfielder', value: ratingsData.M }, { code: 'W', name: 'Winger', value: ratingsData.W }, { code: 'F', name: 'Forward', value: ratingsData.F } ]; const container = document.createElement('div'); container.className = 'mz-rating-container'; const ratingsList = document.createElement('div'); ratingsList.className = 'mz-rating-list'; positions.forEach(pos => { const row = document.createElement('div'); row.className = 'mz-rating-row'; const isTop = pos.value === ratingsData.top; const posName = document.createElement('span'); posName.className = 'mz-pos-name' + (isTop ? ' mz-pos-top' : ''); posName.textContent = pos.name + ':'; const posValue = document.createElement('span'); posValue.className = 'mz-pos-value' + (isTop ? ' mz-pos-top' : ''); posValue.textContent = pos.value; row.appendChild(posName); row.appendChild(posValue); ratingsList.appendChild(row); }); container.appendChild(ratingsList); const infoRow = document.createElement('div'); infoRow.className = 'mz-rating-info-row'; infoRow.innerHTML = `Total Balls: ${ratingsData.B} Top: ${ratingsData.top}`; container.appendChild(infoRow); return container; } function shouldAddButton(playerElement) { const skillsTable = playerElement.querySelector('.player_skills'); if (skillsTable && skillsTable.querySelector('tbody > tr > td.skillval > span')) { return true; } const isSinglePlayerPage = window.location.search.includes('pid='); const isOnTransferMarket = playerElement.querySelector('a[href*="p=transfer&sub=players&u="]'); if (isSinglePlayerPage && isOnTransferMarket) { return true; } return false; } function addRatingButton(playerElement) { const idElement = playerElement.querySelector('.player_id_span'); if (!idElement) { return; } const playerId = idElement.textContent.trim(); if (!playerId) { return; } if (!shouldAddButton(playerElement)) { return; } if (idElement.parentNode.querySelector('.mz-rating-btn')) { return; } const btn = document.createElement('button'); btn.className = 'mz-rating-btn'; btn.innerHTML = ''; btn.title = 'Show player ratings'; btn.dataset.playerId = playerId; let ratingContainer = null; let isVisible = false; let isLoading = false; btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (isLoading) return; if (isVisible && ratingContainer) { ratingContainer.classList.remove('mz-rating-visible'); setTimeout(() => { if (ratingContainer && ratingContainer.parentNode) { ratingContainer.parentNode.removeChild(ratingContainer); } ratingContainer = null; }, 300); isVisible = false; btn.innerHTML = ''; btn.title = 'Show player ratings'; return; } isLoading = true; btn.innerHTML = ''; btn.title = 'Loading ratings...'; let skills = {}; try { skills = extractPlayerSkillsDirectly(playerElement); if (Object.keys(skills).length === 0) { const isSinglePlayerPage = window.location.search.includes('pid='); const isOnTransferMarket = playerElement.querySelector('a[href*="p=transfer&sub=players&u="]'); if (isSinglePlayerPage && isOnTransferMarket) { skills = await fetchSkillsFromTransfer(playerId); } } if (Object.keys(skills).length > 0) { const ratingsData = calculateRatings(skills); ratingContainer = createRatingDisplay(ratingsData); const playerHeader = playerElement.querySelector('.subheader'); const targetElement = playerHeader ? playerHeader.nextSibling : playerElement.firstChild; playerHeader.parentNode.insertBefore(ratingContainer, targetElement); requestAnimationFrame(() => { requestAnimationFrame(() => { if (ratingContainer) ratingContainer.classList.add('mz-rating-visible'); }); }); isVisible = true; btn.innerHTML = ''; btn.title = 'Hide player ratings'; } else { btn.innerHTML = ''; btn.title = 'Could not retrieve skills'; setTimeout(() => { if (!isVisible) { btn.innerHTML = ''; btn.title = 'Show player ratings'; } }, 2000); } } catch (error) { console.error(`Error getting ratings for player ${playerId}:`, error); btn.innerHTML = ''; btn.title = `Error: ${error.message || error}`; setTimeout(() => { if (!isVisible) { btn.innerHTML = ''; btn.title = 'Show player ratings'; } }, 3000); } finally { isLoading = false; if (!isVisible && !btn.innerHTML.includes('fa-triangle-exclamation')) { btn.innerHTML = ''; btn.title = 'Show player ratings'; } } }); idElement.parentNode.insertBefore(btn, idElement.nextSibling); } function processPlayerElements() { const playerContainers = document.querySelectorAll('div[id^="thePlayers_"]'); playerContainers.forEach(container => { try { addRatingButton(container); } catch (e) { console.error("Error processing player container:", container, e); } }); } function setUpObserver() { const targetNode = document.getElementById('players_container') || document.querySelector('.mainContent') || document.body; if (!targetNode) { console.error("MZ Ratings: Could not find a suitable node to observe for mutations."); return null; } const observer = new MutationObserver((mutations) => { let needsProcessing = false; mutations.forEach(mutation => { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if ((node.id && node.id.startsWith('thePlayers_')) || (node.querySelector && node.querySelector('div[id^="thePlayers_"]'))) { needsProcessing = true; break; } } } } if(needsProcessing) return; }); if (needsProcessing) { setTimeout(processPlayerElements, 200); } }); observer.observe(targetNode, { childList: true, subtree: true }); return observer; } function addStyles() { GM_addStyle( `.mz-rating-btn { display: inline-flex; align-items: center; justify-content: center; margin-left: 8px; width: 20px; height: 20px; border: none; border-radius: 50%; background: #1a73e8; color: white; cursor: pointer; font-size: 12px; line-height: 1; vertical-align: middle; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.15); padding: 0; } .mz-rating-btn:hover { background: #0d5bbb; transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .mz-rating-btn > i { font-size: 12px; line-height: 1; vertical-align: baseline; } .mz-rating-container { margin: 10px 0 5px 5px; padding: 10px 12px; background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); width: fit-content; opacity: 0; max-height: 0; overflow: hidden; transform: translateY(-10px); transition: all 0.3s ease-out; } .mz-rating-visible { opacity: 1; max-height: 500px; transform: translateY(0); margin-bottom: 10px; } .mz-rating-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 5px 10px; margin-bottom: 8px; } .mz-rating-row { display: flex; justify-content: space-between; align-items: center; padding: 2px 0px; font-size: 12px; } .mz-pos-name { color: #444; margin-right: 5px; } .mz-pos-value { font-weight: bold; color: #222; font-family: monospace; } .mz-pos-top { color: #1a73e8; font-weight: bold; } .mz-rating-info-row { margin-top: 8px; padding-top: 6px; border-top: 1px solid #e0e0e0; font-size: 11px; color: #555; display: flex; justify-content: space-between; } .mz-rating-info-row strong { color: #111; font-weight: 600; }` ); } function init() { addStyles(); processPlayerElements(); setUpObserver(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(init, 300); } else { window.addEventListener('DOMContentLoaded', () => setTimeout(init, 300)); } })();