Greasy Fork

MZ - Training History

Fetches player training history and counts skills gained across seasons

目前为 2024-12-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         MZ - Training History
// @namespace    douglaskampl
// @version      2.2
// @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 dm = dateNode.textContent.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/);
        if (!dm) return null;

        const currentDate = new Date(`${dm[2]}/${dm[1]}/${dm[3]}`);

        const matches = header.textContent.match(/\d+/g);
        if (!matches || matches.length < 3) return null;

        const season = parseInt(matches[0], 10);
        const day = parseInt(matches[2], 10);

        return { currentDate, season, day };
    }

    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 parseSeriesData(txt) {
        const m = txt.match(/var series = (\[.*?\]);/);
        return m ? JSON.parse(m[1]) : null;
    }

    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 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 csi = getCurrentSeasonInfo();
    if (!csi) return;
    const getSeasonFn = getSeasonCalculator(csi);
    const container = document.getElementById('players_container');
    if (container) {
        insertButtons(getSeasonFn, csi.currentDate, csi.season);
        const obs = new MutationObserver(() => insertButtons(getSeasonFn, csi.currentDate, csi.season));
        obs.observe(container, { childList: true, subtree: true });
    }
})();