MZ - Training History

Fetches player training history and counts skills gained across seasons

当前为 2024-12-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MZ - Training History
// @namespace    douglaskampl
// @version      2.0
// @description  Fetches player training history and counts skills gained across seasons
// @author       Douglas
// @match        https://www.managerzone.com/?p=players
// @match        https://www.managerzone.com/?p=transfer
// @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     trainingHistoryStyles https://u18mz.vercel.app/mz/userscript/other/vTrainingHistory.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('trainingHistoryStyles'));

    const SKILL_MAP = {
        '1': "Speed",
        '2': "Stamina",
        '3': "Play Intelligence",
        '4': "Passing",
        '5': "Shooting",
        '6': "Heading",
        '7': "Keeping",
        '8': "Ball Control",
        '9': "Tackling",
        '10': "Aerial Passing",
        '11': "Set Plays"
    };

    function getCurrentSeasonInfo() {
        const header = document.querySelector('#header-stats-wrapper h5.flex-grow-1.textCenter.linked');
        const dateNode = document.querySelector('#header-stats-wrapper h5.flex-grow-1.textCenter');
        if (!header || !dateNode) return null;
        const sm = header.textContent.match(/Season\s+(\d+).*Day\s+(\d+)/);
        const dm = dateNode.textContent.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/);
        if (!sm || !dm) return null;
        const s = parseInt(sm[1], 10);
        const d = parseInt(sm[2], 10);
        const c = new Date(`${dm[2]}/${dm[1]}/${dm[3]}`);
        return { season: s, day: d, currentDate: c };
    }

    function createModal(content, showSpinner) {
        const overlay = document.createElement('div');
        overlay.className = 'mz-training-overlay';
        const modal = document.createElement('div');
        modal.className = 'mz-training-modal';
        const body = document.createElement('div');
        body.className = 'mz-training-modal-content';
        const spinnerEl = document.createElement('div');
        spinnerEl.style.height = '60px';
        spinnerEl.style.display = showSpinner ? 'block' : 'none';
        body.appendChild(spinnerEl);
        if (content) body.innerHTML += content;
        const closeBtn = document.createElement('div');
        closeBtn.className = 'mz-training-modal-close';
        closeBtn.innerHTML = '×';
        closeBtn.onclick = () => overlay.remove();
        modal.appendChild(closeBtn);
        modal.appendChild(body);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);
        overlay.addEventListener('click', e => {
            if (e.target === overlay) overlay.remove();
        });
        requestAnimationFrame(() => {
            overlay.classList.add('show');
            modal.classList.add('show');
        });
        let spinnerInstance = null;
        if (showSpinner) {
            spinnerInstance = new Spinner({
                color: '#ffa500',
                lines: 12
            });
            spinnerInstance.spin(spinnerEl);
        }
        return { modal, spinnerEl, spinnerInstance, overlay };
    }

    function parseSeriesData(txt) {
        const m = txt.match(/var series = (\[.*?\]);/);
        return m ? JSON.parse(m[1]) : null;
    }

    function getSeasonCalculator(cs) {
        if (!cs) return () => 0;
        const baseSeason = cs.season;
        const baseDate = cs.currentDate;
        const dayOffset = cs.day;
        const seasonStart = new Date(baseDate);
        seasonStart.setDate(seasonStart.getDate() - (dayOffset - 1));
        return (date) => {
            let s = baseSeason;
            let ref = seasonStart.getTime();
            let diff = Math.floor((date.getTime() - ref) / 86400000);
            while (diff < 0) {
                s--;
                diff += 91;
            }
            while (diff >= 91) {
                s++;
                diff -= 91;
            }
            return s;
        };
    }

    function getAgeForSeason(ageNow, currentSeason, targetSeason) {
        return ageNow - (currentSeason - targetSeason);
    }

    function getPlayerAge(container) {
        const strongs = container.querySelectorAll('strong');
        for (const s of strongs) {
            const val = parseInt(s.textContent.trim(), 10);
            if (val >= 14 && val <= 55) return val;
        }
        return 18;
    }

    function generateReportHTML(playerName, bySeason, total, skillTotals, minAgePerSeason, currentSeason, ageNow) {
        let html = `<h2 class="mz-training-title">Gains for ${playerName}</h2>`;
        const sortedSeasons = Object.keys(bySeason).map(Number).sort((a, b) => a - b);
        sortedSeasons.forEach(seasonNum => {
            const items = bySeason[seasonNum];
            const approximateAge = getAgeForSeason(ageNow, currentSeason, seasonNum);
            html += `<div class="mz-training-season">
                <h3>Season ${seasonNum} (Age ${approximateAge}) – Balls: ${items.length}</h3>
                <ul>`;
            items.forEach(it => {
                html += `<li><strong>${it.dateString}</strong> – ${it.skillName}</li>`;
            });
            html += `</ul></div>`;
        });
        html += `<hr>`
        html += `<h3 class="mz-training-final-summary">Total balls earned across all seasons: ${total}</h3>`;
        const finalSkills = Object.entries(skillTotals)
            .filter(([_, count]) => count > 0)
            .map(([skill, count]) => `${skill} (${count})`)
            .join(', ');
        html += `<h3 class="mz-training-skilltotals">
                    ${finalSkills}
                 </h3>`;
        return html;
    }

    function processTrainingHistory(series, getSeasonFn, currentDate) {
        const bySeason = {};
        const skillTotals = {};
        let total = 0;
        series.forEach(item => {
            item.data.forEach((point, i) => {
                if (point.marker && point.marker.symbol.includes("gained_skill.png") && item.data[i + 1]) {
                    const date = new Date(item.data[i + 1].x);
                    const s = getSeasonFn(date);
                    const sid = item.data[i + 1].y.toString();
                    const skillName = SKILL_MAP[sid] || "Unknown Skill";
                    if (!bySeason[s]) bySeason[s] = [];
                    bySeason[s].push({
                        dateString: date.toDateString(),
                        skillName
                    });
                    if (!skillTotals[skillName]) skillTotals[skillName] = 0;
                    skillTotals[skillName]++;
                    if (skillName !== "Unknown Skill") total++;
                }
            });
        });
        return { bySeason, skillTotals, total };
    }

    function fetchTrainingData(pid, container, getSeasonFn, currentDate, currentSeason) {
        const ageNow = getPlayerAge(container);
        const playerNameEl = container.querySelector('span.player_name');
        const playerName = playerNameEl ? playerNameEl.textContent.trim() : 'Unknown Player';
        const { modal, spinnerEl, spinnerInstance } = createModal('', true);
        fetch(`https://www.managerzone.com/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}`)
            .then(r => r.text())
            .then(t => {
                if (spinnerInstance) spinnerInstance.stop();
                spinnerEl.style.display = 'none';
                const series = parseSeriesData(t);
                if (!series) throw new Error();
                return series;
            })
            .then(series => {
                const result = processTrainingHistory(series, getSeasonFn, currentDate);
                const html = generateReportHTML(
                    playerName,
                    result.bySeason,
                    result.total,
                    result.skillTotals,
                    {},
                    currentSeason,
                    ageNow
                );
                modal.querySelector('.mz-training-modal-content').innerHTML = html;
            })
            .catch(() => {
                if (spinnerInstance) spinnerInstance.stop();
                spinnerEl.style.display = 'none';
                modal.querySelector('.mz-training-modal-content').innerText =
                    'Failed to process the training data.';
            });
    }

    function insertButtons(getSeasonFn, currentDate, currentSeason) {
        const nodes = document.querySelectorAll('.playerContainer .floatRight[id^="player_id_"]');
        nodes.forEach(n => {
            if (n.querySelector('.my-training-btn')) return;
            const span = n.querySelector('.player_id_span');
            if (!span) return;
            const pid = span.textContent.trim();
            const btn = document.createElement('button');
            btn.className = 'my-training-btn button_blue';
            btn.innerHTML = '<i class="fa-solid fa-chart-pyramid"></i>';
            btn.onclick = () => {
                const container = n.closest('.playerContainer');
                fetchTrainingData(pid, container, getSeasonFn, currentDate, currentSeason);
            };
            n.appendChild(btn);
        });
    }

    const cs = getCurrentSeasonInfo();
    if (!cs) return;
    const getSeasonFn = getSeasonCalculator(cs);
    const container = document.getElementById('players_container');
    if (container) {
        insertButtons(getSeasonFn, cs.currentDate, cs.season);
        const obs = new MutationObserver(() => insertButtons(getSeasonFn, cs.currentDate, cs.season));
        obs.observe(container, { childList: true, subtree: true });
    }
})();