MZ - Player Ratings from MZLive

Displays player ratings on transfer and player pages

// ==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
// ==/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 = `<span>Total Balls: <strong>${ratingsData.B}</strong></span> <span>Top: <strong>${ratingsData.top}</strong></span>`;
        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 = '<i class="fa-solid fa-calculator"></i>';
        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 = '<i class="fa-solid fa-calculator"></i>';
                btn.title = 'Show player ratings';
                return;
            }

            isLoading = true;
            btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
            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 = '<i class="fa-solid fa-xmark"></i>';
                    btn.title = 'Hide player ratings';
                } else {
                    btn.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i>';
                    btn.title = 'Could not retrieve skills';
                    setTimeout(() => {
                        if (!isVisible) {
                            btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
                            btn.title = 'Show player ratings';
                        }
                    }, 2000);
                }

            } catch (error) {
                console.error(`Error getting ratings for player ${playerId}:`, error);
                btn.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i>';
                btn.title = `Error: ${error.message || error}`;
                setTimeout(() => {
                    if (!isVisible) {
                        btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
                        btn.title = 'Show player ratings';
                    }
                }, 3000);
            } finally {
                isLoading = false;
                if (!isVisible && !btn.innerHTML.includes('fa-triangle-exclamation')) {
                    btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
                    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));
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址