// ==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
// @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}
`;
items.forEach(it => {
html += `- ${it.dateString} – ${it.skillName}
`;
});
html += `
`;
});
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 });
}
})();