// ==UserScript== // @name MZ - Training History // @namespace douglaskampl // @version 2.1 // @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 // @downloadURL none // ==/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 = `

Gains for ${playerName}

`; 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 += `

Season ${seasonNum} (Age ${approximateAge}) – Balls: ${items.length}

`; }); html += `
` html += `

Total balls earned across all seasons: ${total}

`; const finalSkills = Object.entries(skillTotals) .filter(([_, count]) => count > 0) .map(([skill, count]) => `${skill} (${count})`) .join(', '); html += `

${finalSkills}

`; 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 = ''; 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 }); } })();