// ==UserScript==
// @name MZ - Player Training History
// @namespace douglaskampl
// @version 5.9
// @description Provides the player development/gains across previous MZ seasons
// @author Douglas
// @match https://www.managerzone.com/?p=players
// @match https://www.managerzone.com/?p=players&pid=*
// @match https://www.managerzone.com/?p=players&tid=*
// @match https://www.managerzone.com/?p=transfer*
// @exclude https://www.managerzone.com/?p=transfer_history*
// @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @require https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js
// @resource trainingHistoryStyles https://br18.org/mz/userscript/other/trainingHistoryNew.css
// @resource skillTranslationsJS https://br18.org/16translations.js
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
'use strict';
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' };
const ORDERED_SKILLS = ['Speed', 'Stamina', 'Play Intelligence', 'Passing', 'Shooting', 'Heading', 'Keeping', 'Ball Control', 'Tackling', 'Aerial Passing', 'Set Plays'];
const CURRENCIES = {
"R$": 2.62589, EUR: 9.1775, USD: 7.4234, "点": 1,
SEK: 1, NOK: 1.07245, DKK: 1.23522, GBP: 13.35247,
CHF: 5.86737, RUB: 0.26313, CAD: 5.70899, AUD: 5.66999,
MZ: 1, MM: 1, PLN: 1.95278, ILS: 1.6953,
INR: 0.17, THB: 0.17079, ZAR: 1.23733, SKK: 0.24946,
BGN: 4.70738, MXN: 0.68576, ARS: 2.64445, BOB: 0.939,
UYU: 0.256963, PYG: 0.001309, ISK: 0.10433, SIT: 0.03896,
JPY: 0.06,
};
let skillTranslations = {};
let insensitiveSkillTranslations = {};
let myTeamId = null;
let preferredCurrency = GM_getValue('PREFERRED_CURRENCY', 'USD');
try {
GM_addStyle(GM_getResourceText('trainingHistoryStyles'));
const translationsCode = GM_getResourceText('skillTranslationsJS');
if (!translationsCode) {
throw new Error("Could not load translations resource.");
}
const codeToRun = translationsCode.replace(/^export\s+/gm, '');
const getTranslations = new Function(`${codeToRun}; return { SKILL_TRANSLATIONS, insensitiveSkillTranslations };`);
const loadedTranslations = getTranslations();
skillTranslations = loadedTranslations.SKILL_TRANSLATIONS || {};
insensitiveSkillTranslations = loadedTranslations.insensitiveSkillTranslations || {};
if (Object.keys(skillTranslations).length === 0) {
throw new Error("Translations object loaded but is empty.");
}
if (Object.keys(insensitiveSkillTranslations).length === 0) {
if(Object.keys(skillTranslations).length > 0) {
insensitiveSkillTranslations = Object.fromEntries(
Object.entries(skillTranslations).map(([key, value]) => [key.toLowerCase(), value])
);
} else {
throw new Error("Could not create insensitive translations map.");
}
}
} catch (_err) {
return;
}
const getEnglishSkillName = (nativeName) => {
if (!nativeName) return '';
return insensitiveSkillTranslations[nativeName.toLowerCase()] || nativeName;
};
const isClubMember = () => {
const headerUsernameStyle = document.querySelector('#header-username')?.getAttribute('style');
return headerUsernameStyle && headerUsernameStyle.includes('background-image');
};
const canRunUserscript = () => isClubMember();
const getCurrentSeasonInfo = () => {
const w = document.querySelector('#header-stats-wrapper');
if (!w) return null;
const dn = w.querySelector('h5.flex-grow-1.textCenter:not(.linked)');
const ln = w.querySelector('h5.flex-grow-1.textCenter.linked');
if (!dn || !ln) return null;
const dm = dn.textContent.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
if (!dm) return null;
const d = dm[1];
const m = dm[2];
const y = dm[3];
const currentDate = new Date([m, d, y].join('/'));
const digits = ln.textContent.match(/\d+/g);
if (!digits || digits.length < 3) return null;
const season = parseInt(digits[0], 10);
const day = parseInt(digits[2], 10);
return { currentDate, season, day };
};
const 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));
return d => {
if (!(d instanceof Date)) return 0;
let s = baseSeason;
let ref = seasonStart.getTime();
let diff = Math.floor((d.getTime() - ref) / 86400000);
while (diff < 0) {
s--;
diff += 91;
}
while (diff >= 91) {
s++;
diff -= 91;
}
return s;
};
};
const calculateHistoricalAge = ({ currentAge, currentSeason, targetSeason }) => {
if (!currentAge) return null;
const seasonDiff = currentSeason - targetSeason;
return currentAge - seasonDiff;
};
const getPlayerContainerNode = (n) => {
let c = n.closest('.playerContainer');
if (!c) c = document.querySelector('.playerContainer');
return c;
};
const hasVisibleSkills = (container) => container.querySelector('table.player_skills') !== null;
const parsePlayerAge = (container) => {
const strongEls = container.querySelectorAll('strong');
for (const el of strongEls) {
const numberMatch = el.textContent.trim().match(/^(\d{1,2})$/);
if (numberMatch) {
const age = parseInt(numberMatch[1], 10);
if (age >= 15 && age <= 45) return age;
}
}
const allNums = container.textContent.match(/\b(\d{1,2})\b/g);
if (allNums) {
for (const numString of allNums) {
const age = parseInt(numString, 10);
if (age >= 15 && age <= 45) return age;
}
}
return null;
};
const parsePriceString = (priceStr) => {
if (!priceStr || priceStr === 'N/A' || priceStr === '-') return { amount: 0, currency: '' };
const match = priceStr.match(/(\d[\d\s]+)(?:\s*)([A-Za-z$]+|点|MM|R\$)/);
if (!match) return { amount: 0, currency: '' };
const rawAmount = match[1].replace(/\s+/g, '');
const amount = parseFloat(rawAmount);
const currency = match[2];
return { amount, currency };
};
const convertPrice = (priceObj, targetCurrency) => {
if (!priceObj.amount || !priceObj.currency || !CURRENCIES[priceObj.currency]) {
return { amount: 0, currency: targetCurrency };
}
const sourceRate = CURRENCIES[priceObj.currency];
const targetRate = CURRENCIES[targetCurrency];
if (!sourceRate || !targetRate) {
return { amount: priceObj.amount, currency: priceObj.currency };
}
const inSEK = priceObj.amount * sourceRate;
const convertedAmount = inSEK / targetRate;
return {
amount: Math.round(convertedAmount),
currency: targetCurrency
};
};
const formatPrice = (priceObj) => {
if (!priceObj.amount) return 'N/A';
const formatted = new Intl.NumberFormat().format(priceObj.amount);
return `${formatted} ${priceObj.currency}`;
};
const parseSeriesData = (txt) => {
const m = txt.match(/var series = (\[.*?\]);/);
return m ? JSON.parse(m[1]) : null;
};
const extractChipsInfo = (series) => {
const chips = [];
if (!series) return chips;
series.forEach(s => {
s.data.forEach(pt => {
if (pt.marker?.symbol.includes('training_camp_chip.png') && pt.name) {
const chipName = pt.name.replace(/<\/b>\t\t\t\n/g, ' ').trim();
const date = new Date(pt.x);
chips.push({
name: chipName,
date: date,
dateString: date.toLocaleDateString()
});
}
});
});
return chips;
};
const gatherCurrentSkills = (container) => {
const rows = container.querySelectorAll('table.player_skills tr');
const out = {};
let i = 1;
rows.forEach(r => {
const valCell = r.querySelector('.skillval span');
if (!valCell) return;
const name = SKILL_MAP[i.toString()];
if (name) {
const v = parseInt(valCell.textContent.trim(), 10);
out[name] = isNaN(v) ? 0 : v;
}
i++;
});
return out;
};
const getTotalBallsFromSkillMap = (map) => Object.values(map).reduce((a, b) => a + b, 0);
const processTrainingHistory = (series, getSeasonFn) => {
const bySeason = {};
const skillTotals = {};
const chips = extractChipsInfo(series);
const chipsBySeason = {};
const skillMaxedSeason = {};
let total = 0;
let earliest = 9999;
chips.forEach(chip => {
const season = getSeasonFn(chip.date);
if (!chipsBySeason[season]) chipsBySeason[season] = [];
chipsBySeason[season].push(chip);
});
if (series) {
series.forEach(s => {
s.data.forEach((pt, i) => {
if (pt.marker?.symbol.includes('gained_skill.png') && s.data[i + 1]) {
const nextPt = s.data[i + 1];
const d = new Date(nextPt.x);
const sea = getSeasonFn(d);
if (!bySeason[sea]) bySeason[sea] = [];
const sid = nextPt.y.toString();
const sk = SKILL_MAP[sid] || 'Unknown';
const isMaxed = nextPt.hasOwnProperty('name');
bySeason[sea].push({ dateString: d.toLocaleDateString(), skillName: sk, maxed: isMaxed });
if (isMaxed && sk !== 'Unknown' && !skillMaxedSeason[sk]) {
skillMaxedSeason[sk] = sea;
}
if (!skillTotals[sk]) skillTotals[sk] = 0;
skillTotals[sk]++;
total++;
if (sea < earliest) earliest = sea;
}
});
});
}
return { bySeason, skillTotals, total, earliestSeason: earliest, chips, chipsBySeason, skillMaxedSeason };
};
const fillSeasonGains = (bySeason, earliestSeason, currentSeason, skillTotals) => {
const out = {};
for (let s = earliestSeason; s <= currentSeason; s++) {
out[s] = {};
ORDERED_SKILLS.forEach(sk => { out[s][sk] = 0; });
if (bySeason[s]) {
bySeason[s].forEach(ev => {
if (skillTotals[ev.skillName]) {
if (!out[s][ev.skillName]) out[s][ev.skillName] = 0;
out[s][ev.skillName]++;
}
});
}
}
return out;
};
const buildSeasonCheckpointData = (earliestSeason, currentSeason, finalMap, seasonGains, currentAge) => {
const out = [];
const currentMap = {};
ORDERED_SKILLS.forEach(sk => {
currentMap[sk] = finalMap[sk] || 0;
});
out.push({
season: currentSeason,
label: 'Current',
distribution: { ...finalMap }
});
for (let s = currentSeason; s >= earliestSeason; s--) {
if (seasonGains[s]) {
Object.keys(seasonGains[s]).forEach(k => {
if (currentMap.hasOwnProperty(k)) {
currentMap[k] -= seasonGains[s][k];
if (currentMap[k] < 0) currentMap[k] = 0;
}
});
}
const age = calculateHistoricalAge({ currentAge, currentSeason, targetSeason: s });
const label = age !== null ? `${s} (${age})` : s.toString();
const snapshot = { ...currentMap };
out.unshift({ season: s, label, distribution: snapshot });
}
return out;
};
const makeSkillRows = (params) => {
const { map, prevMap, arrivalMap, currentSeasonForState, isCurrentState, scoutData, skillMaxedSeason } = params;
let comparisonHtml = '';
let arrivalGainHtml = '';
let totalIncreaseFromPrev = 0;
let totalGainSinceArrival = 0;
ORDERED_SKILLS.forEach((k, idx) => {
let v = map[k] || 0;
if (v < 0) v = 0;
if (v > 10) v = 10;
let changeHTML = '';
let gainSinceArrivalTextHTML = '';
let initialBallsVizHTML = '';
let gainedBallsVizHTML = '';
let potentialClass = '';
let potentialIcon = '';
let skillNameSpecificClass = '';
const maxedSeasonForSkill = skillMaxedSeason[k];
const isVisuallyMaxed = (v === 10) || (maxedSeasonForSkill && maxedSeasonForSkill < currentSeasonForState);
const isMaxedClass = isVisuallyMaxed ? ' th-skill-maxed' : '';
if (scoutData) {
if (scoutData.hp > 0 && (k === scoutData.firstHpSkill || k === scoutData.secondHpSkill)) {
skillNameSpecificClass = ` th-skill-potential-hp${scoutData.hp}`;
} else if (scoutData.lp > 0 && (k === scoutData.firstLpSkill || k === scoutData.secondLpSkill)) {
skillNameSpecificClass = ` th-skill-potential-lp${scoutData.lp}`;
}
if (scoutData.hp > 0 && scoutData.hpPotentialIndices?.includes(idx)) {
potentialClass = ` th-skill-potential-hp${scoutData.hp}`;
potentialIcon = `<i class="fas fa-star th-potential-icon th-potential-icon-hp${scoutData.hp}"></i>`;
} else if (scoutData.lp > 0 && scoutData.lpPotentialIndices?.includes(idx)) {
potentialClass = ` th-skill-potential-lp${scoutData.lp}`;
potentialIcon = `<i class="fas fa-star th-potential-icon th-potential-icon-lp${scoutData.lp}"></i>`;
}
}
if (prevMap) {
const prevVal = prevMap[k] || 0;
const change = v - prevVal;
if (change > 0) {
changeHTML = `<span class="th-skill-increase">(+${change})</span>`;
totalIncreaseFromPrev += change;
}
}
const baseSkillRowStartHtml = `
<div class="th-state-skill${potentialClass}">
<div class="th-skill-name${skillNameSpecificClass}">${potentialIcon}<strong>${k}</strong></div>`;
comparisonHtml += `${baseSkillRowStartHtml}
<div class="th-skill-val">
<img src="nocache-922/img/soccer/wlevel_${v}.gif" alt="">
<span class="th-skill-value-text${isMaxedClass}">(${v})</span>
</div>
<div class="th-skill-change">${changeHTML}</div>
</div>`;
if (arrivalMap && isCurrentState) {
const arrivalVal = arrivalMap[k] || 0;
const gainSinceArrival = v - arrivalVal;
initialBallsVizHTML = `<span class="th-initial-balls">${arrivalVal > 0 ? '●'.repeat(arrivalVal) : ''}</span>`;
if (gainSinceArrival > 0) {
gainSinceArrivalTextHTML = `<span class="th-gain-since-arrival">(+${gainSinceArrival})</span>`;
totalGainSinceArrival += gainSinceArrival;
gainedBallsVizHTML = `<span class="th-gained-balls">${'●'.repeat(gainSinceArrival)}</span>`;
} else {
gainSinceArrivalTextHTML = '';
gainedBallsVizHTML = '';
}
arrivalGainHtml += `${baseSkillRowStartHtml}
<div class="th-skill-val th-arrival-skill-val">
${initialBallsVizHTML}
${gainedBallsVizHTML}
</div>
<div class="th-skill-change">${gainSinceArrivalTextHTML}</div>
</div>`;
}
});
return { comparisonHtml, arrivalGainHtml, totalIncrease: totalIncreaseFromPrev, totalGainSinceArrival };
};
const createModal = (content, spin) => {
const ov = document.createElement('div');
ov.className = 'th-overlay';
const mo = document.createElement('div');
mo.className = 'th-modal';
const bd = document.createElement('div');
bd.className = 'th-modal-content';
const sp = document.createElement('div');
sp.style.height = '60px';
sp.style.display = spin ? 'block' : 'none';
bd.appendChild(sp);
if (content) bd.innerHTML += content;
const cl = document.createElement('div');
cl.className = 'th-modal-close';
cl.innerHTML = '×';
cl.onclick = () => ov.remove();
mo.appendChild(cl);
mo.appendChild(bd);
ov.appendChild(mo);
document.body.appendChild(ov);
ov.addEventListener('click', e => {
if (e.target === ov) ov.remove();
});
requestAnimationFrame(() => {
ov.classList.add('show');
mo.classList.add('show');
});
let spinnerInstance = null;
if (spin) {
spinnerInstance = new Spinner({ color: '#5555aa', lines: 12 });
spinnerInstance.spin(sp);
}
return { modal: mo, spinnerEl: sp, spinnerInstance, overlay: ov };
};
const generateEvolHTML = (processedData, currentAge, currentSeason) => {
const { bySeason, total, skillTotals, chips, chipsBySeason, earliestSeason } = processedData;
let html = '';
const getSeason = getSeasonCalculator({ currentDate: new Date(), season: currentSeason, day: 1 });
const sortedSeasons = Object.keys(bySeason)
.map(x => parseInt(x, 10))
.sort((a, b) => a - b);
sortedSeasons.forEach(se => {
const items = bySeason[se];
const age = calculateHistoricalAge({ currentAge, currentSeason, targetSeason: se });
const label = age !== null ? `Season ${se} (Age ${age})` : `Season ${se}`;
let seasonChipsHtml = '';
if (chipsBySeason[se] && chipsBySeason[se].length > 0) {
seasonChipsHtml = '<div class="th-chips-list">Chips: ';
seasonChipsHtml += chipsBySeason[se].map(chip => {
const simplifiedName = chip.name.split('</b>')[1]?.trim() || chip.name;
return `${simplifiedName} (${chip.dateString})`;
}).join(', ');
seasonChipsHtml += '</div>';
}
html += `<div class="th-training-season">
<h3>${label} — ${items.length} Ball${items.length !== 1 ? 's' : ''} Earned</h3>
${seasonChipsHtml}
<ul>`;
items.forEach(it => {
const maxedIndicator = it.maxed ? ' <span class="th-maxed-indicator">(Maxed)</span>' : '';
html += `<li><strong>${it.dateString}</strong> ${it.skillName}${maxedIndicator}</li>`;
});
html += '</ul></div>';
});
html += `<hr><h3 class="th-training-final-summary">Total Balls Earned: ${total}</h3>`;
const fs = Object.entries(skillTotals)
.filter(([, count]) => count > 0)
.sort(([, countA], [, countB]) => countB - countA)
.map(([skill, count]) => `${skill} (${count})`)
.join(', ');
html += `<h3 class="th-training-skilltotals">${fs}</h3>`;
const allChipsSorted = [...chips].sort((a, b) => a.date - b.date);
if (allChipsSorted.length > 0) {
html += `<h3 class="th-training-final-summary">All Applied Chips</h3><ul class="th-all-chips-list">`;
allChipsSorted.forEach(chip => {
const simplifiedName = chip.name.split('</b>')[1]?.trim() || chip.name;
const chipSeason = getSeason(chip.date);
html += `<li>S${chipSeason}: ${simplifiedName} (${chip.dateString})</li>`;
});
html += `</ul>`;
}
return html;
};
const buildStatesLayout = (processedData, finalMap, currentAge, currentSeason, scoutData, transferData) => {
const { bySeason, skillTotals, earliestSeason, chipsBySeason, skillMaxedSeason } = processedData;
const seasonGains = fillSeasonGains(bySeason, earliestSeason, currentSeason, skillTotals);
const arr = buildSeasonCheckpointData(earliestSeason, currentSeason, finalMap, seasonGains, currentAge);
const arrivalMap = arr.length > 0 ? arr[0].distribution : null;
let paginatedHtml = '<div class="th-state-wrapper th-paginated-view">';
let allViewHtml = '<div class="th-state-wrapper th-all-view" style="display:none;">';
arr.forEach((o, index) => {
const sum = getTotalBallsFromSkillMap(o.distribution);
let headerText;
const isCurrent = o.label === 'Current';
const seasonNumber = isCurrent ? currentSeason : parseInt(o.label.split(' ')[0], 10);
let stateSkillsHtml = '';
const prevDistribution = index > 0 ? arr[index - 1].distribution : null;
const useArrivalMapForComparison = isCurrent ? arrivalMap : null;
const skillRowsResult = makeSkillRows({
map: o.distribution,
prevMap: prevDistribution,
arrivalMap: useArrivalMapForComparison,
currentSeasonForState: seasonNumber,
isCurrentState: isCurrent,
scoutData: scoutData,
skillMaxedSeason: skillMaxedSeason
});
if (isCurrent) {
headerText = `Current State - Season ${currentSeason}`;
if (currentAge !== null) headerText += ` (Age ${currentAge})`;
stateSkillsHtml = `
<div class="th-state-skills">
<h5>Changes vs Start of Season ${currentSeason}</h5>
${skillRowsResult.comparisonHtml}
${skillRowsResult.totalIncrease > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalIncrease} total this season)</span></div>` : ''}
</div>
<div class="th-state-skills th-arrival-gains">
<h5>Gains Since Arrival (Season ${arr[0]?.season || '?'})</h5>
${skillRowsResult.arrivalGainHtml}
${skillRowsResult.totalGainSinceArrival > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalGainSinceArrival} total since arrival)</span></div>` : ''}
</div>
`;
} else {
const [seasonStr, ageStr] = o.label.split(' ');
const agePart = ageStr ? ageStr.replace(/[()]/g, '') : '?';
if (index === 0) {
headerText = `Arrival at Club - Season ${seasonStr}`;
if (agePart !== '?') headerText += ` (Age ${agePart})`;
} else {
headerText = `Start of Season ${seasonStr}`;
if (agePart !== '?') headerText += ` (Age ${agePart})`;
}
stateSkillsHtml = `
<div class="th-state-skills">
${index > 0 ? `<h5>Changes vs Start of Season ${arr[index - 1]?.season}</h5>` : ''}
${skillRowsResult.comparisonHtml}
${skillRowsResult.totalIncrease > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalIncrease} total vs prev)</span></div>` : ''}
</div>
`;
}
let chipInfo = '';
if (chipsBySeason[seasonNumber] && chipsBySeason[seasonNumber].length > 0 && !isCurrent) {
const chipNames = chipsBySeason[seasonNumber].map(c => {
const simplifiedName = c.name.split('</b>')[1]?.trim() || c.name;
return simplifiedName;
}).join(', ');
chipInfo = `<div class="th-chips-list">Chips used during S${seasonNumber}: ${chipNames}</div>`;
}
let transferInfo = '';
if (transferData && transferData[seasonNumber] && transferData[seasonNumber].length > 0 && !isCurrent) {
transferInfo = '<div class="th-transfer-info" data-season="' + seasonNumber + '">' +
'<div class="th-transfer-header">Season ' + seasonNumber + ': ' +
'<i class="fa fa-cog th-transfer-currency-icon" title="Change currency"></i>' +
'<div class="th-transfer-currency-dropdown"><ul>';
Object.keys(CURRENCIES).forEach(curr => {
transferInfo += `<li data-currency="${curr}" ${curr === preferredCurrency ? 'class="selected"' : ''}>${curr}</li>`;
});
transferInfo += '</ul></div></div><ul>';
transferData[seasonNumber].forEach(t => {
const priceObj = parsePriceString(t.price);
const convertedPrice = convertPrice(priceObj, preferredCurrency);
const displayPrice = formatPrice(convertedPrice);
transferInfo += `<li data-original-price="${t.price}" data-price-amount="${priceObj.amount}" data-price-currency="${priceObj.currency}">
${t.dateString}: ${t.fromTeamName} <i class="fa fa-arrow-right"></i> ${t.toTeamName} (${displayPrice})
</li>`;
});
transferInfo += '</ul></div>';
}
const colHtml = `<div class="th-state-col" data-page="${index}" data-season="${seasonNumber}">
<h4>${headerText}</h4>
<div class="th-state-info">Total Balls: <strong>${sum}</strong></div>
${stateSkillsHtml}
${chipInfo}
${transferInfo}
</div>`;
paginatedHtml += colHtml;
allViewHtml += colHtml;
});
paginatedHtml += '</div>';
allViewHtml += '</div>';
let scoutHtml = '';
if (scoutData) {
const { trainingSpeed, hp, lp, firstHpSkill, secondHpSkill, firstLpSkill, secondLpSkill } = scoutData;
const speedClass = `th-speed-s${trainingSpeed}`;
const hpClass = `th-hp-h${hp}`;
const lpClass = `th-lp-l${lp}`;
const speedText = trainingSpeed > 0 ? `S${trainingSpeed}` : 'N/A';
const hpText = hp > 0 ? `HP${hp}` : 'N/A';
const lpText = lp > 0 ? `LP${lp}` : 'N/A';
let hpSkillsText = '';
if (hp > 0 && firstHpSkill) {
hpSkillsText += `<span class="th-potential-skill th-skill-potential-hp${hp}">${firstHpSkill}</span>`;
if (secondHpSkill) hpSkillsText += `/<span class="th-potential-skill th-skill-potential-hp${hp}">${secondHpSkill}</span>`;
}
hpSkillsText = hpSkillsText ? ` ${hpSkillsText}` : '';
let lpSkillsText = '';
if (lp > 0 && firstLpSkill) {
lpSkillsText += `<span class="th-potential-skill th-skill-potential-lp${lp}">${firstLpSkill}</span>`;
if (secondLpSkill) lpSkillsText += `/<span class="th-potential-skill th-skill-potential-lp${lp}">${secondLpSkill}</span>`;
}
lpSkillsText = lpSkillsText ? ` ${lpSkillsText}` : '';
scoutHtml = `
<div class="th-scout-info">
<span class="${speedClass}">TrainingSpeed ${speedText}</span> |
<span class="${hpClass}">${hpText}</span>${hpSkillsText} |
<span class="${lpClass}">${lpText}</span>${lpSkillsText}
</div>`;
}
return scoutHtml + paginatedHtml + allViewHtml;
};
const generateTabsHTML = (name, evo, st) => `
<h2 class="th-title">${name}</h2>
<div class="th-tabs">
<div class="th-tab-row">
<div class="th-tab-buttons">
<button class="th-tab-btn active" data-tab="states">Player Development</button>
<button class="th-tab-btn" data-tab="evolution">Gains Log</button>
</div>
<div class="th-pagination-controls">
<button class="th-pagination-btn th-prev-btn" disabled>←</button>
<span class="th-pagination-indicator">1 / 1</span>
<button class="th-pagination-btn th-next-btn" disabled>→</button>
<button class="th-pagination-toggle">Show All</button>
</div>
</div>
<div class="th-tab-content" data-content="evolution">${evo}</div>
<div class="th-tab-content active" data-content="states">${st}</div>
</div>`;
const detachPaginationEvents = (modal) => {
const prevBtn = modal.querySelector('.th-prev-btn');
const nextBtn = modal.querySelector('.th-next-btn');
const toggleBtn = modal.querySelector('.th-pagination-toggle');
if (prevBtn) {
const newPrevBtn = prevBtn.cloneNode(true);
prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn);
}
if (nextBtn) {
const newNextBtn = nextBtn.cloneNode(true);
nextBtn.parentNode.replaceChild(newNextBtn, nextBtn);
}
if (toggleBtn) {
const newToggleBtn = toggleBtn.cloneNode(true);
toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
}
};
const attachPaginationEvents = (modal, initialIndex) => {
const statesContent = modal.querySelector('.th-tab-content[data-content="states"]');
if (!statesContent) return;
const prevBtn = modal.querySelector('.th-prev-btn');
const nextBtn = modal.querySelector('.th-next-btn');
const paginationIndicator = modal.querySelector('.th-pagination-indicator');
const toggleBtn = modal.querySelector('.th-pagination-toggle');
const paginatedView = statesContent.querySelector('.th-paginated-view');
const allView = statesContent.querySelector('.th-all-view');
if (!paginatedView || !allView || !prevBtn || !nextBtn || !paginationIndicator || !toggleBtn) return;
const stateColumns = Array.from(paginatedView.querySelectorAll('.th-state-col'));
const totalPages = stateColumns.length;
let currentIndex = initialIndex;
const updatePaginationUI = () => {
if (totalPages === 0) {
prevBtn.disabled = true;
nextBtn.disabled = true;
paginationIndicator.textContent = '0 / 0';
toggleBtn.style.display = 'none';
return;
}
toggleBtn.style.display = '';
prevBtn.disabled = currentIndex === 0;
nextBtn.disabled = currentIndex === totalPages - 1;
paginationIndicator.textContent = `${currentIndex + 1} / ${totalPages}`;
stateColumns.forEach((col, index) => {
col.style.display = index === currentIndex ? '' : 'none';
});
};
prevBtn.addEventListener('click', () => {
if (paginatedView.style.display === 'none') return;
if (currentIndex > 0) {
currentIndex--;
updatePaginationUI();
}
});
nextBtn.addEventListener('click', () => {
if (paginatedView.style.display === 'none') return;
if (currentIndex < totalPages - 1) {
currentIndex++;
updatePaginationUI();
}
});
toggleBtn.addEventListener('click', () => {
const isPaginated = paginatedView.style.display !== 'none';
paginatedView.style.display = isPaginated ? 'none' : '';
allView.style.display = isPaginated ? '' : 'none';
toggleBtn.textContent = isPaginated ? 'Show Paginated' : 'Show All';
prevBtn.disabled = !isPaginated || currentIndex === 0;
nextBtn.disabled = !isPaginated || currentIndex === totalPages - 1;
if (!isPaginated) {
updatePaginationUI();
}
});
updatePaginationUI();
};
const initPaginationState = (modal) => {
const statesContent = modal.querySelector('.th-tab-content[data-content="states"]');
if (!statesContent) return;
const paginatedView = statesContent.querySelector('.th-paginated-view');
const allView = statesContent.querySelector('.th-all-view');
const paginationToggle = modal.querySelector('.th-pagination-toggle');
const paginationControls = modal.querySelector('.th-pagination-controls');
if (!paginatedView || !allView || !paginationToggle || !paginationControls) {
if (paginationControls) paginationControls.style.display = 'none';
return;
}
const stateColumns = paginatedView.querySelectorAll('.th-state-col');
if (!stateColumns.length) {
if (paginationControls) paginationControls.style.display = 'none';
return;
} else {
paginationControls.style.display = '';
}
paginatedView.style.display = '';
allView.style.display = 'none';
paginationToggle.textContent = 'Show All';
let initialIndex = 0;
stateColumns.forEach((col, index) => {
col.style.display = index === initialIndex ? '' : 'none';
});
const paginationIndicator = modal.querySelector('.th-pagination-indicator');
const prevBtn = modal.querySelector('.th-prev-btn');
const nextBtn = modal.querySelector('.th-next-btn');
if (paginationIndicator) {
paginationIndicator.textContent = `${initialIndex + 1} / ${stateColumns.length}`;
}
if (prevBtn) prevBtn.disabled = initialIndex === 0;
if (nextBtn) nextBtn.disabled = initialIndex >= stateColumns.length - 1;
detachPaginationEvents(modal);
attachPaginationEvents(modal, initialIndex);
};
const updateTransferPrices = (modal, newCurrency) => {
const transferInfoSections = modal.querySelectorAll('.th-transfer-info');
transferInfoSections.forEach(section => {
const items = section.querySelectorAll('li');
items.forEach(item => {
const originalAmount = parseFloat(item.getAttribute('data-price-amount'));
const originalCurrency = item.getAttribute('data-price-currency');
if (originalAmount && originalCurrency) {
const priceObj = { amount: originalAmount, currency: originalCurrency };
const convertedPrice = convertPrice(priceObj, newCurrency);
const displayPrice = formatPrice(convertedPrice);
const text = item.innerHTML;
const pricePattern = /\([^)]*\)(?=[^(]*$)/;
item.innerHTML = text.replace(pricePattern, `(${displayPrice})`);
}
});
});
};
const setUpCurrencyDropdowns = (modal) => {
const icons = modal.querySelectorAll('.th-transfer-currency-icon');
icons.forEach(icon => {
const newIcon = icon.cloneNode(true);
icon.parentNode.replaceChild(newIcon, icon);
});
modal.querySelectorAll('.th-transfer-currency-icon').forEach(icon => {
icon.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
const dropdown = this.nextElementSibling;
dropdown.classList.toggle('show');
if (dropdown.classList.contains('show')) {
const rect = this.getBoundingClientRect();
dropdown.style.top = (rect.bottom - rect.top) + 5 + 'px';
dropdown.style.left = '0px';
}
});
});
modal.querySelectorAll('.th-transfer-currency-dropdown li').forEach(item => {
const newItem = item.cloneNode(true);
item.parentNode.replaceChild(newItem, item);
newItem.addEventListener('click', function(e) {
e.stopPropagation();
const newCurrency = this.getAttribute('data-currency');
preferredCurrency = newCurrency;
GM_setValue('PREFERRED_CURRENCY', newCurrency);
modal.querySelectorAll('.th-transfer-currency-dropdown li').forEach(li => {
li.classList.remove('selected');
});
modal.querySelectorAll(`.th-transfer-currency-dropdown li[data-currency="${newCurrency}"]`).forEach(li => {
li.classList.add('selected');
});
updateTransferPrices(modal, newCurrency);
this.closest('.th-transfer-currency-dropdown').classList.remove('show');
});
});
};
const attachTabEvents = (modal) => {
const tbs = modal.querySelectorAll('.th-tab-btn');
const cs = modal.querySelectorAll('.th-tab-content');
const paginationControls = modal.querySelector('.th-pagination-controls');
const statesContent = modal.querySelector('.th-tab-content[data-content="states"]');
if (!statesContent || !paginationControls) return;
const updatePaginationVisibility = () => {
const activeTab = modal.querySelector('.th-tab-btn.active')?.getAttribute('data-tab');
paginationControls.style.display = (activeTab === 'states') ? '' : 'none';
};
setUpCurrencyDropdowns(modal);
document.addEventListener('click', function(e) {
if (!e.target.closest('.th-transfer-currency-dropdown') && !e.target.classList.contains('th-transfer-currency-icon')) {
document.querySelectorAll('.th-transfer-currency-dropdown.show').forEach(dropdown => {
dropdown.classList.remove('show');
});
}
});
updatePaginationVisibility();
tbs.forEach(btn => {
btn.addEventListener('click', () => {
tbs.forEach(x => x.classList.remove('active'));
btn.classList.add('active');
const t = btn.getAttribute('data-tab');
cs.forEach(cc => {
cc.classList.toggle('active', cc.getAttribute('data-content') === t);
});
updatePaginationVisibility();
if (t === 'states') {
initPaginationState(modal);
}
});
});
};
const fetchScoutData = async (pid) => {
try {
const response = await fetch(`https://www.managerzone.com/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer`);
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
const defaultResult = { trainingSpeed: 0, hp: 0, lp: 0, firstHpSkill: '', secondHpSkill: '', firstLpSkill: '', secondLpSkill: '', hpPotentialIndices: [], lpPotentialIndices: [] };
const paperContent = doc.querySelector('.paper-content');
if (!paperContent) return defaultResult;
const dlElement = paperContent.querySelector('dl');
if (!dlElement) return defaultResult;
const hpDd = dlElement.querySelector('dd i.fa-line-chart')?.closest('dd');
const lpDd = dlElement.querySelector('dd i.fa-exclamation-triangle')?.closest('dd');
const speedDd = dlElement.querySelector('dd i.fa-heartbeat')?.closest('dd');
const getSkillsAndIndices = (dd) => {
const skills = [];
const indices = [];
if (!dd) return { skills, indices };
const listItems = dd.querySelectorAll('ul li');
listItems.forEach((li, index) => {
const span = li.querySelector('.blurred span:last-child');
if (span) {
const skillName = span.textContent.trim();
skills.push(skillName);
if (li.querySelector('.stars i.fa-star.lit')) {
indices.push(index);
}
}
});
return { skills, indices };
};
const getStars = (dd) =>
dd ? dd.querySelectorAll('.stars i.fa-star.lit').length : 0;
const { skills: hpSkillsNative, indices: hpIndices } = getSkillsAndIndices(hpDd);
const { skills: lpSkillsNative, indices: lpIndices } = getSkillsAndIndices(lpDd);
const hpStars = getStars(hpDd);
const lpStars = getStars(lpDd);
const speedStars = getStars(speedDd);
const firstHpNative = hpSkillsNative[0] || '';
const secondHpNative = hpSkillsNative[1] || '';
const firstLpNative = lpSkillsNative[0] || '';
const secondLpNative = lpSkillsNative[1] || '';
return {
hp: hpStars,
lp: lpStars,
trainingSpeed: speedStars,
firstHpSkill: getEnglishSkillName(firstHpNative),
secondHpSkill: getEnglishSkillName(secondHpNative),
firstLpSkill: getEnglishSkillName(firstLpNative),
secondLpSkill: getEnglishSkillName(secondLpNative),
hpPotentialIndices: hpIndices,
lpPotentialIndices: lpIndices
};
} catch (error) {
return null;
}
};
const fetchTransferData = async (pid, getSeasonFn) => {
const url = `https://www.managerzone.com/?p=players&pid=${pid}`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
let transferTable = null;
const potentialTables = doc.querySelectorAll('table.hitlist');
for (const table of potentialTables) {
const tbody = table.querySelector('tbody');
if (!tbody) continue;
const firstRow = tbody.querySelector('tr');
if (!firstRow) continue;
const cells = firstRow.querySelectorAll('td');
if (cells.length < 5) continue;
const dateCellText = cells[0]?.textContent.trim();
const priceCellText = cells[4]?.textContent.trim();
const priceCellDiv = cells[4]?.querySelector('div[title]');
const isDateLike = dateCellText && /(\d{2})-(\d{2})-(\d{4})|(\d{4})-(\d{2})-(\d{2})/.test(dateCellText);
const isPriceLike = (priceCellText === '-') || (priceCellText && /\d/.test(priceCellText)) || priceCellDiv;
if (isDateLike && isPriceLike) {
transferTable = table;
break;
}
}
if (!transferTable) {
return {};
}
const transfersBySeason = {};
const rows = transferTable.querySelectorAll('tbody tr');
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length < 5) return;
const dateCell = cells[0];
const actionOrFromTeamCell = cells[1];
const toTeamCell = cells[3];
const priceCell = cells[4];
const dateStrRaw = dateCell?.textContent.trim();
const dateMatch = dateStrRaw.match(/(\d{2})-(\d{2})-(\d{4})|(\d{4})-(\d{2})-(\d{2})/);
if (!dateMatch) return;
let day, month, year;
if (dateMatch[1]) {
day = dateMatch[1]; month = dateMatch[2]; year = dateMatch[3];
} else {
day = dateMatch[6]; month = dateMatch[5]; year = dateMatch[4];
}
const transferDate = new Date(`${year}-${month}-${day}`);
if (isNaN(transferDate)) return;
const season = getSeasonFn(transferDate);
const fromTeamLink = actionOrFromTeamCell.querySelector('a[href*="tid="]');
const toTeamLink = toTeamCell.querySelector('a[href*="tid="]');
let fromTeamName = 'Youth Academy';
if (fromTeamLink) {
fromTeamName = fromTeamLink.textContent.trim();
} else {
const fromText = actionOrFromTeamCell.textContent.trim();
if (fromText && fromText !== '-') {
fromTeamName = fromText;
}
}
const toTeamName = toTeamLink ? toTeamLink.textContent.trim() : toTeamCell.textContent.trim();
const priceDiv = priceCell?.querySelector('div[title]');
let price = priceDiv?.title || priceCell?.textContent.trim() || 'N/A';
if (price === '-') price = 'N/A';
if (!transfersBySeason[season]) {
transfersBySeason[season] = [];
}
transfersBySeason[season].push({
date: transferDate,
dateString: transferDate.toLocaleDateString(),
fromTeamName: fromTeamName,
toTeamName: toTeamName,
price: price
});
});
Object.values(transfersBySeason).forEach(seasonTransfers => {
seasonTransfers.sort((a, b) => a.date - b.date);
});
return transfersBySeason;
} catch (_error) {
return {};
}
};
function decodeHtmlEntities(text) {
if (!text) return '';
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
async function fetchCurrentSkillsViaAjax(pid) {
return new Promise((resolve, reject) => {
const url = `https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=${pid}&nationality=all_nationalities&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=15&ageb=45&birth_season_low=0&birth_season_high=100&tot_low=0&tot_high=120&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 = {};
const skillRows = skillsTable.querySelectorAll('tbody > tr');
skillRows.forEach((row, index) => {
const skillId = (index + 1).toString();
const skillName = SKILL_MAP[skillId];
if (skillName) {
const valueElem = row.querySelector('td.skillval > span');
if (valueElem) {
const value = parseInt(valueElem.textContent.trim().replace(/[()]/g, ''), 10);
if (!isNaN(value)) {
skills[skillName] = value;
} else {
skills[skillName] = 0;
}
} else {
skills[skillName] = 0;
}
}
});
if (Object.keys(skills).length === ORDERED_SKILLS.length) {
resolve(skills);
} else {
reject("Could not extract all expected skills from the AJAX response table.");
}
} else {
reject("Skills table not found in AJAX response.");
}
} catch (e) {
reject("Error parsing AJAX response: " + e.message);
}
} else {
reject("No player data found in AJAX response.");
}
})
.catch(error => {
reject("Error during fetch request: " + error.message);
});
});
}
const fetchCombinedPlayerData = async (pid, node, getSeasonFn, csi) => {
const cont = getPlayerContainerNode(node);
if (!cont) return;
const curSeason = csi.season;
const nmEl = cont.querySelector('.player_name');
const nm = nmEl ? nmEl.textContent.trim() : 'Unknown Player';
const { modal, spinnerEl, spinnerInstance } = createModal('', true);
const currentAge = parsePlayerAge(cont);
try {
let currentSkillsMap = {};
const isSpecificPlayerPage = window.location.href.includes('/?p=players&pid=');
if (isSpecificPlayerPage) {
if (hasVisibleSkills(cont)) {
currentSkillsMap = gatherCurrentSkills(cont);
} else {
try {
currentSkillsMap = await fetchCurrentSkillsViaAjax(pid);
} catch (fetchError) {
throw new Error(`Could not retrieve current skills via AJAX. ${fetchError.message || fetchError}`);
}
}
} else {
currentSkillsMap = gatherCurrentSkills(cont);
}
if (!currentSkillsMap || Object.keys(currentSkillsMap).length === 0) {
throw new Error("Failed to determine current skills for player.");
}
const [trainingResponse, scoutData, transferData] = await Promise.all([
fetch(`https://www.managerzone.com/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}`).then(res => res.ok ? res.text() : Promise.reject(`HTTP error ${res.status} for training data`)),
fetchScoutData(pid),
fetchTransferData(pid, getSeasonFn)
]);
if (spinnerInstance) spinnerInstance.stop();
spinnerEl.style.display = 'none';
const series = parseSeriesData(trainingResponse);
const processedTrainingData = processTrainingHistory(series, getSeasonFn);
const transferSeasons = Object.keys(transferData).map(s => parseInt(s)).filter(s => !isNaN(s));
let effectiveEarliestSeason = processedTrainingData.earliestSeason;
if (transferSeasons.length > 0) {
const earliestTransferSeason = Math.min(...transferSeasons);
effectiveEarliestSeason = Math.max(1, Math.min(effectiveEarliestSeason, earliestTransferSeason));
}
if (effectiveEarliestSeason === 9999) {
if (transferSeasons.length > 0) {
effectiveEarliestSeason = Math.max(1, Math.min(...transferSeasons));
} else {
effectiveEarliestSeason = curSeason;
}
}
processedTrainingData.earliestSeason = effectiveEarliestSeason;
const evoHTML = generateEvolHTML(processedTrainingData, currentAge, curSeason);
const stHTML = buildStatesLayout(processedTrainingData, currentSkillsMap, currentAge, curSeason, scoutData, transferData);
const finalHTML = generateTabsHTML(nm, evoHTML, stHTML);
modal.querySelector('.th-modal-content').innerHTML = finalHTML;
setTimeout(() => {
attachTabEvents(modal);
initPaginationState(modal);
}, 50);
} catch (error) {
if (spinnerInstance) spinnerInstance.stop();
if(spinnerEl) spinnerEl.style.display = 'none';
const contentDiv = modal.querySelector('.th-modal-content');
if (contentDiv) {
contentDiv.innerHTML = `<div class="th-error-message"><p>Failed to process player data. (${error.message || 'Unknown error'})</p></div>`;
}
}
};
const insertButtons = (getSeasonFn, csi) => {
const containers = document.querySelectorAll('.playerContainer');
const isPlayerProfilePage = window.location.href.includes('/?p=players&pid=');
containers.forEach(cc => {
const targetElements = cc.querySelectorAll('.floatRight[id^="player_id_"]');
targetElements.forEach(ff => {
const pidSpan = ff.querySelector('.player_id_span');
if (!pidSpan) return;
const pid = pidSpan.textContent.trim();
if (!pid) return;
const existingBtn = ff.querySelector('.th-btn');
if (existingBtn) return;
let shouldInsert = false;
const disabledGraphIcon = cc.querySelector('.training-graphs-icon.training-graphs-icon--disabled');
if (!disabledGraphIcon) {
if (isPlayerProfilePage) {
const trainingGraphIcon = document.querySelector(
`span.player_icon_placeholder.training_graphs.soccer a[href*="p=training_graphs"][href*="pid=${pid}"]`
);
if (trainingGraphIcon) {
shouldInsert = true;
}
} else {
if (hasVisibleSkills(cc)) {
shouldInsert = true;
}
}
}
if (shouldInsert) {
const b = document.createElement('button');
b.className = 'th-btn';
b.innerHTML = '<i class="fa fa-chart-line"></i>';
b.title = 'View Training History';
b.onclick = (e) => {
e.preventDefault();
fetchCombinedPlayerData(pid, ff, getSeasonFn, csi);
};
ff.appendChild(b);
}
});
});
};
const initTeamId = () => {
const stored = GM_getValue('TEAM_ID');
if (stored) {
myTeamId = stored;
return;
}
const usernameEl = document.querySelector('#header-username');
if (!usernameEl) return;
const username = usernameEl.textContent.trim();
if (!username) return;
fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${encodeURIComponent(username)}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
return response.text();
})
.then(text => {
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/xml');
const teamNodes = doc.querySelectorAll('Team[sport="soccer"]');
if (!teamNodes || !teamNodes.length) return;
const tid = teamNodes[0].getAttribute('teamId');
if (tid) {
GM_setValue('TEAM_ID', tid);
myTeamId = tid;
}
}).catch((_err) => {});
};
const run = () => {
initTeamId();
if (!canRunUserscript()) {
return;
}
const csi = getCurrentSeasonInfo();
if (!csi) {
return;
}
const getSeasonFn = getSeasonCalculator(csi);
insertButtons(getSeasonFn, csi);
const obs = new MutationObserver((mutations) => {
let playerContainerChanged = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
if (mutation.target.matches && (mutation.target.matches('.playerContainer') || mutation.target.querySelector('.playerContainer'))) {
playerContainerChanged = true;
break;
}
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && (node.matches('.playerContainer') || node.querySelector('.playerContainer'))) {
playerContainerChanged = true;
break;
}
}
if (playerContainerChanged) break;
}
}
if (playerContainerChanged) {
insertButtons(getSeasonFn, csi);
}
});
obs.observe(document.body, { childList: true, subtree: true });
};
run();
})();